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"}} + + +
+This is your user home page.
+ {{if .IsAdmin}} + + {{end}}