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()
|
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 (
|
var (
|
||||||
ErrInvalidCredentials = &AuthError{"invalid username/email or password"}
|
ErrInvalidCredentials = &AuthError{"invalid username/email or password"}
|
||||||
ErrSessionNotFound = &AuthError{"session not found"}
|
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/middleware"
|
||||||
"dk/internal/password"
|
"dk/internal/password"
|
||||||
"dk/internal/router"
|
"dk/internal/router"
|
||||||
"dk/internal/template"
|
|
||||||
"dk/internal/template/components"
|
"dk/internal/template/components"
|
||||||
"dk/internal/users"
|
"dk/internal/users"
|
||||||
|
|
||||||
@ -36,10 +35,24 @@ func RegisterAuthRoutes(r *router.Router) {
|
|||||||
|
|
||||||
// showLogin displays the login form
|
// showLogin displays the login form
|
||||||
func showLogin(ctx router.Ctx, _ []string) {
|
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{
|
components.RenderPageTemplate(ctx, "Log In", "auth/login.html", map[string]any{
|
||||||
"csrf_token": csrf.GetToken(ctx, auth.Manager),
|
"csrf_token": csrf.GetToken(ctx, auth.Manager),
|
||||||
"csrf_field": csrf.HiddenField(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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
|
email := strings.TrimSpace(string(ctx.PostArgs().Peek("id")))
|
||||||
userPassword := string(ctx.PostArgs().Peek("password"))
|
userPassword := string(ctx.PostArgs().Peek("password"))
|
||||||
|
|
||||||
if email == "" || userPassword == "" {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := auth.Manager.Authenticate(email, userPassword)
|
user, err := auth.Manager.Authenticate(email, userPassword)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,12 +96,27 @@ func processLogin(ctx router.Ctx, _ []string) {
|
|||||||
|
|
||||||
// showRegister displays the registration form
|
// showRegister displays the registration form
|
||||||
func showRegister(ctx router.Ctx, _ []string) {
|
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{
|
components.RenderPageTemplate(ctx, "Register", "auth/register.html", map[string]any{
|
||||||
"csrf_token": csrf.GetToken(ctx, auth.Manager),
|
"csrf_token": csrf.GetToken(ctx, auth.Manager),
|
||||||
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
|
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
|
||||||
"error_message": "",
|
"error_message": errorHTML,
|
||||||
"username": "",
|
"username": username,
|
||||||
"email": "",
|
"email": email,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,17 +134,32 @@ func processRegister(ctx router.Ctx, _ []string) {
|
|||||||
confirmPassword := string(ctx.PostArgs().Peek("confirm_password"))
|
confirmPassword := string(ctx.PostArgs().Peek("confirm_password"))
|
||||||
|
|
||||||
if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := users.GetByUsername(username); err == nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := users.GetByEmail(email); err == nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +171,12 @@ func processRegister(ctx router.Ctx, _ []string) {
|
|||||||
user.Auth = 1
|
user.Auth = 1
|
||||||
|
|
||||||
if err := user.Insert(); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,68 +208,6 @@ func processLogout(ctx router.Ctx, params []string) {
|
|||||||
|
|
||||||
// Helper functions
|
// 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 {
|
func validateRegistration(username, email, password, confirmPassword string) error {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return fmt.Errorf("username is required")
|
return fmt.Errorf("username is required")
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="id">Email/Username</label>
|
<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>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<div class="town">
|
<div class="town">
|
||||||
<div class="options">
|
<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>
|
<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>
|
<b>Town Options</b><br>
|
||||||
<ul hx-boost="true" hx-target="#middle">
|
<ul class="unstyled">
|
||||||
<li><a href="/inn">Rest at the Inn</a></li>
|
<li><a href="/town/inn">Rest at the Inn</a></li>
|
||||||
<li><a href="/shop">Browse the Shop</a></li>
|
<li><a href="/town/shop">Browse the Shop</a></li>
|
||||||
<li><a href="/maps">Buy Maps</a></li>
|
<li><a href="/town/maps">Buy Maps</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user