update form layouts, add button styles, clean up auth routes

This commit is contained in:
Sky Johnson 2025-08-11 12:07:06 -05:00
parent 5ac348a2d2
commit 1af8333801
5 changed files with 211 additions and 216 deletions

View File

@ -162,4 +162,52 @@ form#move-compass {
height: 38px; height: 38px;
} }
} }
}
button.btn {
font-family: inherit;
font-size: 1rem;
appearance: none;
outline: none;
background-color: #808080;
background-image: url("/assets/images/overlay.png");
border: 1px solid #808080;
padding: 0.5rem 1rem;
cursor: pointer;
color: white;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2);
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1);
&:hover {
background-color: #909090;
}
}
form.standard {
& > div.row {
display: flex;
flex-direction: column;
margin-bottom: 1.5rem;
label {
font-weight: bold;
}
}
span.help {
font-size: 0.8em;
color: #555;
}
& > div.actions {
margin-top: 1rem;
}
}
.mb-1 {
margin-bottom: 1rem;
}
.mb-05 {
margin-bottom: 0.5rem;
} }

BIN
assets/images/overlay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

View File

@ -6,7 +6,6 @@ import (
"dk/internal/auth" "dk/internal/auth"
"dk/internal/csrf" "dk/internal/csrf"
"dk/internal/database"
"dk/internal/middleware" "dk/internal/middleware"
"dk/internal/password" "dk/internal/password"
"dk/internal/router" "dk/internal/router"
@ -21,174 +20,138 @@ import (
func RegisterAuthRoutes(r *router.Router) { func RegisterAuthRoutes(r *router.Router) {
// Guest routes // Guest routes
guestGroup := r.Group("") guestGroup := r.Group("")
guestGroup.Use(middleware.RequireGuest("/")) guestGroup.Use(middleware.RequireGuest())
guestGroup.Get("/login", showLogin) guestGroup.Get("/login", showLogin)
guestGroup.Post("/login", processLogin()) guestGroup.Post("/login", processLogin)
guestGroup.Get("/register", showRegister) guestGroup.Get("/register", showRegister)
guestGroup.Post("/register", processRegister()) guestGroup.Post("/register", processRegister)
// Authenticated routes // Authenticated routes
authGroup := r.Group("") authGroup := r.Group("")
authGroup.Use(middleware.RequireAuth("/login")) authGroup.Use(middleware.RequireAuth())
authGroup.Post("/logout", processLogout()) authGroup.Post("/logout", processLogout)
} }
// showLogin displays the login form // showLogin displays the login form
func showLogin(ctx router.Ctx, params []string) { func showLogin(ctx router.Ctx, _ []string) {
loginTmpl, err := template.Cache.Load("auth/login.html") components.RenderPageTemplate(ctx, "Log In", "auth/login.html", map[string]any{
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
loginFormData := 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": "",
} })
loginContent := loginTmpl.RenderNamed(loginFormData)
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
}
} }
// processLogin handles login form submission // processLogin handles login form submission
func processLogin() router.Handler { func processLogin(ctx router.Ctx, _ []string) {
return func(ctx router.Ctx, params []string) { if !csrf.ValidateFormToken(ctx, auth.Manager) {
if !csrf.ValidateFormToken(ctx, auth.Manager) { ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetStatusCode(fasthttp.StatusForbidden) ctx.WriteString("CSRF validation failed")
ctx.WriteString("CSRF validation failed") return
return
}
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
userPassword := string(ctx.PostArgs().Peek("password"))
if email == "" || userPassword == "" {
showLoginError(ctx, "Email and password are required")
return
}
user, err := auth.Manager.Authenticate(email, userPassword)
if err != nil {
showLoginError(ctx, "Invalid email or password")
return
}
middleware.Login(ctx, auth.Manager, user)
// Transfer CSRF token from cookie to session for authenticated user
if cookieToken := csrf.GetTokenFromCookie(ctx); cookieToken != "" {
if session := csrf.GetCurrentSession(ctx); session != nil {
csrf.StoreToken(session, cookieToken)
}
}
ctx.Redirect("/", fasthttp.StatusFound)
} }
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
userPassword := string(ctx.PostArgs().Peek("password"))
if email == "" || userPassword == "" {
showLoginError(ctx, "Email and password are required")
return
}
user, err := auth.Manager.Authenticate(email, userPassword)
if err != nil {
showLoginError(ctx, "Invalid email or password")
return
}
middleware.Login(ctx, auth.Manager, user)
// Transfer CSRF token from cookie to session for authenticated user
if cookieToken := csrf.GetTokenFromCookie(ctx); cookieToken != "" {
if session := csrf.GetCurrentSession(ctx); session != nil {
csrf.StoreToken(session, cookieToken)
}
}
ctx.Redirect("/", fasthttp.StatusFound)
} }
// showRegister displays the registration form // showRegister displays the registration form
func showRegister(ctx router.Ctx, _ []string) { func showRegister(ctx router.Ctx, _ []string) {
registerTmpl, err := template.Cache.Load("auth/register.html") components.RenderPageTemplate(ctx, "Register", "auth/register.html", map[string]any{
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
registerContent := registerTmpl.RenderNamed(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": "",
"username": "", "username": "",
"email": "", "email": "",
}) })
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
}
} }
// processRegister handles registration form submission // processRegister handles registration form submission
func processRegister() router.Handler { func processRegister(ctx router.Ctx, _ []string) {
return func(ctx router.Ctx, params []string) { if !csrf.ValidateFormToken(ctx, auth.Manager) {
if !csrf.ValidateFormToken(ctx, auth.Manager) { ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetStatusCode(fasthttp.StatusForbidden) ctx.WriteString("CSRF validation failed")
ctx.WriteString("CSRF validation failed") return
return
}
username := strings.TrimSpace(string(ctx.PostArgs().Peek("username")))
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
userPassword := string(ctx.PostArgs().Peek("password"))
confirmPassword := string(ctx.PostArgs().Peek("confirm_password"))
if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil {
showRegisterError(ctx, err.Error(), username, email)
return
}
if _, err := users.GetByUsername(username); err == nil {
showRegisterError(ctx, "Username already exists", username, email)
return
}
if _, err := users.GetByEmail(email); err == nil {
showRegisterError(ctx, "Email already registered", username, email)
return
}
user := users.New()
user.Username = username
user.Email = email
user.Password = password.Hash(userPassword)
user.ClassID = 1
user.Auth = 1
if err := user.Insert(); err != nil {
showRegisterError(ctx, "Failed to create account", username, email)
return
}
// Auto-login after registration
middleware.Login(ctx, auth.Manager, user)
// Transfer CSRF token from cookie to session for authenticated user
if cookieToken := csrf.GetTokenFromCookie(ctx); cookieToken != "" {
if session := csrf.GetCurrentSession(ctx); session != nil {
csrf.StoreToken(session, cookieToken)
}
}
ctx.Redirect("/", fasthttp.StatusFound)
} }
username := strings.TrimSpace(string(ctx.PostArgs().Peek("username")))
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
userPassword := string(ctx.PostArgs().Peek("password"))
confirmPassword := string(ctx.PostArgs().Peek("confirm_password"))
if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil {
showRegisterError(ctx, err.Error(), username, email)
return
}
if _, err := users.GetByUsername(username); err == nil {
showRegisterError(ctx, "Username already exists", username, email)
return
}
if _, err := users.GetByEmail(email); err == nil {
showRegisterError(ctx, "Email already registered", username, email)
return
}
user := users.New()
user.Username = username
user.Email = email
user.Password = password.Hash(userPassword)
user.ClassID = 1
user.Auth = 1
if err := user.Insert(); err != nil {
showRegisterError(ctx, "Failed to create account", username, email)
return
}
// Auto-login after registration
middleware.Login(ctx, auth.Manager, user)
// Transfer CSRF token from cookie to session for authenticated user
if cookieToken := csrf.GetTokenFromCookie(ctx); cookieToken != "" {
if session := csrf.GetCurrentSession(ctx); session != nil {
csrf.StoreToken(session, cookieToken)
}
}
ctx.Redirect("/", fasthttp.StatusFound)
} }
// processLogout handles logout // processLogout handles logout
func processLogout() router.Handler { func processLogout(ctx router.Ctx, params []string) {
return func(ctx router.Ctx, params []string) { // Validate CSRF token
// Validate CSRF token if !csrf.ValidateFormToken(ctx, auth.Manager) {
if !csrf.ValidateFormToken(ctx, auth.Manager) { ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetStatusCode(fasthttp.StatusForbidden) ctx.WriteString("CSRF validation failed")
ctx.WriteString("CSRF validation failed") return
return
}
middleware.Logout(ctx, auth.Manager)
ctx.Redirect("/", fasthttp.StatusFound)
} }
middleware.Logout(ctx, auth.Manager)
ctx.Redirect("/", fasthttp.StatusFound)
} }
// Helper functions // Helper functions
@ -279,23 +242,3 @@ func validateRegistration(username, email, password, confirmPassword string) err
} }
return nil return nil
} }
// createUser inserts a new user into the database
// This is a simplified version - in a real app you'd have a proper users.Create function
func createUser(user *users.User) error {
query := `INSERT INTO users (username, password, email, verified, auth) VALUES (?, ?, ?, ?, ?)`
err := database.Exec(query, user.Username, user.Password, user.Email, user.Verified, user.Auth)
if err != nil {
return fmt.Errorf("failed to insert user: %w", err)
}
// Get the user ID (simplified - in real app you'd return it from insert)
createdUser, err := users.GetByUsername(user.Username)
if err != nil {
return fmt.Errorf("failed to get created user: %w", err)
}
user.ID = createdUser.ID
return nil
}

View File

@ -1,28 +1,30 @@
<h1>Log In</h1>
{error_message} {error_message}
<form action="/login" method="post"> <form class="standard mb-1" action="/login" method="post">
{csrf_field} {csrf_field}
<table width="75%">
<tr>
<td width="30%">Email/Username:</td>
<td><input type="text" size="30" name="email" required></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" size="30" name="password" required></td>
</tr>
<tr>
<td colspan="2"><input type="submit" name="submit" value="Log In"></td>
</tr>
<tr>
<td colspan="2">
Want to play? You gotta <a href="/register">register your own character.</a>
<br><br> <div class="row">
<label for="id">Email/Username</label>
<input id="id" type="text" name="id" required>
</div>
You may also <a href="/change-password">change your password</a>, or <div class="row">
<a href="/lost-password">request a new one</a> if you've lost yours. <label for="password">Password</label>
</td> <input id="password" type="password" name="password" required>
</tr> </div>
</table>
<div class="actions">
<button class="btn" type="submit">Log in</button>
</div>
</form> </form>
<p class="mb-1">
Want to play? You gotta <a href="/register">register your own character.</a>
</p>
<p>
You may also <a href="/change-password">change your password</a>, or
<a href="/lost-password">request a new one</a> if you've lost yours.
</p>

View File

@ -1,44 +1,46 @@
<h1>Register</h1>
{error_message} {error_message}
<form action="/register" method="post"> <form class="standard" action="/register" method="post">
{csrf_field} {csrf_field}
<table width="80%">
<tr> <div class="row">
<td width="20%">Username:</td> <div>
<td> <label for="username">Username</label>
<input type="text" name="username" size="30" maxlength="30" value="{username}" required> <span class="help">Must be 30 alphanumeric characters or less.</span>
<br> </div>
Usernames must be 30 alphanumeric characters or less. <input type="text" id="username" name="username" maxlength="30" value="{username}" required>
<br><br><br> </div>
</td>
</tr> <div class="row">
<tr> <div>
<td>Password:</td> <label for="password">Password</label>
<td><input type="password" name="password" size="30" required></td> <span class="help">Passwords must be at least 6 characters.</span>
</tr> </div>
<tr> <input type="password" id="password" name="password" required class="mb-05">
<td>Verify Password:</td> <label for="confirm_password">Verify Password</label>
<td> <input type="password" id="confirm_password" name="confirm_password" required>
<input type="password" name="confirm_password" size="30" required> </div>
<br>
Passwords must be at least 6 characters. <div class="row">
<br><br><br> <label for="email">Email</label>
</td> <input type="email" id="email" name="email" maxlength="100" value="{email}" required>
</tr> </div>
<tr>
<td>Email Address:</td> <div class="row">
<td> <label for="charclass">Class</label>
<input type="email" name="email" size="30" maxlength="100" value="{email}" required> <select id="charclass" name="charclass">
<br> <option>Choose a Class</option>
A valid email address is required. <option value="1">Mage</option>
<br><br><br> <option value="2">Warrior</option>
</td> <option value="3">Paladin</option>
</tr> </select>
<tr> </div>
<td colspan="2">
<input type="submit" name="submit" value="Register"> <div class="actions">
<input type="reset" name="reset" value="Reset"> <button class="btn" type="submit" name="submit">Register</button>
</td> <button class="btn" type="reset" name="reset">Reset</button>
</tr> </div>
</table>
</form> </form>