Compare commits

...

6 Commits

6 changed files with 101 additions and 52 deletions

View File

@@ -1,4 +1,3 @@
// internal/handlers/account/signup.go
package accountHandler package accountHandler
import ( import (
@@ -20,7 +19,6 @@ import (
"github.com/justinas/nosurf" "github.com/justinas/nosurf"
) )
// kept for handler-local parsing only (NOT stored in session)
type registerForm struct { type registerForm struct {
Username string Username string
Email string Email string
@@ -39,7 +37,6 @@ func SignupGet(c *gin.Context) {
} }
ctx["CSRFToken"] = nosurf.Token(c.Request) ctx["CSRFToken"] = nosurf.Token(c.Request)
// Rehydrate maps (not structs) from session for sticky form + field errors
if v := sm.Pop(c.Request.Context(), "register.form"); v != nil { if v := sm.Pop(c.Request.Context(), "register.form"); v != nil {
if fm, ok := v.(map[string]string); ok { if fm, ok := v.(map[string]string); ok {
ctx["Form"] = fm ctx["Form"] = fm
@@ -51,11 +48,7 @@ func SignupGet(c *gin.Context) {
} }
} }
// layout-first, finalized path tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/signup.html")
tmpl := templateHelpers.LoadTemplateFiles(
"web/templates/layout.html",
"web/templates/account/signup.html",
)
c.Status(http.StatusOK) c.Status(http.StatusOK)
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
@@ -79,9 +72,8 @@ func SignupPost(c *gin.Context) {
AcceptTerms: r.FormValue("accept_terms") == "on", AcceptTerms: r.FormValue("accept_terms") == "on",
} }
errors := validateRegisterForm(db, form) errMap := validateRegisterForm(db, form)
if len(errors) > 0 { if len(errMap) > 0 {
// ✅ Stash maps instead of a struct → gob-safe with SCS
formMap := map[string]string{ formMap := map[string]string{
"username": form.Username, "username": form.Username,
"email": form.Email, "email": form.Email,
@@ -93,7 +85,7 @@ func SignupPost(c *gin.Context) {
}(), }(),
} }
sm.Put(r.Context(), "register.form", formMap) sm.Put(r.Context(), "register.form", formMap)
sm.Put(r.Context(), "register.errors", errors) sm.Put(r.Context(), "register.errors", errMap)
sm.Put(r.Context(), "flash", "Please fix the highlighted errors.") sm.Put(r.Context(), "flash", "Please fix the highlighted errors.")
c.Redirect(http.StatusSeeOther, "/account/signup") c.Redirect(http.StatusSeeOther, "/account/signup")
@@ -101,7 +93,6 @@ func SignupPost(c *gin.Context) {
return return
} }
// Hash password
hash, err := securityHelpers.HashPassword(form.Password) hash, err := securityHelpers.HashPassword(form.Password)
if err != nil { if err != nil {
logging.Info("❌ Hash error: %v", err) logging.Info("❌ Hash error: %v", err)
@@ -111,18 +102,15 @@ func SignupPost(c *gin.Context) {
return return
} }
// Create user
id, err := usersStorage.CreateUser(db, form.Username, form.Email, hash) id, err := usersStorage.CreateUser(db, form.Username, form.Email, hash)
if err != nil { if err != nil {
logging.Info("❌ CreateUser error: %v", err) logging.Info("❌ CreateUser error: %v", err)
// Unique constraints might still trip here
sm.Put(r.Context(), "flash", "That username or email is already taken.") sm.Put(r.Context(), "flash", "That username or email is already taken.")
c.Redirect(http.StatusSeeOther, "/account/signup") c.Redirect(http.StatusSeeOther, "/account/signup")
c.Abort() c.Abort()
return return
} }
// Audit registration
auditlogStorage.LogSignup( auditlogStorage.LogSignup(
db, db,
id, id,
@@ -165,6 +153,5 @@ func validateRegisterForm(db *sql.DB, f registerForm) map[string]string {
} }
func looksLikeEmail(s string) bool { func looksLikeEmail(s string) bool {
// Keep it simple; you can swap for a stricter validator later
return strings.Count(s, "@") == 1 && strings.Contains(s, ".") return strings.Count(s, "@") == 1 && strings.Contains(s, ".")
} }

View File

@@ -32,6 +32,7 @@ package bootstrap
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/gob"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
@@ -79,10 +80,15 @@ func Load(configPath string) (*App, error) {
return nil, fmt.Errorf("ensure schema: %w", err) return nil, fmt.Errorf("ensure schema: %w", err)
} }
gob.Register(map[string]string{})
gob.Register([]string{})
gob.Register(time.Time{})
sessions := session.New(cfg) sessions := session.New(cfg)
router := gin.New() router := gin.New()
router.Use(gin.Logger(), gin.Recovery()) //router.Use(gin.Logger(), gin.Recovery())
router.Use(gin.Logger())
router.Static("/static", "./web/static") router.Static("/static", "./web/static")
router.StaticFile("/favicon.ico", "./web/static/favicon.ico") router.StaticFile("/favicon.ico", "./web/static/favicon.ico")

View File

@@ -1,7 +1,6 @@
package session package session
import ( import (
"encoding/gob"
"net/http" "net/http"
"time" "time"
@@ -11,7 +10,6 @@ import (
) )
func New(cfg config.Config) *scs.SessionManager { func New(cfg config.Config) *scs.SessionManager {
gob.Register(time.Time{})
s := scs.New() s := scs.New()
// Lifetime (absolute max age) // Lifetime (absolute max age)

View File

@@ -4,6 +4,17 @@
-- - utf8mb4 for full Unicode -- - utf8mb4 for full Unicode
-- Booleans are TINYINT(1). Dates use DATE/DATETIME/TIMESTAMP as appropriate. -- Booleans are TINYINT(1). Dates use DATE/DATETIME/TIMESTAMP as appropriate.
-- USERS
CREATE TABLE IF NOT EXISTS users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(191) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
is_admin TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE audit_registration ( CREATE TABLE audit_registration (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL,
@@ -18,14 +29,6 @@ CREATE TABLE audit_registration (
ON DELETE CASCADE ON DELETE CASCADE
); );
-- USERS
CREATE TABLE IF NOT EXISTS users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(191) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
is_admin TINYINT(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- THUNDERBALL RESULTS -- THUNDERBALL RESULTS
CREATE TABLE IF NOT EXISTS results_thunderball ( CREATE TABLE IF NOT EXISTS results_thunderball (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,

View File

@@ -1,43 +1,103 @@
{{ define "content" }} {{ define "content" }}
<h2>Create your account</h2> <h2>Create your account</h2>
{{ if .Flash }}<div class="flash">{{ .Flash }}</div>{{ end }}
{{ if .Flash }}
<div class="alert alert-warning" role="alert">{{ .Flash }}</div>
{{ end }}
<form method="POST" action="/account/signup" class="form"> <form method="POST" action="/account/signup" class="form">
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
{{ $form := .Form }}
{{ $errs := .Errors }}
<div class="mb-3"> <div class="mb-3">
<label for="username">Username</label> <label for="username" class="form-label">Username</label>
<input type="text" name="username" id="username" required class="form-control" <input
value="{{ with .Form }}{{ .Username }}{{ end }}"> type="text"
{{ with .Errors }}{{ with index . "username" }}<div class="error">{{ . }}</div>{{ end }}{{ end }} name="username"
id="username"
class="form-control {{ if $errs }}{{ if index $errs "username" }}is-invalid{{ end }}{{ end }}"
required
value="{{ if $form }}{{ index $form "username" }}{{ end }}"
autocomplete="username"
>
{{ if $errs }}
{{ with index $errs "username" }}
<div class="invalid-feedback">{{ . }}</div>
{{ end }}
{{ end }}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="email">Email</label> <label for="email" class="form-label">Email</label>
<input type="email" name="email" id="email" required class="form-control" <input
value="{{ with .Form }}{{ .Email }}{{ end }}"> type="email"
{{ with .Errors }}{{ with index . "email" }}<div class="error">{{ . }}</div>{{ end }}{{ end }} name="email"
id="email"
class="form-control {{ if $errs }}{{ if index $errs "email" }}is-invalid{{ end }}{{ end }}"
required
value="{{ if $form }}{{ index $form "email" }}{{ end }}"
autocomplete="email"
>
{{ if $errs }}
{{ with index $errs "email" }}
<div class="invalid-feedback">{{ . }}</div>
{{ end }}
{{ end }}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password">Password</label> <label for="password" class="form-label">Password</label>
<input type="password" name="password" id="password" required class="form-control"> <input
{{ with .Errors }}{{ with index . "password" }}<div class="error">{{ . }}</div>{{ end }}{{ end }} type="password"
name="password"
id="password"
class="form-control {{ if $errs }}{{ if index $errs "password" }}is-invalid{{ end }}{{ end }}"
required
autocomplete="new-password"
>
{{ if $errs }}
{{ with index $errs "password" }}
<div class="invalid-feedback">{{ . }}</div>
{{ end }}
{{ end }}
<div class="form-text">Minimum 8 characters.</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password_confirm">Confirm Password</label> <label for="password_confirm" class="form-label">Confirm Password</label>
<input type="password" name="password_confirm" id="password_confirm" required class="form-control"> <input
{{ with .Errors }}{{ with index . "password_confirm" }}<div class="error">{{ . }}</div>{{ end }}{{ end }} type="password"
name="password_confirm"
id="password_confirm"
class="form-control {{ if $errs }}{{ if index $errs "password_confirm" }}is-invalid{{ end }}{{ end }}"
required
autocomplete="new-password"
>
{{ if $errs }}
{{ with index $errs "password_confirm" }}
<div class="invalid-feedback">{{ . }}</div>
{{ end }}
{{ end }}
</div> </div>
<div class="form-check mb-3"> <div class="form-check mb-3">
<input type="checkbox" name="accept_terms" id="accept_terms" class="form-check-input" <input
{{ with .Form }}{{ if .AcceptTerms }}checked{{ end }}{{ end }}> type="checkbox"
name="accept_terms"
id="accept_terms"
class="form-check-input {{ if $errs }}{{ if index $errs "accept_terms" }}is-invalid{{ end }}{{ end }}"
{{ if $form }}{{ if eq (index $form "accept_terms") "on" }}checked{{ end }}{{ end }}
>
<label for="accept_terms" class="form-check-label">I accept the terms</label> <label for="accept_terms" class="form-check-label">I accept the terms</label>
{{ with .Errors }}{{ with index . "accept_terms" }}<div class="error">{{ . }}</div>{{ end }}{{ end }} {{ if $errs }}
{{ with index $errs "accept_terms" }}
<div class="invalid-feedback d-block">{{ . }}</div>
{{ end }}
{{ end }}
</div> </div>
<button type="submit" class="btn btn-primary">Create account</button> <button type="submit" class="btn btn-primary">Create account</button>
</form> </form>
{{ end }} {{ end }}

View File

@@ -66,11 +66,6 @@
<!-- Main Content --> <!-- Main Content -->
<main class="col px-md-4 pt-4"> <main class="col px-md-4 pt-4">
{{ if .Flash }}
<div class="alert alert-info" role="alert">
{{ .Flash }}
</div>
{{ end }}
{{ template "content" . }} {{ template "content" . }}
</main> </main>
</div> </div>