cerca

lean forum software (pmc local branch)
Log | Files | Refs | README | LICENSE

commit 4acea12cf4463530874c8c32301a9d4dce13809a
parent ee0cc33c9f9cd3f5285b3a6dd895f1146b5466f9
Author: cblgh <cblgh@cblgh.org>
Date:   Mon, 11 Dec 2023 17:28:52 +0100

add moderation log

* add moderation logs (with translated output!)
* user removal mostly good! lil mod log bug need to suss out (ids are not being updated during removal\?)
* last tweak for deletes and moderation log to work as intended

Diffstat:
Mcmd/add-admin/main.go | 1+
Mcmd/admin-reset/main.go | 1+
Aconstants/constants.go | 13+++++++++++++
Mdatabase/database.go | 179++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mhtml/admin.html | 35++++++++++++++++++-----------------
Ahtml/admins-list.html | 22++++++++++++++++++++++
Ahtml/moderation-log.html | 33+++++++++++++++++++++++++++++++++
Mhtml/register.html | 2++
Mi18n/i18n.go | 11+++++++++++
Mserver/server.go | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
10 files changed, 386 insertions(+), 46 deletions(-)

diff --git a/cmd/add-admin/main.go b/cmd/add-admin/main.go @@ -56,6 +56,7 @@ func main() { complain("username %s not in database", username) } inform("Attempting to make %s (id %d) admin...", username, userid) + // TODO (2023-12-12): log cmd actions just as admin web-actions are logged err = db.AddAdmin(userid) if err != nil { complain("Something went wrong: %s", err) diff --git a/cmd/admin-reset/main.go b/cmd/admin-reset/main.go @@ -63,6 +63,7 @@ func main() { db := database.InitDB(dbPath) ed := util.Describe("admin reset") newPassword, err := db.ResetPassword(userid) + // TODO (2023-12-12): log cmd actions just as admin web-actions are logged if err != nil { complain("reset password failed (%w)", err) diff --git a/constants/constants.go b/constants/constants.go @@ -0,0 +1,13 @@ +package constants + +const ( + MODLOG_RESETPW = iota + MODLOG_ADMIN_VETO + MODLOG_ADMIN_MAKE + // MODLOG_ADMIN_PROPOSE + // MODLOG_ADMIN_CONFIRM + MODLOG_REMOVE_USER + // MODLOG_DELETE_VETO + // MODLOG_DELETE_PROPOSE + // MODLOG_DELETE_CONFIRM +) diff --git a/database/database.go b/database/database.go @@ -44,7 +44,25 @@ func InitDB(filepath string) DB { log.Fatalln("db is nil") } createTables(db) - return DB{db} + instance := DB{db} + instance.makeSureDefaultUsersExist() + return instance +} + +const DELETED_USER_NAME = "deleted user" +func (d DB) makeSureDefaultUsersExist() { + ed := util.Describe("create default users") + deletedUserExists, err := d.CheckUsernameExists(DELETED_USER_NAME) + if err != nil { + log.Fatalln(ed.Eout(err, "check username exists")) + } + if !deletedUserExists { + passwordHash, err := crypto.HashPassword(crypto.GeneratePassword()) + _, err = d.CreateUser(DELETED_USER_NAME, passwordHash) + if err != nil { + log.Fatalln(ed.Eout(err, "create deleted user")) + } + } } func createTables(db *sql.DB) { @@ -69,6 +87,18 @@ func createTables(db *sql.DB) { ); `, ` + CREATE TABLE IF NOT EXISTS moderation_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + actingid INTEGER NOT NULL, + recipientid INTEGER, + action INTEGER, + time DATE, + + FOREIGN KEY (actingid) REFERENCES users(id), + FOREIGN KEY (recipientid) REFERENCES users(id) + ); + `, + ` CREATE TABLE IF NOT EXISTS registrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, userid INTEGER, @@ -396,10 +426,89 @@ func (d DB) UpdateUserPasswordHash(userid int, newhash string) { util.Check(err, "changing user %d's description to %s", userid, newhash) } -func (d DB) DeleteUser(userid int) { - stmt := `DELETE FROM users WHERE id = ?` - _, err := d.Exec(stmt, userid) - util.Check(err, "deleting user %d", userid) +// there are a bunch of places that reference a user's id, so i don't want to break all of those +// +// i also want to avoid big invisible holes in a conversation's history + +// remove user performs the following operation: +// 1. checks to see if the DELETED USER exists; otherwise create it and remember its id +// +// 2. if it exists, we swap out the userid for the DELETED_USER in tables: +// - table threads authorid +// - table posts authorid +// - table moderation_log actingid or recipientid +// +// the entry in registrations correlating to userid is removed + +// if allowing deletion of post contents as well when removing account, +// userid should be used to get all posts from table posts and change the contents +// to say _deleted_ +func (d DB) RemoveUser(userid int) (finalErr error) { + ed := util.Describe("remove user") + // there is a single user we call the "deleted user", and we make sure this deleted user exists on startup + // they will take the place of the old user when they remove their account. + deletedUserID, err := d.GetUserID(DELETED_USER_NAME) + if err != nil { + log.Fatalln(ed.Eout(err, "get deleted user id")) + } + // create a transaction spanning all our removal-related ops + tx, err := d.db.BeginTx(context.Background(), &sql.TxOptions{}) // proper tx options? + rollbackOnErr:= func(incomingErr error) { + if incomingErr != nil { + _ = tx.Rollback() + log.Println(incomingErr, "rolling back") + finalErr = incomingErr + return + } + } + rollbackOnErr(ed.Eout(err, "start transaction")) + + // create prepared statements performing the required removal operations for tables that reference a userid as a + // foreign key: threads, posts, moderation_log, and registrations + threadsStmt, err := tx.Prepare("UPDATE threads SET authorid = ? WHERE authorid = ?") + rollbackOnErr(ed.Eout(err, "prepare threads stmt")) + defer threadsStmt.Close() + + postsStmt, err := tx.Prepare(`UPDATE posts SET content = "_deleted_", authorid = ? WHERE authorid = ?`) + rollbackOnErr(ed.Eout(err, "prepare posts stmt")) + defer postsStmt.Close() + + modlogStmt1, err := tx.Prepare("UPDATE moderation_log SET recipientid = ? WHERE recipientid = ?") + rollbackOnErr(ed.Eout(err, "prepare modlog stmt #1")) + defer modlogStmt1.Close() + + modlogStmt2, err := tx.Prepare("UPDATE moderation_log SET actingid = ? WHERE actingid = ?") + rollbackOnErr(ed.Eout(err, "prepare modlog stmt #2")) + defer modlogStmt2.Close() + + stmtReg, err := tx.Prepare("DELETE FROM registrations where userid = ?") + rollbackOnErr(ed.Eout(err, "prepare registrations stmt")) + defer stmtReg.Close() + + // and finally: removing the entry from the user's table itself + stmtUsers, err := tx.Prepare("DELETE FROM users where id = ?") + rollbackOnErr(ed.Eout(err, "prepare users stmt")) + defer stmtUsers.Close() + + _, err = threadsStmt.Exec(deletedUserID, userid) + rollbackOnErr(ed.Eout(err, "exec threads stmt")) + _, err = postsStmt.Exec(deletedUserID, userid) + rollbackOnErr(ed.Eout(err, "exec posts stmt")) + _, err = modlogStmt1.Exec(deletedUserID, userid) + fmt.Println("modlog1: err?", err) + rollbackOnErr(ed.Eout(err, "exec modlog #1 stmt")) + _, err = modlogStmt2.Exec(deletedUserID, userid) + fmt.Println("modlog2: err?", err) + rollbackOnErr(ed.Eout(err, "exec modlog #2 stmt")) + _, err = stmtReg.Exec(userid) + rollbackOnErr(ed.Eout(err, "exec registration stmt")) + _, err = stmtUsers.Exec(userid) + rollbackOnErr(ed.Eout(err, "exec users stmt")) + + err = tx.Commit() + ed.Check(err, "commit transaction") + finalErr = nil + return } func (d DB) AddRegistration(userid int, verificationLink string) error { @@ -417,6 +526,64 @@ func (d DB) AddRegistration(userid int, verificationLink string) error { return nil } +func (d DB) AddModerationLog(actingid, recipientid, action int) error { + ed := util.Describe("add moderation log") + t := time.Now() + // we have a recipient + var err error + if recipientid > 0 { + stmt := `INSERT INTO moderation_log (actingid, recipientid, action, time) VALUES (?, ?, ?, ?)` + _, err = d.Exec(stmt, actingid, recipientid, action, t) + } else { + // we are not listing a recipient + stmt := `INSERT INTO moderation_log (actingid, action, time) VALUES (?, ?, ?)` + _, err = d.Exec(stmt, actingid, action, t) + } + if err = ed.Eout(err, "exec prepared statement"); err != nil { + return err + } + return nil +} + +type ModerationEntry struct { + ActingUsername, RecipientUsername string + Action int + Time time.Time +} +func (d DB) GetModerationLogs () []ModerationEntry { + ed := util.Describe("moderation log") + query := `SELECT uact.name, urecp.name, m.action, m.time + FROM moderation_LOG m + LEFT JOIN users uact ON uact.id = m.actingid + LEFT JOIN users urecp ON urecp.id = m.recipientid + ORDER BY time DESC` + + stmt, err := d.db.Prepare(query) + ed.Check(err, "prep stmt") + defer stmt.Close() + + rows, err := stmt.Query() + util.Check(err, "run query") + defer rows.Close() + + var entry ModerationEntry + var logs []ModerationEntry + for rows.Next() { + var actingUsername, recipientUsername sql.NullString + if err := rows.Scan(&actingUsername, &recipientUsername, &entry.Action, &entry.Time); err != nil { + ed.Check(err, "scanning loop") + } + if actingUsername.Valid { + entry.ActingUsername = actingUsername.String + } + if recipientUsername.Valid { + entry.RecipientUsername = recipientUsername.String + } + logs = append(logs, entry) + } + return logs +} + func (d DB) ResetPassword(userid int) (string, error) { ed := util.Describe("reset password") exists, err := d.CheckUserExists(userid) @@ -466,6 +633,7 @@ func (d DB) AddAdmin(userid int) error { } return nil } + func (d DB) IsUserAdmin (userid int) (bool, error) { stmt := `SELECT 1 FROM admins WHERE id = ?` return d.existsQuery(stmt, userid) @@ -496,6 +664,7 @@ func (d DB) GetAdmins() []User { } return admins } + func (d DB) GetUsers(includeAdmin bool) []User { ed := util.Describe("get users") query := `SELECT u.name, u.id diff --git a/html/admin.html b/html/admin.html @@ -18,7 +18,7 @@ {{ else }} {{ $userID := .LoggedInID }} <!-- do some kind of extra styling to indicate "hey this is you!" --> {{ range $index, $user := .Data.Admins }} - <p> {{ $user.Name }} ({{ $user.ID }}) </p> + <p> {{ $user.Name }} ({{ $user.ID }}) {{ if eq $userID $user.ID }} <i>(this is you!)</i>{{ end }}</p> {{ end }} {{ end }} </section> @@ -30,22 +30,23 @@ {{ else }} <table> {{ range $index, $user := .Data.Users }} - <form method="POST"> - <input type="hidden" name="userid" value="{{$user.ID}}"> - <tr> - <td>{{ $user.Name }} ({{ $user.ID }})</td> - <td> - <!-- TODO only have one form and use select's form attribute to target that form --> - <select name="admin-action" action="/admin/" id="select-{{$user.ID}}"> - <option selected value="reset-password">Reset password</option> - <option value="remove-account">Remove account</option> - <option value="make-admin">Make admin</option> - </select> - </td> - <td> - <button type="submit">Submit</button> - </td> - </tr> + {{ if ne $user.Name "deleted user" }} + <form method="POST"> + <input type="hidden" name="userid" value="{{$user.ID}}"> + <tr> + <td>{{ $user.Name }} ({{ $user.ID }})</td> + <td> + <select name="admin-action" action="/admin/" id="select-{{$user.ID}}"> + <option selected value="reset-password">Reset password</option> + <option value="remove-account">Remove account</option> + <option value="make-admin">Make admin</option> + </select> + </td> + <td> + <button type="submit">Submit</button> + </td> + </tr> + {{ end }} </form> {{ end }} </table> diff --git a/html/admins-list.html b/html/admins-list.html @@ -0,0 +1,22 @@ +{{ template "head" . }} +<main> + <h1>{{ .Title }}</h1> + {{ if .LoggedIn }} + <section> + <p>View the <a href="/moderations">moderation log</a>.</p> + {{ if len .Data.Admins | eq 0 }} + <p> there are no admins; chaos reigns </p> + {{ else }} + <p>This forum currently has the following {{ len .Data.Admins }} admins: + <ul> + {{ range $index, $user := .Data.Admins }} + <li> {{ $user.Name }} </li> + {{ end }} + </ul> + </section> + {{ end }} + {{ else }} + <p> Only logged in users may view the forum's admins. </p> + {{ end }} +</main> +{{ template "footer" . }} diff --git a/html/moderation-log.html b/html/moderation-log.html @@ -0,0 +1,33 @@ +{{ template "head" . }} +<main> + <h1>{{ .Title }}</h1> + {{ if .LoggedIn }} + <section> + {{ if len .Data.Log | eq 100 }} + <p> there are no logged moderation actions </p> + {{ else }} + <p>This resource lists the moderation actions taken by the forum's administrators. {{ if .IsAdmin }} You are + viewing this page as an admin, you will see slightly more details. {{ end }}</p> + <style> + section ul { padding-left: 0; } + section ul li { + list-style-type: none; + border: darkred solid 1px; + } + section ul > li:nth-of-type(2n) { + color: wheat; + background: darkred; + } + </style> + <ul> + {{ range $index, $entry := .Data.Log }} + <li> {{ $entry | tohtml }} </li> + {{ end }} + </ul> + </section> + {{ end }} + {{ else }} + <p> Only logged-in users may view the moderation log. </p> + {{ end }} +</main> +{{ template "footer" . }} diff --git a/html/register.html b/html/register.html @@ -30,7 +30,9 @@ </div> </div> {{ end }} + <div> <input type="submit" value='{{ "Register" | translate | capitalize }}'> + </div> </form> {{ if .Data.ErrorMessage }} diff --git a/i18n/i18n.go b/i18n/i18n.go @@ -27,6 +27,17 @@ var English = map[string]string{ "SortRecentPosts": "recent posts", "SortRecentThreads": "most recent threads", + "modlogResetPassword": `<code>{{ .Data.Time }}</code> <b>{{ .Data.ActingUsername }}</b> reset a user's password`, + "modlogResetPasswordAdmin": `<code>{{ .Data.Time }}</code> <b>{{ .Data.ActingUsername }}</b> reset <b> {{ .Data.RecipientUsername}}</b>'s password`, + "modlogMakeAdmin": `<code>{{ .Data.Time }}</code> <b>{{ .Data.ActingUsername }}</b> made <b> {{ .Data.RecipientUsername}} an admin`, + // "modlogProposeAdmin": `<code>{{ .Data.Time }}</code> <b>{{ .Data.ActingUsername }}</b> made <b> {{ .Data.RecipientUsername}} an admin`, + // "modlogVetoAdmin": `<code>{{ .Data.Time }}</code> <b>{{ .Data.ActingUsername }}</b> vetoed making {{ .Data.RecipientUsername }} an admin`, + // "modlogConfirmAdmin": `<code>{{ .Data.Time }}</code> <b>{{ .Data.ActingUsername }}</b> confirmed making {{ .Data.RecipientUsername }} a new admin`, + // "modlogProposeDelete": `<code>{{ .Data.Time }}</code> <b>{{ .Data.ActingUsername }}</b> proposed deleting a user's account`, + // "modlogVetoDelete": `<code>{{ .Data.Time }}</code> <b>{{ .Data.ActingUsername }}</b> vetoed deleting a user's account`, + // "modlogConfirmDelete": `<code>{{ .Data.Time }}</code> <b>{{ .Data.ActingUsername }}</b> confirmed deleting a user's account`, + "modlogRemoveUser": `<code>{{ .Data.Time }}</code> <b>{{ .Data.ActingUsername }}</b> removed a user's account`, + "LoginNoAccount": "Don't have an account yet? <a href='/register'>Register</a> one.", "LoginFailure": "<b>Failed login attempt:</b> incorrect password, wrong username, or a non-existent user.", "LoginAlreadyLoggedIn": `You are already logged in. Would you like to <a href="/logout">log out</a>?`, diff --git a/server/server.go b/server/server.go @@ -18,6 +18,7 @@ import ( "time" "cerca/crypto" + "cerca/constants" "cerca/database" "cerca/defaults" cercaHTML "cerca/html" @@ -262,6 +263,8 @@ func generateTemplates(config types.Config, translator i18n.Translator) (*templa "register-success", "thread", "admin", + "admins-list", + "moderation-log", "password-reset", "change-password", "change-password-success", @@ -303,7 +306,6 @@ func (h RequestHandler) renderView(res http.ResponseWriter, viewName string, dat } } - func (h RequestHandler) renderGenericMessage(res http.ResponseWriter, req *http.Request, incomingData GenericMessageData) { loggedIn, _ := h.IsLoggedIn(req) data := TemplateData{ @@ -317,40 +319,68 @@ func (h RequestHandler) renderGenericMessage(res http.ResponseWriter, req *http. return } -func (h *RequestHandler) AdminRemoveUser(res http.ResponseWriter, req *http.Request) { +func (h *RequestHandler) AdminRemoveUser(res http.ResponseWriter, req *http.Request, targetUserid int) { + ed := util.Describe("Admin remove user") loggedIn, _ := h.IsLoggedIn(req) - if req.Method == "POST" && loggedIn { - // perform action - // get results - // render simple page saying "user <x> was removed but their posts were kept" + isAdmin, adminUserid := h.IsAdmin(req) + if req.Method == "GET" || !loggedIn || !isAdmin { + // redirect to index + IndexRedirect(res, req) + return } + + err := h.db.RemoveUser(targetUserid) + + if err != nil { + // TODO (2023-12-09): bubble up error to visible page as feedback for admin + errMsg := ed.Eout(err, "remove user failed") + fmt.Println(errMsg) + data := GenericMessageData{ + Title: "User removal", + Message: errMsg.Error(), + } + h.renderGenericMessage(res, req, data) + return + } + + err = h.db.AddModerationLog(adminUserid, -1, constants.MODLOG_REMOVE_USER) + if err != nil { + fmt.Println(ed.Eout(err, "error adding moderation log")) + } + // success! redirect back to /admin + http.Redirect(res, req, "/admin", http.StatusSeeOther) } func (h *RequestHandler) AdminMakeUserAdmin(res http.ResponseWriter, req *http.Request, targetUserid int) { + ed := util.Describe("make user admin") loggedIn, _ := h.IsLoggedIn(req) - isAdmin, _ := h.IsAdmin(req) + isAdmin, adminUserid := h.IsAdmin(req) if req.Method == "GET" || !loggedIn || !isAdmin { // redirect to index IndexRedirect(res, req) return } + // TODO (2023-12-10): introduce 2-quorom err := h.db.AddAdmin(targetUserid) + if err != nil { // TODO (2023-12-09): bubble up error to visible page as feedback for admin - errMsg := fmt.Sprintf("make admin failed (%w)\n", err) + errMsg := ed.Eout(err, "make admin failed") fmt.Println(errMsg) data := GenericMessageData{ Title: "Make admin", - Message: errMsg, + Message: errMsg.Error(), } h.renderGenericMessage(res, req, data) return } - // adminUsername, _ := h.db.GetUsername(adminUserid) + username, _ := h.db.GetUsername(targetUserid) - // TODO (2023-12-12): h.db.LogModerationAction(adminUserid, targerUserid, fmt.Sprintf("%s made %s an admin", - // adminUsername, username)) + err = h.db.AddModerationLog(adminUserid, targetUserid, constants.MODLOG_ADMIN_MAKE) + if err != nil { + fmt.Println(ed.Eout(err, "error adding moderation log")) + } // output copy-pastable credentials page for admin to send to the user data := GenericMessageData{ @@ -361,31 +391,36 @@ func (h *RequestHandler) AdminMakeUserAdmin(res http.ResponseWriter, req *http.R Link: "/admin", } h.renderGenericMessage(res, req, data) - return } func (h *RequestHandler) AdminResetUserPassword(res http.ResponseWriter, req *http.Request, targetUserid int) { + ed := util.Describe("admin reset password") loggedIn, _ := h.IsLoggedIn(req) - isAdmin, _ := h.IsAdmin(req) + isAdmin, adminUserid := h.IsAdmin(req) if req.Method == "GET" || !loggedIn || !isAdmin { // redirect to index IndexRedirect(res, req) return } + newPassword, err := h.db.ResetPassword(targetUserid) + if err != nil { // TODO (2023-12-09): bubble up error to visible page as feedback for admin - errMsg := fmt.Sprintf("reset password failed (%w)\n", err) + errMsg := ed.Eout(err, "reset password failed") fmt.Println(errMsg) data := GenericMessageData{ Title: "Admin reset password", - Message: errMsg, + Message: errMsg.Error(), } h.renderGenericMessage(res, req, data) return } - // adminUsername, _ := h.db.GetUsername(adminUserid) - // TODO (2023-12-12): h.db.LogModerationAction(adminUserid, targerUserid, fmt.Sprintf("%s changed reset a user's password", adminUsername)) + + err = h.db.AddModerationLog(adminUserid, targetUserid, constants.MODLOG_RESETPW) + if err != nil { + fmt.Println(ed.Eout(err, "error adding moderation log")) + } username, _ := h.db.GetUsername(targetUserid) @@ -398,7 +433,47 @@ func (h *RequestHandler) AdminResetUserPassword(res http.ResponseWriter, req *ht Link: "/admin", } h.renderGenericMessage(res, req, data) - return +} + +type ModerationData struct { + Log []string +} + +// Note: this will by definition contain ugc, so we need to escape all usernames with html.EscapeString(username) before +// populating ModerationLogEntry +/* sorted by time descending, from latest entry to oldest */ + +func (h *RequestHandler) ModerationLogRoute(res http.ResponseWriter, req *http.Request) { + loggedIn, _ := h.IsLoggedIn(req) + isAdmin, _ := h.IsAdmin(req) + logs := h.db.GetModerationLogs() + fmt.Println("logs", logs) + viewData := ModerationData{Log: make([]string, 0)} + type translationData struct { + Time, ActingUsername, RecipientUsername string + } + for _, entry := range logs { + var tdata translationData + var translationString string + tdata.Time = entry.Time.Format("2006-01-02 15:04:05") + tdata.ActingUsername = template.HTMLEscapeString(entry.ActingUsername) + tdata.RecipientUsername = template.HTMLEscapeString(entry.RecipientUsername) + switch entry.Action { + case constants.MODLOG_RESETPW: + translationString = "modlogResetPassword" + if isAdmin { + translationString += "Admin" + } + case constants.MODLOG_ADMIN_MAKE: + translationString = "modlogMakeAdmin" + case constants.MODLOG_REMOVE_USER: + translationString = "modlogRemoveUser" + } + str := h.translator.TranslateWithData(translationString, i18n.TranslationData{Data: tdata}) + viewData.Log = append(viewData.Log, str) + } + view := TemplateData{Title: "Moderation log", IsAdmin: isAdmin, LoggedIn: loggedIn, Data: viewData} + h.renderView(res, "moderation-log", view) } // TODO (2023-12-10): introduce 2-quorum for consequential actions like // * make admin @@ -412,24 +487,25 @@ func (h *RequestHandler) AdminRoute(res http.ResponseWriter, req *http.Request) if req.Method == "POST" && loggedIn && isAdmin { action := req.PostFormValue("admin-action") useridString := req.PostFormValue("userid") - targetUserId, err := strconv.Atoi(useridString) + targetUserid, err := strconv.Atoi(useridString) util.Check(err, "convert user id string to a plain userid") switch action { case "reset-password": - h.AdminResetUserPassword(res, req, targetUserId) + h.AdminResetUserPassword(res, req, targetUserid) case "make-admin": - h.AdminMakeUserAdmin(res, req, targetUserId) + h.AdminMakeUserAdmin(res, req, targetUserid) fmt.Println("make admin!") case "remove-account": fmt.Println("gone with the account!") + h.AdminRemoveUser(res, req, targetUserid) } return } if req.Method == "GET" && loggedIn { if !isAdmin { // TODO (2023-12-10): redirect to /admins - IndexRedirect(res, req) + h.ListAdminsRoute(res, req) return } admins := h.db.GetAdmins() @@ -440,6 +516,15 @@ func (h *RequestHandler) AdminRoute(res http.ResponseWriter, req *http.Request) } } +func (h *RequestHandler) ListAdminsRoute(res http.ResponseWriter, req *http.Request) { + loggedIn, _ := h.IsLoggedIn(req) + admins := h.db.GetAdmins() + data := AdminsData{Admins: admins} + view := TemplateData{Title: "Forum Administrators", Data: &data, HasRSS: false, LoggedIn: loggedIn} + h.renderView(res, "admins-list", view) + return +} + func (h *RequestHandler) ThreadRoute(res http.ResponseWriter, req *http.Request) { threadid, ok := util.GetURLPortion(req, 2) loggedIn, userid := h.IsLoggedIn(req) @@ -816,7 +901,7 @@ func (h RequestHandler) RegisterRoute(res http.ResponseWriter, req *http.Request if err = ed.Eout(err, "add registration"); err != nil { dump(err) } - h.renderView(res, "register-success", TemplateData{HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn, Title: h.translator.Translate("RegisterSuccess")}) + h.renderView(res, "register-success", TemplateData{HasRSS: h.config.RSS.URL != "", LoggedIn: true, Title: h.translator.Translate("RegisterSuccess")}) default: fmt.Println("non get/post method, redirecting to index") IndexRedirect(res, req) @@ -1033,6 +1118,8 @@ func NewServer(allowlist []string, sessionKey, dir string, config types.Config) // TODO (2022-01-10): introduce middleware to make sure there is never an issue with trailing slashes s.ServeMux.HandleFunc("/reset/", handler.ResetPasswordRoute) s.ServeMux.HandleFunc("/admin", handler.AdminRoute) + s.ServeMux.HandleFunc("/moderations", handler.ModerationLogRoute) + s.ServeMux.HandleFunc("/admins", handler.ListAdminsRoute) s.ServeMux.HandleFunc("/about", handler.AboutRoute) s.ServeMux.HandleFunc("/logout", handler.LogoutRoute) s.ServeMux.HandleFunc("/login", handler.LoginRoute)