Compare commits
No commits in common. "1af833380187b4d4bdcc4ae443b580e065537617" and "8eb869a9716f610525f4fb0efc9552eb5951f9b6" have entirely different histories.
1af8333801
...
8eb869a971
@ -162,52 +162,4 @@ 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;
|
|
||||||
}
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 261 KiB |
@ -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,138 +21,174 @@ 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 {
|
||||||
if !csrf.ValidateFormToken(ctx, auth.Manager) {
|
return func(ctx router.Ctx, params []string) {
|
||||||
ctx.SetStatusCode(fasthttp.StatusForbidden)
|
if !csrf.ValidateFormToken(ctx, auth.Manager) {
|
||||||
ctx.WriteString("CSRF validation failed")
|
ctx.SetStatusCode(fasthttp.StatusForbidden)
|
||||||
return
|
ctx.WriteString("CSRF validation failed")
|
||||||
}
|
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) {
|
||||||
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 {
|
||||||
if !csrf.ValidateFormToken(ctx, auth.Manager) {
|
return func(ctx router.Ctx, params []string) {
|
||||||
ctx.SetStatusCode(fasthttp.StatusForbidden)
|
if !csrf.ValidateFormToken(ctx, auth.Manager) {
|
||||||
ctx.WriteString("CSRF validation failed")
|
ctx.SetStatusCode(fasthttp.StatusForbidden)
|
||||||
return
|
ctx.WriteString("CSRF validation failed")
|
||||||
}
|
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(ctx router.Ctx, params []string) {
|
func processLogout() router.Handler {
|
||||||
// Validate CSRF token
|
return func(ctx router.Ctx, params []string) {
|
||||||
if !csrf.ValidateFormToken(ctx, auth.Manager) {
|
// Validate CSRF token
|
||||||
ctx.SetStatusCode(fasthttp.StatusForbidden)
|
if !csrf.ValidateFormToken(ctx, auth.Manager) {
|
||||||
ctx.WriteString("CSRF validation failed")
|
ctx.SetStatusCode(fasthttp.StatusForbidden)
|
||||||
return
|
ctx.WriteString("CSRF validation failed")
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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>`
|
|
||||||
}
|
|
||||||
}
|
|
@ -23,7 +23,7 @@ type TemplateCache struct {
|
|||||||
|
|
||||||
type RenderOptions struct {
|
type RenderOptions struct {
|
||||||
ResolveIncludes bool
|
ResolveIncludes bool
|
||||||
Blocks map[string]string
|
Blocks map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Template struct {
|
type Template struct {
|
||||||
@ -261,7 +261,7 @@ func (t *Template) getStructField(obj any, fieldName string) any {
|
|||||||
if obj == nil {
|
if obj == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rv := reflect.ValueOf(obj)
|
rv := reflect.ValueOf(obj)
|
||||||
if rv.Kind() == reflect.Ptr {
|
if rv.Kind() == reflect.Ptr {
|
||||||
if rv.IsNil() {
|
if rv.IsNil() {
|
||||||
@ -269,16 +269,16 @@ func (t *Template) getStructField(obj any, fieldName string) any {
|
|||||||
}
|
}
|
||||||
rv = rv.Elem()
|
rv = rv.Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
if rv.Kind() != reflect.Struct {
|
if rv.Kind() != reflect.Struct {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
field := rv.FieldByName(fieldName)
|
field := rv.FieldByName(fieldName)
|
||||||
if !field.IsValid() {
|
if !field.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return field.Interface()
|
return field.Interface()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,29 +314,29 @@ func (t *Template) WriteToWithOptions(ctx *fasthttp.RequestCtx, data any, opts R
|
|||||||
// processIncludes handles {include "template.html"} directives
|
// processIncludes handles {include "template.html"} directives
|
||||||
func (t *Template) processIncludes(content string, data map[string]any, opts RenderOptions) string {
|
func (t *Template) processIncludes(content string, data map[string]any, opts RenderOptions) string {
|
||||||
result := content
|
result := content
|
||||||
|
|
||||||
for {
|
for {
|
||||||
start := strings.Index(result, "{include ")
|
start := strings.Index(result, "{include ")
|
||||||
if start == -1 {
|
if start == -1 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
end := strings.Index(result[start:], "}")
|
end := strings.Index(result[start:], "}")
|
||||||
if end == -1 {
|
if end == -1 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
end += start
|
end += start
|
||||||
|
|
||||||
directive := result[start+9 : end] // Skip "{include "
|
directive := result[start+9:end] // Skip "{include "
|
||||||
templateName := strings.Trim(directive, "\" ")
|
templateName := strings.Trim(directive, "\" ")
|
||||||
|
|
||||||
if includedTemplate, err := t.cache.Load(templateName); err == nil {
|
if includedTemplate, err := t.cache.Load(templateName); err == nil {
|
||||||
var includedContent string
|
var includedContent string
|
||||||
if data != nil {
|
if data != nil {
|
||||||
// Create new options to pass blocks to included template
|
// Create new options to pass blocks to included template
|
||||||
includeOpts := RenderOptions{
|
includeOpts := RenderOptions{
|
||||||
ResolveIncludes: opts.ResolveIncludes,
|
ResolveIncludes: opts.ResolveIncludes,
|
||||||
Blocks: opts.Blocks,
|
Blocks: opts.Blocks,
|
||||||
}
|
}
|
||||||
includedContent = includedTemplate.RenderNamedWithOptions(includeOpts, data)
|
includedContent = includedTemplate.RenderNamedWithOptions(includeOpts, data)
|
||||||
} else {
|
} else {
|
||||||
@ -348,7 +348,7 @@ func (t *Template) processIncludes(content string, data map[string]any, opts Ren
|
|||||||
result = result[:start] + result[end+1:]
|
result = result[:start] + result[end+1:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,16 +357,16 @@ func (t *Template) processYield(content string, opts RenderOptions) string {
|
|||||||
if opts.Blocks == nil {
|
if opts.Blocks == nil {
|
||||||
return strings.ReplaceAll(content, "{yield}", "")
|
return strings.ReplaceAll(content, "{yield}", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
result := content
|
result := content
|
||||||
for blockName, blockContent := range opts.Blocks {
|
for blockName, blockContent := range opts.Blocks {
|
||||||
yieldPlaceholder := fmt.Sprintf("{yield %s}", blockName)
|
yieldPlaceholder := fmt.Sprintf("{yield %s}", blockName)
|
||||||
result = strings.ReplaceAll(result, yieldPlaceholder, blockContent)
|
result = strings.ReplaceAll(result, yieldPlaceholder, blockContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace any remaining {yield} with empty string
|
// Replace any remaining {yield} with empty string
|
||||||
result = strings.ReplaceAll(result, "{yield}", "")
|
result = strings.ReplaceAll(result, "{yield}", "")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,23 +375,23 @@ func (t *Template) processBlocks(content string, opts *RenderOptions) string {
|
|||||||
if opts.Blocks == nil {
|
if opts.Blocks == nil {
|
||||||
opts.Blocks = make(map[string]string)
|
opts.Blocks = make(map[string]string)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := content
|
result := content
|
||||||
|
|
||||||
for {
|
for {
|
||||||
start := strings.Index(result, "{block ")
|
start := strings.Index(result, "{block ")
|
||||||
if start == -1 {
|
if start == -1 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
nameEnd := strings.Index(result[start:], "}")
|
nameEnd := strings.Index(result[start:], "}")
|
||||||
if nameEnd == -1 {
|
if nameEnd == -1 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
nameEnd += start
|
nameEnd += start
|
||||||
|
|
||||||
blockName := strings.Trim(result[start+7:nameEnd], "\" ")
|
blockName := strings.Trim(result[start+7:nameEnd], "\" ")
|
||||||
|
|
||||||
contentStart := nameEnd + 1
|
contentStart := nameEnd + 1
|
||||||
endTag := "{/block}"
|
endTag := "{/block}"
|
||||||
contentEnd := strings.Index(result[contentStart:], endTag)
|
contentEnd := strings.Index(result[contentStart:], endTag)
|
||||||
@ -399,38 +399,13 @@ func (t *Template) processBlocks(content string, opts *RenderOptions) string {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
contentEnd += contentStart
|
contentEnd += contentStart
|
||||||
|
|
||||||
blockContent := result[contentStart:contentEnd]
|
blockContent := result[contentStart:contentEnd]
|
||||||
opts.Blocks[blockName] = blockContent
|
opts.Blocks[blockName] = blockContent
|
||||||
|
|
||||||
// Remove the block definition from the template
|
// Remove the block definition from the template
|
||||||
result = result[:start] + result[contentEnd+len(endTag):]
|
result = result[:start] + result[contentEnd+len(endTag):]
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
@ -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%">
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="row">
|
<br><br>
|
||||||
<label for="id">Email/Username</label>
|
|
||||||
<input id="id" type="text" name="id" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
You may also <a href="/change-password">change your password</a>, or
|
||||||
<label for="password">Password</label>
|
<a href="/lost-password">request a new one</a> if you've lost yours.
|
||||||
<input id="password" type="password" name="password" required>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
|
</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>
|
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user