Remove CSRF, add password change, admin user management

Stripped all CSRF token generation, injection, and validation since it
breaks behind Apache reverse proxy. Removed handlers/csrf.go, stripped
CSRFToken from PageData, removed validateCSRFToken from all POST handlers,
and cleaned up hidden inputs and JS CSRF references.

Added self-service password change at /password/ with current-password
verification and bcrypt update. New Password link in navbar.

Extended admin panel with user management: lists all users with join dates
and allows admins to delete other users (self-deletion blocked). Added
GetAllUsers() and DeleteUser() to models.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Joca 2026-05-09 20:02:41 -03:00
parent a5a2e7063a
commit f4bc5c925c
Signed by: jocadbz
GPG Key ID: B1836DCE2F50BDF7
30 changed files with 291 additions and 142 deletions

View File

@ -3,6 +3,7 @@ package handlers
import (
"log"
"net/http"
"strconv"
"threadr/models"
"github.com/gorilla/sessions"
@ -31,8 +32,25 @@ func AdminHandler(app *App) http.HandlerFunc {
cookie, _ := r.Cookie("threadr_cookie_banner")
if r.Method == http.MethodPost {
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
action := r.URL.Query().Get("action")
if action == "delete_user" {
targetIDStr := r.FormValue("user_id")
targetID, err := strconv.Atoi(targetIDStr)
if err != nil || targetID <= 0 {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
if targetID == userID {
http.Error(w, "Cannot delete your own account", http.StatusBadRequest)
return
}
if err := models.DeleteUser(app.DB, targetID); err != nil {
log.Printf("Error deleting user %d: %v", targetID, err)
http.Error(w, "Failed to delete user", http.StatusInternalServerError)
return
}
http.Redirect(w, r, app.Config.ThreadrDir+"/admin/?deleted=true", http.StatusFound)
return
}
@ -59,10 +77,20 @@ func AdminHandler(app *App) http.HandlerFunc {
return
}
users, err := models.GetAllUsers(app.DB)
if err != nil {
log.Printf("Error fetching users in AdminHandler: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
data := struct {
PageData
AllowSignup bool
ShowSuccess bool
ShowDeleted bool
Users []models.User
CurrentUserID int
}{
PageData: PageData{
Title: "ThreadR - Admin",
@ -74,10 +102,12 @@ func AdminHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
},
AllowSignup: settings.AllowSignup,
ShowSuccess: r.URL.Query().Get("saved") == "true",
ShowDeleted: r.URL.Query().Get("deleted") == "true",
Users: users,
CurrentUserID: userID,
}
if err := app.Tmpl.ExecuteTemplate(w, "admin", data); err != nil {

View File

@ -22,7 +22,6 @@ type PageData struct {
CurrentURL string
ContentTemplate string
BodyClass string
CSRFToken string
}
type Config struct {

View File

@ -58,11 +58,6 @@ func BoardHandler(app *App) http.HandlerFunc {
if r.Method == http.MethodPost && loggedIn {
action := r.URL.Query().Get("action")
if action == "create_thread" {
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
title := r.FormValue("title")
if title == "" {
http.Error(w, "Thread title is required", http.StatusBadRequest)
@ -124,7 +119,6 @@ func BoardHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
},
Board: *board,
Threads: threads,

View File

@ -26,11 +26,6 @@ func BoardsHandler(app *App) http.HandlerFunc {
}
if r.Method == http.MethodPost && loggedIn && isAdmin {
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
name := r.FormValue("name")
description := r.FormValue("description")
boardType := r.FormValue("type")
@ -112,7 +107,6 @@ func BoardsHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
},
PublicBoards: publicBoards,
PrivateBoards: privateBoards,

View File

@ -147,11 +147,6 @@ func ChatHandler(app *App) http.HandlerFunc {
currentUsername := currentUser.Username
if r.URL.Query().Get("ws") == "true" {
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Error upgrading to WebSocket: %v", err)
@ -246,7 +241,6 @@ func ChatHandler(app *App) http.HandlerFunc {
CurrentURL: r.URL.RequestURI(),
ContentTemplate: "chat-content",
BodyClass: "chat-page",
CSRFToken: app.csrfToken(session),
},
Board: *board,
Messages: messages,

View File

@ -1,55 +0,0 @@
package handlers
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"net/http"
"github.com/gorilla/sessions"
)
const csrfSessionKey = "csrf_token"
func (app *App) ensureCSRFToken(session *sessions.Session) (string, error) {
if token, ok := session.Values[csrfSessionKey].(string); ok && token != "" {
return token, nil
}
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
return "", err
}
token := base64.RawURLEncoding.EncodeToString(raw)
session.Values[csrfSessionKey] = token
return token, nil
}
func (app *App) csrfToken(session *sessions.Session) string {
token, err := app.ensureCSRFToken(session)
if err != nil {
return ""
}
return token
}
func (app *App) validateCSRFToken(r *http.Request, session *sessions.Session) bool {
expected, ok := session.Values[csrfSessionKey].(string)
if !ok || expected == "" {
return false
}
provided := r.Header.Get("X-CSRF-Token")
if provided == "" {
provided = r.FormValue("csrf_token")
}
if provided == "" {
provided = r.URL.Query().Get("csrf_token")
}
if len(provided) != len(expected) {
return false
}
return subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) == 1
}

View File

@ -23,11 +23,6 @@ func LikeHandler(app *App) http.HandlerFunc {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
postIDStr := r.FormValue("post_id")
postID, err := strconv.Atoi(postIDStr)
if err != nil {

View File

@ -13,11 +13,6 @@ func LoginHandler(app *App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*sessions.Session)
if r.Method == http.MethodPost {
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
user, err := models.GetUserByUsername(app.DB, username)
@ -59,7 +54,6 @@ func LoginHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
},
Error: "",
}

View File

@ -26,11 +26,6 @@ func NewsHandler(app *App) http.HandlerFunc {
}
if r.Method == http.MethodPost && loggedIn && isAdmin {
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
if action := r.URL.Query().Get("action"); action == "delete" {
newsIDStr := r.URL.Query().Get("id")
newsID, err := strconv.Atoi(newsIDStr)
@ -91,7 +86,6 @@ func NewsHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
},
News: newsItems,
IsAdmin: isAdmin,

96
handlers/password.go Normal file
View File

@ -0,0 +1,96 @@
package handlers
import (
"log"
"net/http"
"threadr/models"
"github.com/gorilla/sessions"
)
func PasswordHandler(app *App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*sessions.Session)
userID, ok := session.Values["user_id"].(int)
if !ok {
http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound)
return
}
user, err := models.GetUserByID(app.DB, userID)
if err != nil {
log.Printf("Error fetching user in PasswordHandler: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if user == nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
cookie, _ := r.Cookie("threadr_cookie_banner")
if r.Method == http.MethodPost {
currentPassword := r.FormValue("current_password")
newPassword := r.FormValue("new_password")
confirmPassword := r.FormValue("confirm_password")
if currentPassword == "" || newPassword == "" || confirmPassword == "" {
renderPasswordPage(app, w, r, session, cookie, "All fields are required.", false)
return
}
if !models.CheckPassword(currentPassword, user.AuthenticationSalt, user.AuthenticationAlgorithm, user.AuthenticationString) {
renderPasswordPage(app, w, r, session, cookie, "Current password is incorrect.", false)
return
}
if newPassword != confirmPassword {
renderPasswordPage(app, w, r, session, cookie, "New passwords do not match.", false)
return
}
if len(newPassword) < 8 {
renderPasswordPage(app, w, r, session, cookie, "New password must be at least 8 characters.", false)
return
}
if err := models.UpdateUserPassword(app.DB, userID, newPassword); err != nil {
log.Printf("Error updating password: %v", err)
renderPasswordPage(app, w, r, session, cookie, "Failed to update password. Please try again.", false)
return
}
http.Redirect(w, r, app.Config.ThreadrDir+"/password/?saved=true", http.StatusFound)
return
}
showSuccess := r.URL.Query().Get("saved") == "true"
renderPasswordPage(app, w, r, session, cookie, "", showSuccess)
}
}
func renderPasswordPage(app *App, w http.ResponseWriter, r *http.Request, session *sessions.Session, cookie *http.Cookie, errMsg string, showSuccess bool) {
data := struct {
PageData
Error string
ShowSuccess bool
}{
PageData: PageData{
Title: "ThreadR - Change Password",
Navbar: "password",
LoggedIn: true,
AllowSignup: app.allowSignup(),
ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
},
Error: errMsg,
ShowSuccess: showSuccess,
}
if err := app.Tmpl.ExecuteTemplate(w, "password", data); err != nil {
log.Printf("Error executing template in PasswordHandler: %v", err)
}
}

View File

@ -19,11 +19,6 @@ func PreferencesHandler(app *App) http.HandlerFunc {
// Handle POST request (saving preferences)
if r.Method == http.MethodPost {
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
// Get form values
autoSaveDrafts := r.FormValue("auto_save_drafts") == "on"
@ -76,7 +71,6 @@ func PreferencesHandler(app *App) http.HandlerFunc {
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
ContentTemplate: "preferences-content",
CSRFToken: app.csrfToken(session),
},
Preferences: prefs,
ShowSuccess: showSuccess,

View File

@ -34,11 +34,6 @@ func ProfileEditHandler(app *App) http.HandlerFunc {
}
if r.Method == http.MethodPost {
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
r.Body = http.MaxBytesReader(w, r.Body, maxProfileImageBytes+(256<<10))
// Handle file upload
@ -138,7 +133,6 @@ func ProfileEditHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
},
User: *user,
}

View File

@ -24,11 +24,6 @@ func SignupHandler(app *App) http.HandlerFunc {
}
if r.Method == http.MethodPost {
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
passwordConfirm := r.FormValue("password_confirm")
@ -49,7 +44,7 @@ func SignupHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
},
Error: "Passwords do not match. Please try again.",
}
@ -77,7 +72,7 @@ func SignupHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
},
Error: "An error occurred during sign up. Please try again.",
}
@ -104,7 +99,7 @@ func SignupHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
},
Error: "",
}

View File

@ -59,11 +59,6 @@ func ThreadHandler(app *App) http.HandlerFunc {
if r.Method == http.MethodPost && loggedIn {
action := r.URL.Query().Get("action")
if action == "submit" {
if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
content := r.FormValue("content")
replyToStr := r.FormValue("reply_to")
if replyToStr == "" {
@ -170,7 +165,6 @@ func ThreadHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
},
Thread: *thread,
Board: *board,

View File

@ -439,6 +439,7 @@ func main() {
handle("/about/", handlers.AboutHandler(app))
handleAuthed("/profile/", handlers.ProfileHandler(app))
handleAuthed("/profile/edit/", handlers.ProfileEditHandler(app))
handleAuthed("/password/", handlers.PasswordHandler(app))
handleAuthed("/preferences/", handlers.PreferencesHandler(app))
handleAuthed("/admin/", handlers.AdminHandler(app))
handleAuthed("/like/", handlers.LikeHandler(app))

View File

@ -168,6 +168,68 @@ func UpdateUserPfp(db *sql.DB, userID int, pfpFileID int64) error {
return err
}
func UpdateUserPassword(db *sql.DB, userID int, newPassword string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcryptCost)
if err != nil {
return err
}
var saltBytes [16]byte
if _, err := rand.Read(saltBytes[:]); err != nil {
return fmt.Errorf("failed to generate salt: %w", err)
}
salt := fmt.Sprintf("%x", saltBytes)
_, err = db.Exec("UPDATE users SET authentication_string = ?, authentication_salt = ?, authentication_algorithm = 'bcrypt', updated_at = NOW() WHERE id = ?",
string(hash), salt, userID)
return err
}
func GetAllUsers(db *sql.DB) ([]User, error) {
query := "SELECT id, username, display_name, pfp_file_id, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users ORDER BY created_at DESC"
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
user := User{}
var displayName sql.NullString
var bio sql.NullString
var createdAtString sql.NullString
var updatedAtString sql.NullString
err := rows.Scan(&user.ID, &user.Username, &displayName, &user.PfpFileID, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions)
if err != nil {
return nil, err
}
if displayName.Valid {
user.DisplayName = displayName.String
}
if bio.Valid {
user.Bio = bio.String
}
if createdAtString.Valid {
user.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtString.String)
if err != nil {
return nil, fmt.Errorf("error parsing created_at: %v", err)
}
}
if updatedAtString.Valid {
user.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtString.String)
if err != nil {
return nil, fmt.Errorf("error parsing updated_at: %v", err)
}
}
users = append(users, user)
}
return users, nil
}
func DeleteUser(db *sql.DB, userID int) error {
_, err := db.Exec("DELETE FROM users WHERE id = ?", userID)
return err
}
const (
PermCreateBoard int64 = 1 << 0
PermManageUsers int64 = 1 << 1

View File

@ -9,7 +9,6 @@
const boardId = chatContainer.dataset.boardId;
const basePath = chatContainer.dataset.basePath || '';
const currentUsername = chatContainer.dataset.currentUsername || '';
const csrfToken = chatContainer.dataset.csrfToken || '';
const usernamesScript = document.getElementById('chat-usernames');
let allUsernames = [];
if (usernamesScript) {
@ -56,7 +55,7 @@
updateConnectionStatus('connecting');
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const query = new URLSearchParams({ ws: 'true', id: boardId, csrf_token: csrfToken });
const query = new URLSearchParams({ ws: 'true', id: boardId });
ws = new WebSocket(protocol + window.location.host + basePath + '/chat/?' + query.toString());
ws.onopen = function() {

View File

@ -5,7 +5,6 @@ function initLikeButtons() {
var postId = btn.getAttribute('data-post-id');
var type = btn.getAttribute('data-type');
var basePath = btn.getAttribute('data-base-path');
var csrfToken = document.body ? document.body.getAttribute('data-csrf-token') : '';
btn.disabled = true;
@ -16,8 +15,7 @@ function initLikeButtons() {
fetch(basePath + '/like/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': csrfToken
'Content-Type': 'application/x-www-form-urlencoded'
},
body: body.toString()
})

View File

@ -17,10 +17,14 @@
Settings saved successfully!
</div>
{{end}}
{{if .ShowDeleted}}
<div class="notification success" style="position: static; margin-bottom: 1em; animation: none;">
User deleted successfully.
</div>
{{end}}
<section>
<h3>Registration</h3>
<form method="post" action="{{.BasePath}}/admin/">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label for="allow_signup" style="display: flex; align-items: center; gap: 0.5em; cursor: pointer;">
<input type="checkbox" id="allow_signup" name="allow_signup" {{if .AllowSignup}}checked{{end}}>
<span>Allow new user signups</span>
@ -32,6 +36,41 @@
<input type="submit" value="Save Settings" style="margin-top: 1.5em;">
</form>
</section>
<section>
<h3>User Management</h3>
{{if .Users}}
<table style="width: 100%; border-collapse: collapse; margin-top: 1em;">
<thead>
<tr style="border-bottom: 2px solid #f582ae;">
<th style="text-align: left; padding: 0.5em;">Username</th>
<th style="text-align: left; padding: 0.5em;">Display Name</th>
<th style="text-align: left; padding: 0.5em;">Joined</th>
<th style="text-align: right; padding: 0.5em;">Actions</th>
</tr>
</thead>
<tbody>
{{range .Users}}
<tr style="border-bottom: 1px solid #ddd;">
<td style="padding: 0.5em;">{{.Username}}{{if eq .ID $.CurrentUserID}} <em>(you)</em>{{end}}</td>
<td style="padding: 0.5em;">{{if .DisplayName}}{{.DisplayName}}{{else}}—{{end}}</td>
<td style="padding: 0.5em;">{{.CreatedAt.Format "02/01/2006"}}</td>
<td style="padding: 0.5em; text-align: right;">
{{if ne .ID $.CurrentUserID}}
<form method="post" action="{{$.BasePath}}/admin/?action=delete_user" style="display: inline;" onsubmit="return confirm('Permanently delete user \'{{.Username}}\'? This cannot be undone.')">
<input type="hidden" name="user_id" value="{{.ID}}">
<button type="submit" style="background: #f582ae; color: #001858; padding: 0.2em 0.6em; font-size: 0.85em;">Delete</button>
</form>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p>No users found.</p>
{{end}}
</section>
</main>
{{template "cookie_banner" .}}
</body>

View File

@ -40,7 +40,6 @@
<section>
<h3>Create New Thread</h3>
<form method="post" action="{{.BasePath}}/board/?id={{.Board.ID}}&action=create_thread">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label for="title">Thread Title:</label>
<input type="text" id="title" name="title" required maxlength="255"><br>
<input type="submit" value="Create Thread">

View File

@ -56,7 +56,6 @@
<section>
<h3>Create New Public Board</h3>
<form method="post" action="{{.BasePath}}/boards/">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label for="name">Board Name:</label>
<input type="text" id="name" name="name" required maxlength="255"><br>
<label for="description">Description:</label>

View File

@ -12,10 +12,10 @@
<script src="{{.StaticPath}}/app.js" defer></script>
<script src="{{.StaticPath}}/chat.js" defer></script>
</head>
<body class="chat-page" data-csrf-token="{{.CSRFToken}}">
<body class="chat-page">
{{template "navbar" .}}
<main>
<div class="chat-container" data-board-id="{{.Board.ID}}" data-base-path="{{.BasePath}}" data-current-username="{{.CurrentUsername}}" data-csrf-token="{{.CSRFToken}}">
<div class="chat-container" data-board-id="{{.Board.ID}}" data-base-path="{{.BasePath}}" data-current-username="{{.CurrentUsername}}">
<div class="chat-breadcrumb">
<a href="{{.BasePath}}/">Home</a>
<span class="chat-breadcrumb-separator"></span>

View File

@ -17,7 +17,6 @@
<p class="field-error" style="text-align: center; font-size: 1em;">{{.Error}}</p>
{{end}}
<form method="post" action="{{.BasePath}}/login/">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required autocomplete="username"><br>
<label for="password">Password:</label>

View File

@ -20,7 +20,6 @@
<p>{{.Content}}</p>
{{if $.IsAdmin}}
<form method="post" action="{{$.BasePath}}/news/?action=delete&id={{.ID}}" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<button type="submit" onclick="return confirm('Are you sure you want to delete this news item?')">Delete</button>
</form>
{{end}}
@ -35,7 +34,6 @@
<section>
<h3>Post New Announcement</h3>
<form method="post" action="{{.BasePath}}/news/">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label for="title">Title:</label>
<input type="text" id="title" name="title" required maxlength="255"><br>
<label for="content">Content:</label>

View File

@ -0,0 +1,38 @@
{{define "password"}}
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head>
<body>
{{template "navbar" .}}
<main>
<header>
<h2>Change Password</h2>
</header>
{{if .ShowSuccess}}
<div class="notification success" style="position: static; margin-bottom: 1em; animation: none;">
Password changed successfully!
</div>
{{end}}
{{if .Error}}
<p class="field-error" style="text-align: center; font-size: 1em;">{{.Error}}</p>
{{end}}
<section>
<form method="post" action="{{.BasePath}}/password/">
<label for="current_password">Current Password:</label>
<input type="password" id="current_password" name="current_password" required autocomplete="current-password"><br>
<label for="new_password">New Password:</label>
<input type="password" id="new_password" name="new_password" required autocomplete="new-password" minlength="8" maxlength="128"><br>
<label for="confirm_password">Confirm New Password:</label>
<input type="password" id="confirm_password" name="confirm_password" required autocomplete="new-password" minlength="8" maxlength="128"><br>
<input type="submit" value="Change Password">
</form>
</section>
</main>
{{template "cookie_banner" .}}
</body>
</html>
{{end}}

View File

@ -11,7 +11,6 @@
{{end}}
<section>
<form method="post" action="{{.BasePath}}/preferences/">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<h3>Draft Auto-Save</h3>
<label for="auto_save_drafts" style="display: flex; align-items: center; gap: 0.5em; cursor: pointer;">
<input type="checkbox" id="auto_save_drafts" name="auto_save_drafts" {{if .Preferences.AutoSaveDrafts}}checked{{end}}>

View File

@ -14,7 +14,6 @@
</header>
<section>
<form method="post" action="{{.BasePath}}/profile/edit/" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label for="display_name">Display Name:</label>
<input type="text" id="display_name" name="display_name" value="{{.User.DisplayName}}" maxlength="255"><br>
<label for="pfp">Profile Picture:</label>

View File

@ -17,7 +17,6 @@
<p class="field-error" style="text-align: center; font-size: 1em;">{{.Error}}</p>
{{end}}
<form method="post" action="{{.BasePath}}/signup/">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required autocomplete="username" minlength="3" maxlength="30"><br>
<label for="password">Password:</label>

View File

@ -7,7 +7,7 @@
<script src="{{.StaticPath}}/likes.js" defer></script>
<script src="{{.StaticPath}}/app.js" defer></script>
</head>
<body data-csrf-token="{{.CSRFToken}}">
<body>
{{template "navbar" .}}
<main>
<div class="breadcrumb">
@ -62,7 +62,6 @@
<button type="button" onclick="clearReply()">x</button>
</div>
<form method="post" action="{{.BasePath}}/thread/?id={{.Thread.ID}}&action=submit" id="reply-form">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" id="reply-to-input" name="reply_to" value="">
<label for="content">Content:</label>
<textarea id="content" name="content" required></textarea><br>
@ -106,3 +105,11 @@
</body>
</html>
{{end}}
{ target.classList.remove('post-highlighted'); }, 2000);
}
});
});
</script>
</body>
</html>
{{end}}

View File

@ -4,6 +4,7 @@
{{if .LoggedIn}}
<li><a {{if eq .Navbar "userhome"}}class="active"{{end}} href="{{.BasePath}}/userhome/">User Home</a></li>
<li><a {{if eq .Navbar "profile"}}class="active"{{end}} href="{{.BasePath}}/profile/">Profile</a></li>
<li><a {{if eq .Navbar "password"}}class="active"{{end}} href="{{.BasePath}}/password/">Password</a></li>
<li><a {{if eq .Navbar "preferences"}}class="active"{{end}} href="{{.BasePath}}/preferences/">Preferences</a></li>
{{if .IsAdmin}}
<li><a {{if eq .Navbar "admin"}}class="active"{{end}} href="{{.BasePath}}/admin/">Admin</a></li>