add flash messages, preserve usernames/emails in forms
This commit is contained in:
parent
1af8333801
commit
b8b77351d0
@ -71,6 +71,124 @@ func (am *AuthManager) Close() error {
|
||||
return am.store.Close()
|
||||
}
|
||||
|
||||
// SetFlash stores a flash message in the session that will be removed after retrieval
|
||||
func (am *AuthManager) SetFlash(sessionID, key string, value any) bool {
|
||||
session, exists := am.store.Get(sessionID)
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
am.store.mu.Lock()
|
||||
defer am.store.mu.Unlock()
|
||||
|
||||
if session.Data == nil {
|
||||
session.Data = make(map[string]any)
|
||||
}
|
||||
|
||||
// Store flash messages under a special key
|
||||
flashData, ok := session.Data["_flash"].(map[string]any)
|
||||
if !ok {
|
||||
flashData = make(map[string]any)
|
||||
}
|
||||
flashData[key] = value
|
||||
session.Data["_flash"] = flashData
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetFlash retrieves and removes a flash message from the session
|
||||
func (am *AuthManager) GetFlash(sessionID, key string) (any, bool) {
|
||||
session, exists := am.store.Get(sessionID)
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
am.store.mu.Lock()
|
||||
defer am.store.mu.Unlock()
|
||||
|
||||
if session.Data == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
flashData, ok := session.Data["_flash"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
value, exists := flashData[key]
|
||||
if exists {
|
||||
delete(flashData, key)
|
||||
if len(flashData) == 0 {
|
||||
delete(session.Data, "_flash")
|
||||
} else {
|
||||
session.Data["_flash"] = flashData
|
||||
}
|
||||
}
|
||||
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// GetAllFlash retrieves and removes all flash messages from the session
|
||||
func (am *AuthManager) GetAllFlash(sessionID string) map[string]any {
|
||||
session, exists := am.store.Get(sessionID)
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
am.store.mu.Lock()
|
||||
defer am.store.mu.Unlock()
|
||||
|
||||
if session.Data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
flashData, ok := session.Data["_flash"].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove flash data from session
|
||||
delete(session.Data, "_flash")
|
||||
|
||||
return flashData
|
||||
}
|
||||
|
||||
// SetSessionData stores arbitrary data in the session
|
||||
func (am *AuthManager) SetSessionData(sessionID, key string, value any) bool {
|
||||
session, exists := am.store.Get(sessionID)
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
am.store.mu.Lock()
|
||||
defer am.store.mu.Unlock()
|
||||
|
||||
if session.Data == nil {
|
||||
session.Data = make(map[string]any)
|
||||
}
|
||||
|
||||
session.Data[key] = value
|
||||
return true
|
||||
}
|
||||
|
||||
// GetSessionData retrieves data from the session
|
||||
func (am *AuthManager) GetSessionData(sessionID, key string) (any, bool) {
|
||||
session, exists := am.store.Get(sessionID)
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
am.store.mu.RLock()
|
||||
defer am.store.mu.RUnlock()
|
||||
|
||||
if session.Data == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
value, exists := session.Data[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidCredentials = &AuthError{"invalid username/email or password"}
|
||||
ErrSessionNotFound = &AuthError{"session not found"}
|
||||
|
98
internal/auth/flash.go
Normal file
98
internal/auth/flash.go
Normal file
@ -0,0 +1,98 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"dk/internal/router"
|
||||
)
|
||||
|
||||
// FlashMessage represents a flash message with type and content
|
||||
type FlashMessage struct {
|
||||
Type string `json:"type"` // "error", "success", "warning", "info"
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// SetFlashMessage sets a flash message for the current session
|
||||
func SetFlashMessage(ctx router.Ctx, msgType, message string) bool {
|
||||
sessionID := GetSessionCookie(ctx)
|
||||
if sessionID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return Manager.SetFlash(sessionID, "message", FlashMessage{
|
||||
Type: msgType,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// GetFlashMessage retrieves and removes the flash message from the current session
|
||||
func GetFlashMessage(ctx router.Ctx) *FlashMessage {
|
||||
sessionID := GetSessionCookie(ctx)
|
||||
if sessionID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
value, exists := Manager.GetFlash(sessionID, "message")
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
if msg, ok := value.(FlashMessage); ok {
|
||||
return &msg
|
||||
}
|
||||
|
||||
// Handle map[string]interface{} from JSON deserialization
|
||||
if msgMap, ok := value.(map[string]interface{}); ok {
|
||||
msg := &FlashMessage{}
|
||||
if t, ok := msgMap["type"].(string); ok {
|
||||
msg.Type = t
|
||||
}
|
||||
if m, ok := msgMap["message"].(string); ok {
|
||||
msg.Message = m
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFormData stores form data temporarily in the session (for repopulating forms after errors)
|
||||
func SetFormData(ctx router.Ctx, data map[string]string) bool {
|
||||
sessionID := GetSessionCookie(ctx)
|
||||
if sessionID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return Manager.SetSessionData(sessionID, "form_data", data)
|
||||
}
|
||||
|
||||
// GetFormData retrieves and removes form data from the session
|
||||
func GetFormData(ctx router.Ctx) map[string]string {
|
||||
sessionID := GetSessionCookie(ctx)
|
||||
if sessionID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
value, exists := Manager.GetSessionData(sessionID, "form_data")
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear form data after retrieval
|
||||
Manager.SetSessionData(sessionID, "form_data", nil)
|
||||
|
||||
if formData, ok := value.(map[string]string); ok {
|
||||
return formData
|
||||
}
|
||||
|
||||
// Handle map[string]interface{} from JSON deserialization
|
||||
if formMap, ok := value.(map[string]interface{}); ok {
|
||||
result := make(map[string]string)
|
||||
for k, v := range formMap {
|
||||
if str, ok := v.(string); ok {
|
||||
result[k] = str
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -9,7 +9,6 @@ import (
|
||||
"dk/internal/middleware"
|
||||
"dk/internal/password"
|
||||
"dk/internal/router"
|
||||
"dk/internal/template"
|
||||
"dk/internal/template/components"
|
||||
"dk/internal/users"
|
||||
|
||||
@ -36,10 +35,24 @@ func RegisterAuthRoutes(r *router.Router) {
|
||||
|
||||
// showLogin displays the login form
|
||||
func showLogin(ctx router.Ctx, _ []string) {
|
||||
// Get flash message if any
|
||||
var errorHTML string
|
||||
if flash := auth.GetFlashMessage(ctx); flash != nil {
|
||||
errorHTML = fmt.Sprintf(`<div style="color: red; margin-bottom: 1rem;">%s</div>`, flash.Message)
|
||||
}
|
||||
|
||||
// Get form data if any (for preserving email/username on error)
|
||||
formData := auth.GetFormData(ctx)
|
||||
id := ""
|
||||
if formData != nil {
|
||||
id = formData["id"]
|
||||
}
|
||||
|
||||
components.RenderPageTemplate(ctx, "Log In", "auth/login.html", map[string]any{
|
||||
"csrf_token": csrf.GetToken(ctx, auth.Manager),
|
||||
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
|
||||
"error_message": "",
|
||||
"error_message": errorHTML,
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
@ -51,17 +64,21 @@ func processLogin(ctx router.Ctx, _ []string) {
|
||||
return
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
|
||||
email := strings.TrimSpace(string(ctx.PostArgs().Peek("id")))
|
||||
userPassword := string(ctx.PostArgs().Peek("password"))
|
||||
|
||||
if email == "" || userPassword == "" {
|
||||
showLoginError(ctx, "Email and password are required")
|
||||
auth.SetFlashMessage(ctx, "error", "Email and password are required")
|
||||
auth.SetFormData(ctx, map[string]string{"id": email})
|
||||
ctx.Redirect("/login", fasthttp.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := auth.Manager.Authenticate(email, userPassword)
|
||||
if err != nil {
|
||||
showLoginError(ctx, "Invalid email or password")
|
||||
auth.SetFlashMessage(ctx, "error", "Invalid email or password")
|
||||
auth.SetFormData(ctx, map[string]string{"id": email})
|
||||
ctx.Redirect("/login", fasthttp.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
@ -79,12 +96,27 @@ func processLogin(ctx router.Ctx, _ []string) {
|
||||
|
||||
// showRegister displays the registration form
|
||||
func showRegister(ctx router.Ctx, _ []string) {
|
||||
// Get flash message if any
|
||||
var errorHTML string
|
||||
if flash := auth.GetFlashMessage(ctx); flash != nil {
|
||||
errorHTML = fmt.Sprintf(`<div style="color: red; margin-bottom: 1rem;">%s</div>`, flash.Message)
|
||||
}
|
||||
|
||||
// Get form data if any (for preserving values on error)
|
||||
formData := auth.GetFormData(ctx)
|
||||
username := ""
|
||||
email := ""
|
||||
if formData != nil {
|
||||
username = formData["username"]
|
||||
email = formData["email"]
|
||||
}
|
||||
|
||||
components.RenderPageTemplate(ctx, "Register", "auth/register.html", map[string]any{
|
||||
"csrf_token": csrf.GetToken(ctx, auth.Manager),
|
||||
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
|
||||
"error_message": "",
|
||||
"username": "",
|
||||
"email": "",
|
||||
"error_message": errorHTML,
|
||||
"username": username,
|
||||
"email": email,
|
||||
})
|
||||
}
|
||||
|
||||
@ -102,17 +134,32 @@ func processRegister(ctx router.Ctx, _ []string) {
|
||||
confirmPassword := string(ctx.PostArgs().Peek("confirm_password"))
|
||||
|
||||
if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil {
|
||||
showRegisterError(ctx, err.Error(), username, email)
|
||||
auth.SetFlashMessage(ctx, "error", err.Error())
|
||||
auth.SetFormData(ctx, map[string]string{
|
||||
"username": username,
|
||||
"email": email,
|
||||
})
|
||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := users.GetByUsername(username); err == nil {
|
||||
showRegisterError(ctx, "Username already exists", username, email)
|
||||
auth.SetFlashMessage(ctx, "error", "Username already exists")
|
||||
auth.SetFormData(ctx, map[string]string{
|
||||
"username": username,
|
||||
"email": email,
|
||||
})
|
||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := users.GetByEmail(email); err == nil {
|
||||
showRegisterError(ctx, "Email already registered", username, email)
|
||||
auth.SetFlashMessage(ctx, "error", "Email already registered")
|
||||
auth.SetFormData(ctx, map[string]string{
|
||||
"username": username,
|
||||
"email": email,
|
||||
})
|
||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
@ -124,7 +171,12 @@ func processRegister(ctx router.Ctx, _ []string) {
|
||||
user.Auth = 1
|
||||
|
||||
if err := user.Insert(); err != nil {
|
||||
showRegisterError(ctx, "Failed to create account", username, email)
|
||||
auth.SetFlashMessage(ctx, "error", "Failed to create account")
|
||||
auth.SetFormData(ctx, map[string]string{
|
||||
"username": username,
|
||||
"email": email,
|
||||
})
|
||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
@ -156,68 +208,6 @@ func processLogout(ctx router.Ctx, params []string) {
|
||||
|
||||
// Helper functions
|
||||
|
||||
func showLoginError(ctx router.Ctx, errorMsg string) {
|
||||
loginTmpl, err := template.Cache.Load("auth/login.html")
|
||||
if err != nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
fmt.Fprintf(ctx, "Template error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var errorHTML string
|
||||
if errorMsg != "" {
|
||||
errorHTML = fmt.Sprintf(`<div style="color: red; margin-bottom: 1rem;">%s</div>`, errorMsg)
|
||||
}
|
||||
|
||||
loginFormData := map[string]any{
|
||||
"csrf_token": csrf.GetToken(ctx, auth.Manager),
|
||||
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
|
||||
"error_message": errorHTML,
|
||||
}
|
||||
|
||||
loginContent := loginTmpl.RenderNamed(loginFormData)
|
||||
|
||||
ctx.SetStatusCode(fasthttp.StatusBadRequest)
|
||||
pageData := components.NewPageData("Login - Dragon Knight", loginContent)
|
||||
if err := components.RenderPage(ctx, pageData, nil); err != nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
fmt.Fprintf(ctx, "Template error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func showRegisterError(ctx router.Ctx, errorMsg, username, email string) {
|
||||
registerTmpl, err := template.Cache.Load("auth/register.html")
|
||||
if err != nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
fmt.Fprintf(ctx, "Template error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var errorHTML string
|
||||
if errorMsg != "" {
|
||||
errorHTML = fmt.Sprintf(`<div style="color: red; margin-bottom: 1rem;">%s</div>`, errorMsg)
|
||||
}
|
||||
|
||||
registerFormData := map[string]any{
|
||||
"csrf_token": csrf.GetToken(ctx, auth.Manager),
|
||||
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
|
||||
"error_message": errorHTML,
|
||||
"username": username,
|
||||
"email": email,
|
||||
}
|
||||
|
||||
registerContent := registerTmpl.RenderNamed(registerFormData)
|
||||
|
||||
ctx.SetStatusCode(fasthttp.StatusBadRequest)
|
||||
pageData := components.NewPageData("Register - Dragon Knight", registerContent)
|
||||
if err := components.RenderPage(ctx, pageData, nil); err != nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
fmt.Fprintf(ctx, "Template error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func validateRegistration(username, email, password, confirmPassword string) error {
|
||||
if username == "" {
|
||||
return fmt.Errorf("username is required")
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
<div class="row">
|
||||
<label for="id">Email/Username</label>
|
||||
<input id="id" type="text" name="id" required>
|
||||
<input id="id" type="text" name="id" value="{id}" required>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
@ -1,11 +1,11 @@
|
||||
<div class="town">
|
||||
<div class="options">
|
||||
<div class="title"><img src="/assets/images/town_{town.ID}.gif" alt="Welcome to {town.Name}" title="Welcome to {town.Name}"></div>
|
||||
<b>Town Options:</b><br>
|
||||
<ul hx-boost="true" hx-target="#middle">
|
||||
<li><a href="/inn">Rest at the Inn</a></li>
|
||||
<li><a href="/shop">Browse the Shop</a></li>
|
||||
<li><a href="/maps">Buy Maps</a></li>
|
||||
<b>Town Options</b><br>
|
||||
<ul class="unstyled">
|
||||
<li><a href="/town/inn">Rest at the Inn</a></li>
|
||||
<li><a href="/town/shop">Browse the Shop</a></li>
|
||||
<li><a href="/town/maps">Buy Maps</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user