package accountHandler import ( "database/sql" "net/http" "strings" httphelpers "synlotto-website/internal/helpers/http" securityHelpers "synlotto-website/internal/helpers/security" templateHelpers "synlotto-website/internal/helpers/template" auditlogStorage "synlotto-website/internal/storage/auditlog" usersStorage "synlotto-website/internal/storage/users" "synlotto-website/internal/logging" "synlotto-website/internal/models" "synlotto-website/internal/platform/bootstrap" "github.com/gin-gonic/gin" "github.com/justinas/nosurf" ) type registerForm struct { Username string Email string Password string PasswordConfirm string AcceptTerms bool } func SignupGet(c *gin.Context) { app := c.MustGet("app").(*bootstrap.App) sm := app.SessionManager ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) if f := sm.PopString(c.Request.Context(), "flash"); f != "" { ctx["Flash"] = f } ctx["CSRFToken"] = nosurf.Token(c.Request) if v := sm.Pop(c.Request.Context(), "register.form"); v != nil { if fm, ok := v.(map[string]string); ok { ctx["Form"] = fm } } if v := sm.Pop(c.Request.Context(), "register.errors"); v != nil { if errs, ok := v.(map[string]string); ok { ctx["Errors"] = errs } } tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/signup.html") c.Status(http.StatusOK) if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { logging.Info("❌ Template render error (register): %v", err) c.String(http.StatusInternalServerError, "Error rendering register page") } } func SignupPost(c *gin.Context) { app := c.MustGet("app").(*bootstrap.App) sm := app.SessionManager db := app.DB r := c.Request form := registerForm{ Username: strings.TrimSpace(r.FormValue("username")), Email: strings.TrimSpace(r.FormValue("email")), Password: r.FormValue("password"), PasswordConfirm: r.FormValue("password_confirm"), AcceptTerms: r.FormValue("accept_terms") == "on", } errMap := validateRegisterForm(db, form) if len(errMap) > 0 { formMap := map[string]string{ "username": form.Username, "email": form.Email, "accept_terms": func() string { if form.AcceptTerms { return "on" } return "" }(), } sm.Put(r.Context(), "register.form", formMap) sm.Put(r.Context(), "register.errors", errMap) sm.Put(r.Context(), "flash", "Please fix the highlighted errors.") c.Redirect(http.StatusSeeOther, "/account/signup") c.Abort() return } hash, err := securityHelpers.HashPassword(form.Password) if err != nil { logging.Info("❌ Hash error: %v", err) sm.Put(r.Context(), "flash", "Something went wrong. Please try again.") c.Redirect(http.StatusSeeOther, "/account/signup") c.Abort() return } id, err := usersStorage.CreateUser(db, form.Username, form.Email, hash) if err != nil { logging.Info("❌ CreateUser error: %v", err) sm.Put(r.Context(), "flash", "That username or email is already taken.") c.Redirect(http.StatusSeeOther, "/account/signup") c.Abort() return } auditlogStorage.LogSignup( db, id, form.Username, form.Email, httphelpers.ClientIP(r), r.UserAgent(), ) sm.Put(r.Context(), "flash", "Account created. You can log in now.") c.Redirect(http.StatusSeeOther, "/account/login") c.Abort() } func validateRegisterForm(db *sql.DB, f registerForm) map[string]string { errs := make(map[string]string) if f.Username == "" || len(f.Username) < 3 { errs["username"] = "Username must be at least 3 characters." } else if usersStorage.UsernameExists(db, f.Username) { errs["username"] = "Username is already in use." } if f.Email == "" || !looksLikeEmail(f.Email) { errs["email"] = "Please enter a valid email." } else if usersStorage.EmailExists(db, f.Email) { errs["email"] = "Email is already registered." } if len(f.Password) < 8 { errs["password"] = "Password must be at least 8 characters." } if f.Password != f.PasswordConfirm { errs["password_confirm"] = "Passwords do not match." } if !f.AcceptTerms { errs["accept_terms"] = "You must accept the terms." } return errs } func looksLikeEmail(s string) bool { return strings.Count(s, "@") == 1 && strings.Contains(s, ".") }