Add admin-controlled signup toggle and hide signup links.
parent
8ff0b7f2c2
commit
a5a2e7063a
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ type PageData struct {
|
|||
Title string
|
||||
Navbar string
|
||||
LoggedIn bool
|
||||
IsAdmin bool
|
||||
AllowSignup bool
|
||||
ShowCookieBanner bool
|
||||
BasePath string
|
||||
StaticPath string
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ func LoginHandler(app *App) http.HandlerFunc {
|
|||
Title: "ThreadR - Login",
|
||||
Navbar: "login",
|
||||
LoggedIn: false,
|
||||
AllowSignup: app.allowSignup(),
|
||||
BasePath: app.Config.ThreadrDir,
|
||||
StaticPath: app.Config.ThreadrDir + "/static",
|
||||
CurrentURL: r.URL.RequestURI(),
|
||||
|
|
@ -64,6 +65,8 @@ func LoginHandler(app *App) http.HandlerFunc {
|
|||
}
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
13
main.go
13
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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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}}
|
||||
|
|
@ -14,6 +14,9 @@
|
|||
</header>
|
||||
<section>
|
||||
<p>This is your user home page.</p>
|
||||
{{if .IsAdmin}}
|
||||
<p><a href="{{.BasePath}}/admin/">Go to Admin Panel</a></p>
|
||||
{{end}}
|
||||
</section>
|
||||
</main>
|
||||
{{template "cookie_banner" .}}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,16 @@
|
|||
<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 "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>
|
||||
{{else}}
|
||||
<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>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<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 "about"}}class="active"{{end}} href="{{.BasePath}}/about/">About</a></li>
|
||||
|
|
|
|||
Loading…
Reference in New Issue