From a5a2e7063abce15f4b16e3dede625ac17dd54948 Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Sun, 19 Apr 2026 14:03:24 -0300 Subject: [PATCH] Add admin-controlled signup toggle and hide signup links. --- handlers/about.go | 1 + handlers/admin.go | 89 ++++++++++++++++++++++++++++++++++ handlers/app.go | 2 + handlers/board.go | 1 + handlers/boards.go | 1 + handlers/chat.go | 1 + handlers/home.go | 1 + handlers/login.go | 17 ++++--- handlers/news.go | 1 + handlers/preferences.go | 1 + handlers/profile.go | 1 + handlers/profile_edit.go | 1 + handlers/signup.go | 15 ++++++ handlers/site_settings.go | 16 ++++++ handlers/thread.go | 1 + handlers/userhome.go | 5 ++ main.go | 13 +++++ models/site_settings.go | 43 ++++++++++++++++ templates/pages/admin.html | 39 +++++++++++++++ templates/pages/userhome.html | 5 +- templates/partials/navbar.html | 7 ++- 21 files changed, 252 insertions(+), 9 deletions(-) create mode 100644 handlers/admin.go create mode 100644 handlers/site_settings.go create mode 100644 models/site_settings.go create mode 100644 templates/pages/admin.html diff --git a/handlers/about.go b/handlers/about.go index c4a9bd7..20c32f2 100644 --- a/handlers/about.go +++ b/handlers/about.go @@ -29,6 +29,7 @@ func AboutHandler(app *App) http.HandlerFunc { Title: "ThreadR - About", Navbar: "about", LoggedIn: loggedIn, + AllowSignup: app.allowSignup(), ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/handlers/admin.go b/handlers/admin.go new file mode 100644 index 0000000..807f169 --- /dev/null +++ b/handlers/admin.go @@ -0,0 +1,89 @@ +package handlers + +import ( + "log" + "net/http" + "threadr/models" + + "github.com/gorilla/sessions" +) + +func AdminHandler(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 AdminHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if user == nil || !models.HasGlobalPermission(user, models.PermManageUsers) { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + cookie, _ := r.Cookie("threadr_cookie_banner") + + if r.Method == http.MethodPost { + if !app.validateCSRFToken(r, session) { + http.Error(w, "Invalid CSRF token", http.StatusForbidden) + return + } + + allowSignup := r.FormValue("allow_signup") == "on" + if err := models.SetAllowSignup(app.DB, allowSignup); err != nil { + log.Printf("Error updating site settings in AdminHandler: %v", err) + http.Error(w, "Failed to save settings", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, app.Config.ThreadrDir+"/admin/?saved=true", http.StatusFound) + return + } + + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + settings, err := models.GetSiteSettings(app.DB) + if err != nil { + log.Printf("Error fetching site settings in AdminHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := struct { + PageData + AllowSignup bool + ShowSuccess bool + }{ + PageData: PageData{ + Title: "ThreadR - Admin", + Navbar: "admin", + LoggedIn: true, + IsAdmin: true, + AllowSignup: settings.AllowSignup, + ShowCookieBanner: cookie == nil || cookie.Value != "accepted", + 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", + } + + if err := app.Tmpl.ExecuteTemplate(w, "admin", data); err != nil { + log.Printf("Error executing template in AdminHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} diff --git a/handlers/app.go b/handlers/app.go index b5ca225..b3aa03f 100644 --- a/handlers/app.go +++ b/handlers/app.go @@ -14,6 +14,8 @@ type PageData struct { Title string Navbar string LoggedIn bool + IsAdmin bool + AllowSignup bool ShowCookieBanner bool BasePath string StaticPath string diff --git a/handlers/board.go b/handlers/board.go index 96e7353..5ac8ef3 100644 --- a/handlers/board.go +++ b/handlers/board.go @@ -119,6 +119,7 @@ func BoardHandler(app *App) http.HandlerFunc { Title: "ThreadR - " + board.Name, Navbar: "boards", LoggedIn: loggedIn, + AllowSignup: app.allowSignup(), ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/handlers/boards.go b/handlers/boards.go index 9955b88..6b0f192 100644 --- a/handlers/boards.go +++ b/handlers/boards.go @@ -107,6 +107,7 @@ func BoardsHandler(app *App) http.HandlerFunc { Title: "ThreadR - Boards", Navbar: "boards", LoggedIn: loggedIn, + AllowSignup: app.allowSignup(), ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/handlers/chat.go b/handlers/chat.go index 3249df6..8a7eeeb 100644 --- a/handlers/chat.go +++ b/handlers/chat.go @@ -239,6 +239,7 @@ func ChatHandler(app *App) http.HandlerFunc { Title: "ThreadR Chat - " + board.Name, Navbar: "boards", LoggedIn: true, + AllowSignup: app.allowSignup(), ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/handlers/home.go b/handlers/home.go index 956cc0e..6808630 100644 --- a/handlers/home.go +++ b/handlers/home.go @@ -19,6 +19,7 @@ func HomeHandler(app *App) http.HandlerFunc { Title: "ThreadR - Home", Navbar: "home", LoggedIn: loggedIn, + AllowSignup: app.allowSignup(), ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: filepath.Join(app.Config.ThreadrDir, "static"), diff --git a/handlers/login.go b/handlers/login.go index c2d84db..e7d248d 100644 --- a/handlers/login.go +++ b/handlers/login.go @@ -52,18 +52,21 @@ func LoginHandler(app *App) http.HandlerFunc { Error string }{ PageData: PageData{ - Title: "ThreadR - Login", - Navbar: "login", - LoggedIn: false, - BasePath: app.Config.ThreadrDir, - StaticPath: app.Config.ThreadrDir + "/static", - CurrentURL: r.URL.RequestURI(), - CSRFToken: app.csrfToken(session), + Title: "ThreadR - Login", + Navbar: "login", + LoggedIn: false, + AllowSignup: app.allowSignup(), + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.RequestURI(), + CSRFToken: app.csrfToken(session), }, Error: "", } if r.URL.Query().Get("error") == "invalid" { data.Error = "Invalid username or password" + } else if r.URL.Query().Get("error") == "signup_disabled" { + data.Error = "Sign up is currently disabled by the administrator" } if err := app.Tmpl.ExecuteTemplate(w, "login", data); err != nil { diff --git a/handlers/news.go b/handlers/news.go index 9fbe070..7c1acf6 100644 --- a/handlers/news.go +++ b/handlers/news.go @@ -86,6 +86,7 @@ func NewsHandler(app *App) http.HandlerFunc { Title: "ThreadR - News", Navbar: "news", LoggedIn: loggedIn, + AllowSignup: app.allowSignup(), ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/handlers/preferences.go b/handlers/preferences.go index 2f17022..afd377a 100644 --- a/handlers/preferences.go +++ b/handlers/preferences.go @@ -70,6 +70,7 @@ func PreferencesHandler(app *App) http.HandlerFunc { Title: "ThreadR - Preferences", Navbar: "preferences", LoggedIn: true, + AllowSignup: app.allowSignup(), ShowCookieBanner: false, BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/handlers/profile.go b/handlers/profile.go index c1f8baf..3888e41 100644 --- a/handlers/profile.go +++ b/handlers/profile.go @@ -38,6 +38,7 @@ func ProfileHandler(app *App) http.HandlerFunc { Title: "ThreadR - Profile", Navbar: "profile", LoggedIn: true, + AllowSignup: app.allowSignup(), ShowCookieBanner: false, BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/handlers/profile_edit.go b/handlers/profile_edit.go index e35094f..759a409 100644 --- a/handlers/profile_edit.go +++ b/handlers/profile_edit.go @@ -133,6 +133,7 @@ func ProfileEditHandler(app *App) http.HandlerFunc { Title: "ThreadR - Edit Profile", Navbar: "profile", LoggedIn: true, + AllowSignup: app.allowSignup(), ShowCookieBanner: false, BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/handlers/signup.go b/handlers/signup.go index 1652840..8347e60 100644 --- a/handlers/signup.go +++ b/handlers/signup.go @@ -11,6 +11,18 @@ func SignupHandler(app *App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*sessions.Session) cookie, _ := r.Cookie("threadr_cookie_banner") + + settings, err := models.GetSiteSettings(app.DB) + if err != nil { + log.Printf("Error fetching site settings in SignupHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if !settings.AllowSignup { + http.Redirect(w, r, app.Config.ThreadrDir+"/login/?error=signup_disabled", http.StatusFound) + return + } + if r.Method == http.MethodPost { if !app.validateCSRFToken(r, session) { http.Error(w, "Invalid CSRF token", http.StatusForbidden) @@ -32,6 +44,7 @@ func SignupHandler(app *App) http.HandlerFunc { Title: "ThreadR - Sign Up", Navbar: "signup", LoggedIn: false, + AllowSignup: true, ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", @@ -59,6 +72,7 @@ func SignupHandler(app *App) http.HandlerFunc { Title: "ThreadR - Sign Up", Navbar: "signup", LoggedIn: false, + AllowSignup: true, ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", @@ -85,6 +99,7 @@ func SignupHandler(app *App) http.HandlerFunc { Title: "ThreadR - Sign Up", Navbar: "signup", LoggedIn: session.Values["user_id"] != nil, + AllowSignup: true, ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/handlers/site_settings.go b/handlers/site_settings.go new file mode 100644 index 0000000..e78e514 --- /dev/null +++ b/handlers/site_settings.go @@ -0,0 +1,16 @@ +package handlers + +import ( + "log" + "threadr/models" +) + +func (app *App) allowSignup() bool { + settings, err := models.GetSiteSettings(app.DB) + if err != nil { + log.Printf("Error fetching site settings: %v", err) + return true + } + + return settings.AllowSignup +} diff --git a/handlers/thread.go b/handlers/thread.go index 1586c19..9218584 100644 --- a/handlers/thread.go +++ b/handlers/thread.go @@ -165,6 +165,7 @@ func ThreadHandler(app *App) http.HandlerFunc { Title: "ThreadR - " + thread.Title, Navbar: "boards", LoggedIn: loggedIn, + AllowSignup: app.allowSignup(), ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/handlers/userhome.go b/handlers/userhome.go index 7e80e65..ae23a15 100644 --- a/handlers/userhome.go +++ b/handlers/userhome.go @@ -25,6 +25,9 @@ func UserHomeHandler(app *App) http.HandlerFunc { http.Error(w, "User not found", http.StatusNotFound) return } + + isAdmin := models.HasGlobalPermission(user, models.PermManageUsers) + data := struct { PageData Username string @@ -33,6 +36,8 @@ func UserHomeHandler(app *App) http.HandlerFunc { Title: "ThreadR - User Home", Navbar: "userhome", LoggedIn: true, + IsAdmin: isAdmin, + AllowSignup: app.allowSignup(), ShowCookieBanner: false, BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", diff --git a/main.go b/main.go index 6c738f2..c8a237f 100644 --- a/main.go +++ b/main.go @@ -231,6 +231,12 @@ func createTablesIfNotExist(db *sql.DB) error { return fmt.Errorf("error creating user_preferences table: %v", err) } + // Create site_settings table + err = models.EnsureSiteSettings(db) + if err != nil { + return fmt.Errorf("error ensuring site_settings table: %v", err) + } + log.Println("Database tables created.") return nil } @@ -351,6 +357,11 @@ func main() { // Normal startup (without automatic table creation) log.Println("Starting ThreadR server...") + err = models.EnsureSiteSettings(db) + if err != nil { + log.Fatal("Error ensuring site_settings table:", err) + } + dir, err := os.Getwd() if err != nil { log.Fatal("Error getting working directory:", err) @@ -364,6 +375,7 @@ func main() { // Parse page-specific templates with unique names pageTemplates := []string{ + "admin.html", "about.html", "board.html", "boards.html", @@ -428,6 +440,7 @@ func main() { handleAuthed("/profile/", handlers.ProfileHandler(app)) handleAuthed("/profile/edit/", handlers.ProfileEditHandler(app)) handleAuthed("/preferences/", handlers.PreferencesHandler(app)) + handleAuthed("/admin/", handlers.AdminHandler(app)) handleAuthed("/like/", handlers.LikeHandler(app)) handle("/news/", handlers.NewsHandler(app)) handle("/signup/", handlers.SignupHandler(app)) diff --git a/models/site_settings.go b/models/site_settings.go new file mode 100644 index 0000000..7ad56a3 --- /dev/null +++ b/models/site_settings.go @@ -0,0 +1,43 @@ +package models + +import "database/sql" + +const siteSettingsID = 1 + +type SiteSettings struct { + AllowSignup bool +} + +func EnsureSiteSettings(db *sql.DB) error { + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS site_settings ( + id INT PRIMARY KEY, + allow_signup BOOLEAN NOT NULL DEFAULT TRUE, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + )`) + if err != nil { + return err + } + + _, err = db.Exec(` + INSERT INTO site_settings (id, allow_signup) + VALUES (?, TRUE) + ON DUPLICATE KEY UPDATE id = id + `, siteSettingsID) + return err +} + +func GetSiteSettings(db *sql.DB) (*SiteSettings, error) { + settings := &SiteSettings{} + err := db.QueryRow("SELECT allow_signup FROM site_settings WHERE id = ?", siteSettingsID).Scan(&settings.AllowSignup) + if err != nil { + return nil, err + } + + return settings, nil +} + +func SetAllowSignup(db *sql.DB, allowSignup bool) error { + _, err := db.Exec("UPDATE site_settings SET allow_signup = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", allowSignup, siteSettingsID) + return err +} diff --git a/templates/pages/admin.html b/templates/pages/admin.html new file mode 100644 index 0000000..1c131b3 --- /dev/null +++ b/templates/pages/admin.html @@ -0,0 +1,39 @@ +{{define "admin"}} + + + + {{.Title}} + + + + + {{template "navbar" .}} +
+
+

Admin Panel

+
+ {{if .ShowSuccess}} +
+ Settings saved successfully! +
+ {{end}} +
+

Registration

+
+ + +

+ When disabled, the signup page redirects to login and prevents account creation. +

+ + +
+
+
+ {{template "cookie_banner" .}} + + +{{end}} diff --git a/templates/pages/userhome.html b/templates/pages/userhome.html index 69e687a..2808576 100644 --- a/templates/pages/userhome.html +++ b/templates/pages/userhome.html @@ -14,9 +14,12 @@

This is your user home page.

+ {{if .IsAdmin}} +

Go to Admin Panel

+ {{end}}
{{template "cookie_banner" .}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/partials/navbar.html b/templates/partials/navbar.html index 6cb89d0..642bf25 100644 --- a/templates/partials/navbar.html +++ b/templates/partials/navbar.html @@ -5,14 +5,19 @@
  • User Home
  • Profile
  • Preferences
  • + {{if .IsAdmin}} +
  • Admin
  • + {{end}}
  • Logout
  • {{else}}
  • Login
  • + {{if .AllowSignup}}
  • Sign Up
  • {{end}} + {{end}}
  • Boards
  • News
  • About
  • -{{end}} \ No newline at end of file +{{end}}