Compare commits

..

No commits in common. "1af833380187b4d4bdcc4ae443b580e065537617" and "8eb869a9716f610525f4fb0efc9552eb5951f9b6" have entirely different histories.

9 changed files with 274 additions and 324 deletions

View File

@ -163,51 +163,3 @@ form#move-compass {
} }
} }
} }
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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

View File

@ -6,6 +6,7 @@ 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"
@ -20,31 +21,48 @@ 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()) authGroup.Use(middleware.RequireAuth("/login"))
authGroup.Post("/logout", processLogout) authGroup.Post("/logout", processLogout())
} }
// showLogin displays the login form // showLogin displays the login form
func showLogin(ctx router.Ctx, _ []string) { func showLogin(ctx router.Ctx, params []string) {
components.RenderPageTemplate(ctx, "Log In", "auth/login.html", map[string]any{ loginTmpl, err := template.Cache.Load("auth/login.html")
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(ctx router.Ctx, _ []string) { func processLogin() router.Handler {
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")
@ -76,20 +94,36 @@ func processLogin(ctx router.Ctx, _ []string) {
ctx.Redirect("/", fasthttp.StatusFound) 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) {
components.RenderPageTemplate(ctx, "Register", "auth/register.html", map[string]any{ registerTmpl, err := template.Cache.Load("auth/register.html")
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(ctx router.Ctx, _ []string) { func processRegister() router.Handler {
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")
@ -140,9 +174,11 @@ func processRegister(ctx router.Ctx, _ []string) {
ctx.Redirect("/", fasthttp.StatusFound) ctx.Redirect("/", fasthttp.StatusFound)
} }
}
// processLogout handles logout // processLogout handles logout
func processLogout(ctx router.Ctx, params []string) { func processLogout() router.Handler {
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)
@ -153,6 +189,7 @@ func processLogout(ctx router.Ctx, params []string) {
middleware.Logout(ctx, auth.Manager) middleware.Logout(ctx, auth.Manager)
ctx.Redirect("/", fasthttp.StatusFound) ctx.Redirect("/", fasthttp.StatusFound)
} }
}
// Helper functions // Helper functions
@ -242,3 +279,23 @@ 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

@ -3,8 +3,11 @@ package routes
import ( import (
"dk/internal/middleware" "dk/internal/middleware"
"dk/internal/router" "dk/internal/router"
"dk/internal/template"
"dk/internal/template/components" "dk/internal/template/components"
"dk/internal/towns" "fmt"
"github.com/valyala/fasthttp"
) )
func RegisterTownRoutes(r *router.Router) { func RegisterTownRoutes(r *router.Router) {
@ -16,8 +19,21 @@ func RegisterTownRoutes(r *router.Router) {
} }
func showTown(ctx router.Ctx, _ []string) { func showTown(ctx router.Ctx, _ []string) {
town := ctx.UserValue("town").(*towns.Town) tmpl, err := template.Cache.Load("town/town.html")
components.RenderPageTemplate(ctx, town.Name, "town/town.html", map[string]any{ if err != nil {
"town": town, ctx.SetStatusCode(fasthttp.StatusInternalServerError)
}) fmt.Fprintf(ctx, "Template error: %v", err)
return
}
content := tmpl.RenderNamed(map[string]any{
"town": ctx.UserValue("town"),
})
pageData := components.NewPageData("Town - Dragon Knight", content)
if err := components.RenderPage(ctx, pageData, nil); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
} }

View File

@ -3,16 +3,30 @@ package components
import ( import (
"fmt" "fmt"
"maps" "maps"
"strings"
"dk/internal/auth" "dk/internal/auth"
"dk/internal/csrf"
"dk/internal/middleware" "dk/internal/middleware"
"dk/internal/router" "dk/internal/router"
"dk/internal/template" "dk/internal/template"
"github.com/valyala/fasthttp"
) )
// GenerateTopNav generates the top navigation HTML based on authentication status
func GenerateTopNav(ctx router.Ctx) string {
if middleware.IsAuthenticated(ctx) {
csrfField := csrf.HiddenField(ctx, auth.Manager)
return fmt.Sprintf(`<form action="/logout" method="post" class="logout">
%s
<button class="img-button" type="submit"><img src="/assets/images/button_logout.gif" alt="Log Out" title="Log Out"></button>
</form>
<a href="/help"><img src="/assets/images/button_help.gif" alt="Help" title="Help"></a>`, csrfField)
} else {
return `<a href="/login"><img src="/assets/images/button_login.gif" alt="Log In" title="Log In"></a>
<a href="/register"><img src="/assets/images/button_register.gif" alt="Register" title="Register"></a>
<a href="/help"><img src="/assets/images/button_help.gif" alt="Help" title="Help"></a>`
}
}
// PageData holds common page template data // PageData holds common page template data
type PageData struct { type PageData struct {
Title string Title string
@ -86,39 +100,3 @@ func NewPageData(title, content string) PageData {
Build: "dev", Build: "dev",
} }
} }
// PageTitle returns a proper title for a rendered page. If an empty string
// is given, returns "Dragon Knight". If the provided title already has " - Dragon Knight"
// at the end, returns title as-is. Appends " - Dragon Knight" to title otherwise.
func PageTitle(title string) string {
if title == "" {
return "Dragon Knight"
}
if strings.HasSuffix(" - Dragon Knight", title) {
return title
}
return title + " - Dragon Knight"
}
// RenderPageTemplate is a simplified helper that renders a template within the page layout.
// It loads the template, renders it with the provided data, and then renders the full page.
// Returns true if successful, false if an error occurred (error is written to response).
func RenderPageTemplate(ctx router.Ctx, title, templateName string, data map[string]any) bool {
content, err := template.RenderNamed(templateName, data)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return false
}
pageData := NewPageData(PageTitle(title), content)
if err := RenderPage(ctx, pageData, nil); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return false
}
return true
}

View File

@ -1,24 +0,0 @@
package components
import (
"dk/internal/auth"
"dk/internal/csrf"
"dk/internal/middleware"
"dk/internal/router"
"fmt"
)
// GenerateTopNav generates the top navigation HTML based on authentication status
func GenerateTopNav(ctx router.Ctx) string {
if middleware.IsAuthenticated(ctx) {
return fmt.Sprintf(`<form action="/logout" method="post" class="logout">
%s
<button class="img-button" type="submit"><img src="/assets/images/button_logout.gif" alt="Log Out" title="Log Out"></button>
</form>
<a href="/help"><img src="/assets/images/button_help.gif" alt="Help" title="Help"></a>`, csrf.HiddenField(ctx, auth.Manager))
} else {
return `<a href="/login"><img src="/assets/images/button_login.gif" alt="Log In" title="Log In"></a>
<a href="/register"><img src="/assets/images/button_register.gif" alt="Register" title="Register"></a>
<a href="/help"><img src="/assets/images/button_help.gif" alt="Help" title="Help"></a>`
}
}

View File

@ -409,28 +409,3 @@ func (t *Template) processBlocks(content string, opts *RenderOptions) string {
return result return result
} }
// RenderToContext is a simplified helper that renders a template and writes it to the request context
// with error handling. Returns true if successful, false if an error occurred (error is written to response).
func RenderToContext(ctx *fasthttp.RequestCtx, templateName string, data map[string]any) bool {
tmpl, err := Cache.Load(templateName)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return false
}
tmpl.WriteTo(ctx, data)
return true
}
// RenderNamed is a simplified helper that loads and renders a template with the given data,
// returning the rendered content or an error.
func RenderNamed(templateName string, data map[string]any) (string, error) {
tmpl, err := Cache.Load(templateName)
if err != nil {
return "", fmt.Errorf("failed to load template %s: %w", templateName, err)
}
return tmpl.RenderNamed(data), nil
}

View File

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

View File

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