Add admin-controlled signup toggle and hide signup links.

jocadbz
Joca 2026-04-19 14:03:24 -03:00
parent 8ff0b7f2c2
commit a5a2e7063a
Signed by: jocadbz
GPG Key ID: B1836DCE2F50BDF7
21 changed files with 252 additions and 9 deletions

View File

@ -29,6 +29,7 @@ func AboutHandler(app *App) http.HandlerFunc {
Title: "ThreadR - About", Title: "ThreadR - About",
Navbar: "about", Navbar: "about",
LoggedIn: loggedIn, LoggedIn: loggedIn,
AllowSignup: app.allowSignup(),
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

89
handlers/admin.go Normal file
View File

@ -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
}
}
}

View File

@ -14,6 +14,8 @@ type PageData struct {
Title string Title string
Navbar string Navbar string
LoggedIn bool LoggedIn bool
IsAdmin bool
AllowSignup bool
ShowCookieBanner bool ShowCookieBanner bool
BasePath string BasePath string
StaticPath string StaticPath string

View File

@ -119,6 +119,7 @@ func BoardHandler(app *App) http.HandlerFunc {
Title: "ThreadR - " + board.Name, Title: "ThreadR - " + board.Name,
Navbar: "boards", Navbar: "boards",
LoggedIn: loggedIn, LoggedIn: loggedIn,
AllowSignup: app.allowSignup(),
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

View File

@ -107,6 +107,7 @@ func BoardsHandler(app *App) http.HandlerFunc {
Title: "ThreadR - Boards", Title: "ThreadR - Boards",
Navbar: "boards", Navbar: "boards",
LoggedIn: loggedIn, LoggedIn: loggedIn,
AllowSignup: app.allowSignup(),
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

View File

@ -239,6 +239,7 @@ func ChatHandler(app *App) http.HandlerFunc {
Title: "ThreadR Chat - " + board.Name, Title: "ThreadR Chat - " + board.Name,
Navbar: "boards", Navbar: "boards",
LoggedIn: true, LoggedIn: true,
AllowSignup: app.allowSignup(),
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

View File

@ -19,6 +19,7 @@ func HomeHandler(app *App) http.HandlerFunc {
Title: "ThreadR - Home", Title: "ThreadR - Home",
Navbar: "home", Navbar: "home",
LoggedIn: loggedIn, LoggedIn: loggedIn,
AllowSignup: app.allowSignup(),
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: filepath.Join(app.Config.ThreadrDir, "static"), StaticPath: filepath.Join(app.Config.ThreadrDir, "static"),

View File

@ -52,18 +52,21 @@ func LoginHandler(app *App) http.HandlerFunc {
Error string Error string
}{ }{
PageData: PageData{ PageData: PageData{
Title: "ThreadR - Login", Title: "ThreadR - Login",
Navbar: "login", Navbar: "login",
LoggedIn: false, LoggedIn: false,
BasePath: app.Config.ThreadrDir, AllowSignup: app.allowSignup(),
StaticPath: app.Config.ThreadrDir + "/static", BasePath: app.Config.ThreadrDir,
CurrentURL: r.URL.RequestURI(), StaticPath: app.Config.ThreadrDir + "/static",
CSRFToken: app.csrfToken(session), CurrentURL: r.URL.RequestURI(),
CSRFToken: app.csrfToken(session),
}, },
Error: "", Error: "",
} }
if r.URL.Query().Get("error") == "invalid" { if r.URL.Query().Get("error") == "invalid" {
data.Error = "Invalid username or password" 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 { if err := app.Tmpl.ExecuteTemplate(w, "login", data); err != nil {

View File

@ -86,6 +86,7 @@ func NewsHandler(app *App) http.HandlerFunc {
Title: "ThreadR - News", Title: "ThreadR - News",
Navbar: "news", Navbar: "news",
LoggedIn: loggedIn, LoggedIn: loggedIn,
AllowSignup: app.allowSignup(),
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

View File

@ -70,6 +70,7 @@ func PreferencesHandler(app *App) http.HandlerFunc {
Title: "ThreadR - Preferences", Title: "ThreadR - Preferences",
Navbar: "preferences", Navbar: "preferences",
LoggedIn: true, LoggedIn: true,
AllowSignup: app.allowSignup(),
ShowCookieBanner: false, ShowCookieBanner: false,
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

View File

@ -38,6 +38,7 @@ func ProfileHandler(app *App) http.HandlerFunc {
Title: "ThreadR - Profile", Title: "ThreadR - Profile",
Navbar: "profile", Navbar: "profile",
LoggedIn: true, LoggedIn: true,
AllowSignup: app.allowSignup(),
ShowCookieBanner: false, ShowCookieBanner: false,
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

View File

@ -133,6 +133,7 @@ func ProfileEditHandler(app *App) http.HandlerFunc {
Title: "ThreadR - Edit Profile", Title: "ThreadR - Edit Profile",
Navbar: "profile", Navbar: "profile",
LoggedIn: true, LoggedIn: true,
AllowSignup: app.allowSignup(),
ShowCookieBanner: false, ShowCookieBanner: false,
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

View File

@ -11,6 +11,18 @@ func SignupHandler(app *App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*sessions.Session) session := r.Context().Value("session").(*sessions.Session)
cookie, _ := r.Cookie("threadr_cookie_banner") 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 r.Method == http.MethodPost {
if !app.validateCSRFToken(r, session) { if !app.validateCSRFToken(r, session) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden) http.Error(w, "Invalid CSRF token", http.StatusForbidden)
@ -32,6 +44,7 @@ func SignupHandler(app *App) http.HandlerFunc {
Title: "ThreadR - Sign Up", Title: "ThreadR - Sign Up",
Navbar: "signup", Navbar: "signup",
LoggedIn: false, LoggedIn: false,
AllowSignup: true,
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",
@ -59,6 +72,7 @@ func SignupHandler(app *App) http.HandlerFunc {
Title: "ThreadR - Sign Up", Title: "ThreadR - Sign Up",
Navbar: "signup", Navbar: "signup",
LoggedIn: false, LoggedIn: false,
AllowSignup: true,
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",
@ -85,6 +99,7 @@ func SignupHandler(app *App) http.HandlerFunc {
Title: "ThreadR - Sign Up", Title: "ThreadR - Sign Up",
Navbar: "signup", Navbar: "signup",
LoggedIn: session.Values["user_id"] != nil, LoggedIn: session.Values["user_id"] != nil,
AllowSignup: true,
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

16
handlers/site_settings.go Normal file
View File

@ -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
}

View File

@ -165,6 +165,7 @@ func ThreadHandler(app *App) http.HandlerFunc {
Title: "ThreadR - " + thread.Title, Title: "ThreadR - " + thread.Title,
Navbar: "boards", Navbar: "boards",
LoggedIn: loggedIn, LoggedIn: loggedIn,
AllowSignup: app.allowSignup(),
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

View File

@ -25,6 +25,9 @@ func UserHomeHandler(app *App) http.HandlerFunc {
http.Error(w, "User not found", http.StatusNotFound) http.Error(w, "User not found", http.StatusNotFound)
return return
} }
isAdmin := models.HasGlobalPermission(user, models.PermManageUsers)
data := struct { data := struct {
PageData PageData
Username string Username string
@ -33,6 +36,8 @@ func UserHomeHandler(app *App) http.HandlerFunc {
Title: "ThreadR - User Home", Title: "ThreadR - User Home",
Navbar: "userhome", Navbar: "userhome",
LoggedIn: true, LoggedIn: true,
IsAdmin: isAdmin,
AllowSignup: app.allowSignup(),
ShowCookieBanner: false, ShowCookieBanner: false,
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",

13
main.go
View File

@ -231,6 +231,12 @@ func createTablesIfNotExist(db *sql.DB) error {
return fmt.Errorf("error creating user_preferences table: %v", err) 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.") log.Println("Database tables created.")
return nil return nil
} }
@ -351,6 +357,11 @@ func main() {
// Normal startup (without automatic table creation) // Normal startup (without automatic table creation)
log.Println("Starting ThreadR server...") log.Println("Starting ThreadR server...")
err = models.EnsureSiteSettings(db)
if err != nil {
log.Fatal("Error ensuring site_settings table:", err)
}
dir, err := os.Getwd() dir, err := os.Getwd()
if err != nil { if err != nil {
log.Fatal("Error getting working directory:", err) log.Fatal("Error getting working directory:", err)
@ -364,6 +375,7 @@ func main() {
// Parse page-specific templates with unique names // Parse page-specific templates with unique names
pageTemplates := []string{ pageTemplates := []string{
"admin.html",
"about.html", "about.html",
"board.html", "board.html",
"boards.html", "boards.html",
@ -428,6 +440,7 @@ func main() {
handleAuthed("/profile/", handlers.ProfileHandler(app)) handleAuthed("/profile/", handlers.ProfileHandler(app))
handleAuthed("/profile/edit/", handlers.ProfileEditHandler(app)) handleAuthed("/profile/edit/", handlers.ProfileEditHandler(app))
handleAuthed("/preferences/", handlers.PreferencesHandler(app)) handleAuthed("/preferences/", handlers.PreferencesHandler(app))
handleAuthed("/admin/", handlers.AdminHandler(app))
handleAuthed("/like/", handlers.LikeHandler(app)) handleAuthed("/like/", handlers.LikeHandler(app))
handle("/news/", handlers.NewsHandler(app)) handle("/news/", handlers.NewsHandler(app))
handle("/signup/", handlers.SignupHandler(app)) handle("/signup/", handlers.SignupHandler(app))

43
models/site_settings.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,39 @@
{{define "admin"}}
<!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>Admin Panel</h2>
</header>
{{if .ShowSuccess}}
<div class="notification success" style="position: static; margin-bottom: 1em; animation: none;">
Settings saved 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>
</label>
<p style="margin-left: 1.5em; margin-top: 0.25em; font-size: 0.9em; opacity: 0.8;">
When disabled, the signup page redirects to login and prevents account creation.
</p>
<input type="submit" value="Save Settings" style="margin-top: 1.5em;">
</form>
</section>
</main>
{{template "cookie_banner" .}}
</body>
</html>
{{end}}

View File

@ -14,9 +14,12 @@
</header> </header>
<section> <section>
<p>This is your user home page.</p> <p>This is your user home page.</p>
{{if .IsAdmin}}
<p><a href="{{.BasePath}}/admin/">Go to Admin Panel</a></p>
{{end}}
</section> </section>
</main> </main>
{{template "cookie_banner" .}} {{template "cookie_banner" .}}
</body> </body>
</html> </html>
{{end}} {{end}}

View File

@ -5,14 +5,19 @@
<li><a {{if eq .Navbar "userhome"}}class="active"{{end}} href="{{.BasePath}}/userhome/">User Home</a></li> <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 "profile"}}class="active"{{end}} href="{{.BasePath}}/profile/">Profile</a></li>
<li><a {{if eq .Navbar "preferences"}}class="active"{{end}} href="{{.BasePath}}/preferences/">Preferences</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>
{{end}}
<li><a href="{{.BasePath}}/logout/">Logout</a></li> <li><a href="{{.BasePath}}/logout/">Logout</a></li>
{{else}} {{else}}
<li><a {{if eq .Navbar "login"}}class="active"{{end}} href="{{.BasePath}}/login/">Login</a></li> <li><a {{if eq .Navbar "login"}}class="active"{{end}} href="{{.BasePath}}/login/">Login</a></li>
{{if .AllowSignup}}
<li><a {{if eq .Navbar "signup"}}class="active"{{end}} href="{{.BasePath}}/signup/">Sign Up</a></li> <li><a {{if eq .Navbar "signup"}}class="active"{{end}} href="{{.BasePath}}/signup/">Sign Up</a></li>
{{end}} {{end}}
{{end}}
<li><a {{if eq .Navbar "boards"}}class="active"{{end}} href="{{.BasePath}}/boards/">Boards</a></li> <li><a {{if eq .Navbar "boards"}}class="active"{{end}} href="{{.BasePath}}/boards/">Boards</a></li>
<li><a {{if eq .Navbar "news"}}class="active"{{end}} href="{{.BasePath}}/news/">News</a></li> <li><a {{if eq .Navbar "news"}}class="active"{{end}} href="{{.BasePath}}/news/">News</a></li>
<li><a {{if eq .Navbar "about"}}class="active"{{end}} href="{{.BasePath}}/about/">About</a></li> <li><a {{if eq .Navbar "about"}}class="active"{{end}} href="{{.BasePath}}/about/">About</a></li>
</ul> </ul>
<div class="topnav"></div> <div class="topnav"></div>
{{end}} {{end}}