Compare commits
6 Commits
4a6bfad880
...
c9f3863a25
| Author | SHA1 | Date | |
|---|---|---|---|
| c9f3863a25 | |||
| 76cdb96966 | |||
| 29cb50bb34 | |||
| ffcc340034 | |||
| af581a4def | |||
| e0b063fab0 |
@@ -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, ".")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user