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:
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)