commit ee0cc33c9f9cd3f5285b3a6dd895f1146b5466f9
parent c6b62e6d161ec21f47225127b1850b0e54db2f7a
Author: cblgh <cblgh@cblgh.org>
Date: Thu, 7 Dec 2023 20:37:18 +0100
add new feature: multiple admins
* add-admin tool is used to bootstrap the process
* route /admin will be used for administration on the web instead of on
the server
wip admin view and web actions
flesh out admin reset of user password in web
Diffstat:
7 files changed, 440 insertions(+), 44 deletions(-)
diff --git a/cmd/add-admin/main.go b/cmd/add-admin/main.go
@@ -0,0 +1,65 @@
+package main
+
+import (
+ "cerca/database"
+ "flag"
+ "fmt"
+ "os"
+)
+
+func inform(msg string, args ...interface{}) {
+ if len(args) > 0 {
+ fmt.Printf("%s\n", fmt.Sprintf(msg, args...))
+ } else {
+ fmt.Printf("%s\n", msg)
+ }
+}
+
+func complain(msg string, args ...interface{}) {
+ if len(args) > 0 {
+ inform(msg, args)
+ } else {
+ inform(msg)
+ }
+ os.Exit(0)
+}
+
+func main() {
+ var username string
+ var forumDomain string
+ var dbPath string
+ flag.StringVar(&forumDomain, "url", "https://forum.merveilles.town", "root url to forum, referenced in output")
+ flag.StringVar(&username, "username", "", "username who should be made admin")
+ flag.StringVar(&dbPath, "database", "./data/forum.db", "full path to the forum database; e.g. ./data/forum.db")
+ flag.Parse()
+
+ usage := `usage
+ add-admin --username <username to make admin> --url <rool url to forum>
+ add-admin --help for more information
+ `
+
+ adminRoute := fmt.Sprintf("%s/admin", forumDomain)
+
+ if username == "" {
+ complain(usage)
+ }
+
+ // check if database exists! we dont wanna create a new db in this case ':)
+ if !database.CheckExists(dbPath) {
+ complain("couldn't find database at %s", dbPath)
+ }
+
+ db := database.InitDB(dbPath)
+
+ userid, err := db.GetUserID(username)
+ if err != nil {
+ complain("username %s not in database", username)
+ }
+ inform("Attempting to make %s (id %d) admin...", username, userid)
+ err = db.AddAdmin(userid)
+ if err != nil {
+ complain("Something went wrong: %s", err)
+ }
+ inform("Successfully added %s (id %d) as an admin", username, userid)
+ inform("Please visit %s for all your administration needs (changing usernames, resetting passwords, deleting user accounts)", adminRoute)
+}
diff --git a/cmd/admin-add-user/main.go b/cmd/admin-add-user/main.go
@@ -80,7 +80,7 @@ func main() {
db := database.InitDB(dbPath)
newPassword := crypto.GeneratePassword()
- info := createUser(username, newPassword, &db)
+ _ = createUser(username, newPassword, &db)
loginRoute := fmt.Sprintf("%s/login", forumDomain)
resetRoute := fmt.Sprintf("%s/reset", forumDomain)
diff --git a/cmd/admin-reset/main.go b/cmd/admin-reset/main.go
@@ -1,7 +1,6 @@
package main
import (
- "cerca/crypto"
"cerca/database"
"cerca/util"
"flag"
@@ -55,9 +54,6 @@ func main() {
if username == "" {
complain(usage)
}
- if !keypairFlag && !passwordFlag {
- complain("nothing to reset, exiting")
- }
// check if database exists! we dont wanna create a new db in this case ':)
if !database.CheckExists(dbPath) {
@@ -66,35 +62,12 @@ func main() {
db := database.InitDB(dbPath)
ed := util.Describe("admin reset")
+ newPassword, err := db.ResetPassword(userid)
- userid, err := db.GetUserID(username)
if err != nil {
- complain("username %s not in database", username)
- }
-
- // generate new password for user and set it in the database
- if passwordFlag {
- newPassword := crypto.GeneratePassword()
- passwordHash, err := crypto.HashPassword(newPassword)
- ed.Check(err, "hash new password")
- db.UpdateUserPasswordHash(userid, passwordHash)
-
- inform("successfully updated %s's password hash", username)
- inform("new temporary password %s", newPassword)
+ complain("reset password failed (%w)", err)
}
- // generate a new keypair for user and update user's pubkey record with new pubkey
- if keypairFlag {
- kp, err := crypto.GenerateKeypair()
- ed.Check(err, "generate keypair")
- kpBytes, err := kp.Marshal()
- ed.Check(err, "marshal keypair")
- pubkey, err := kp.PublicString()
- ed.Check(err, "get pubkey string")
- err = db.SetPubkey(userid, pubkey)
- ed.Check(err, "set new pubkey in database")
-
- inform("successfully changed %s's stored public key", username)
- inform("new keypair\n%s", string(kpBytes))
- }
+ inform("successfully updated %s's password hash", username)
+ inform("new temporary password %s", newPassword)
}
diff --git a/database/database.go b/database/database.go
@@ -3,6 +3,7 @@ package database
import (
"context"
"database/sql"
+ "cerca/crypto"
"errors"
"fmt"
"html/template"
@@ -63,6 +64,11 @@ func createTables(db *sql.DB) {
);
`,
`
+ CREATE TABLE IF NOT EXISTS admins(
+ id INTEGER PRIMARY KEY
+ );
+ `,
+ `
CREATE TABLE IF NOT EXISTS registrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userid INTEGER,
@@ -410,3 +416,115 @@ func (d DB) AddRegistration(userid int, verificationLink string) error {
}
return nil
}
+
+func (d DB) ResetPassword(userid int) (string, error) {
+ ed := util.Describe("reset password")
+ exists, err := d.CheckUserExists(userid)
+ if !exists {
+ return "", errors.New(fmt.Sprintf("reset password: userid %d did not exist", userid))
+ } else if err != nil {
+ return "", fmt.Errorf("reset password encountered an error (%w)", err)
+ }
+ // generate new password for user and set it in the database
+ newPassword := crypto.GeneratePassword()
+ passwordHash, err := crypto.HashPassword(newPassword)
+ if err != nil {
+ return "", ed.Eout(err, "hash password")
+ }
+ d.UpdateUserPasswordHash(userid, passwordHash)
+ return newPassword, nil
+}
+
+type User struct {
+ Name string
+ ID int
+}
+
+func (d DB) AddAdmin(userid int) error {
+ ed := util.Describe("add admin")
+ // make sure the id exists
+ exists, err := d.CheckUserExists(userid)
+ if !exists {
+ return errors.New(fmt.Sprintf("add admin: userid %d did not exist", userid))
+ }
+ if err != nil {
+ return ed.Eout(err, "CheckUserExists had an error")
+ }
+ isAdminAlready, err := d.IsUserAdmin(userid)
+ if isAdminAlready {
+ return errors.New(fmt.Sprintf("userid %d was already an admin", userid))
+ }
+ if err != nil {
+ // some kind of error, let's bubble it up
+ return ed.Eout(err, "IsUserAdmin")
+ }
+ // insert into table, we gots ourselves a new sheriff in town [|:D
+ stmt := `INSERT INTO admins (id) VALUES (?)`
+ _, err = d.db.Exec(stmt, userid)
+ if err != nil {
+ return ed.Eout(err, "inserting new admin")
+ }
+ return nil
+}
+func (d DB) IsUserAdmin (userid int) (bool, error) {
+ stmt := `SELECT 1 FROM admins WHERE id = ?`
+ return d.existsQuery(stmt, userid)
+}
+
+func (d DB) GetAdmins() []User {
+ ed := util.Describe("get admins")
+ query := `SELECT u.name, a.id
+ FROM users u
+ INNER JOIN admins a ON u.id = a.id
+ ORDER BY u.name
+ `
+ 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 user User
+ var admins []User
+ for rows.Next() {
+ if err := rows.Scan(&user.Name, &user.ID); err != nil {
+ ed.Check(err, "scanning loop")
+ }
+ admins = append(admins, user)
+ }
+ return admins
+}
+func (d DB) GetUsers(includeAdmin bool) []User {
+ ed := util.Describe("get users")
+ query := `SELECT u.name, u.id
+ FROM users u
+ %s
+ ORDER BY u.name
+ `
+
+ if includeAdmin {
+ query = fmt.Sprintf(query, "") // do nothing
+ } else {
+ query = fmt.Sprintf(query, "WHERE u.id NOT IN (select id from admins)") // do nothing
+ }
+
+ 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 user User
+ var users []User
+ for rows.Next() {
+ if err := rows.Scan(&user.Name, &user.ID); err != nil {
+ ed.Check(err, "scanning loop")
+ }
+ users = append(users, user)
+ }
+ return users
+}
diff --git a/html/admin.html b/html/admin.html
@@ -0,0 +1,57 @@
+{{ template "head" . }}
+<main>
+ <h1>{{ .Title }}</h1>
+ <!-- { if .IsAdmin } -->
+ <section>
+ <p>
+ Does someone wish admittance? You can <button>Add new user</button>.
+ </p>
+ <p>
+ If you want to stop being an admin, you can <button>Step down</button>.
+ </p>
+ </section>
+ {{ if .LoggedIn }}
+ <section>
+ <h2> Admins </h2>
+ {{ if len .Data.Admins | eq 0 }}
+ <p> there are no admins; chaos reigns </p>
+ {{ 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>
+ {{ end }}
+ {{ end }}
+ </section>
+
+ <section>
+ <h2> Users </h2>
+ {{ if len .Data.Users | eq 0 }}
+ <p> there are no other users </p>
+ {{ 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>
+ </form>
+ {{ end }}
+ </table>
+ {{ end }}
+ </section>
+
+ {{ end }}
+</main>
+{{ template "footer" . }}
diff --git a/html/head.html b/html/head.html
@@ -30,9 +30,11 @@
font-size: 1rem;
}
button {
- text-decoration: underline;
cursor: pointer;
}
+ button, select {
+ margin-bottom: 0;
+ }
#logo {
width: 48px;
height: 48px;
@@ -194,6 +196,9 @@
{{ if .QuickNav }}
<li><a href="#bottom">{{ "Bottom" | translate }}</a></li>
{{end}}
+ {{ if .IsAdmin }}
+ <li><a href="/admin">admin</a></li>
+ {{end}}
<li><a href="/about">{{ "About" | translate }}</a></li>
{{ if .HasRSS }}
<li><a href="/rss.xml">rss</a></li>
diff --git a/server/server.go b/server/server.go
@@ -6,6 +6,7 @@ import (
"fmt"
"html/template"
"log"
+ "strconv"
"net"
"net/http"
"net/url"
@@ -35,7 +36,8 @@ import (
type TemplateData struct {
Data interface{}
QuickNav bool
- LoggedIn bool // TODO (2022-01-09): put this in a middleware || template function or sth?
+ LoggedIn bool
+ IsAdmin bool
HasRSS bool
LoggedInID int
ForumName string
@@ -82,6 +84,12 @@ type ThreadData struct {
ThreadURL string
}
+type AdminsData struct {
+ Admins []database.User
+ Users []database.User
+ IsAdmin bool
+}
+
type RequestHandler struct {
db *database.DB
session *session.Session
@@ -161,6 +169,35 @@ func (h RequestHandler) IsLoggedIn(req *http.Request) (bool, int) {
return true, userid
}
+// TODO (2023-12-10): any vulns with this approach? could a user forge a session cookie with the user id of an admin?
+func (h RequestHandler) IsAdmin(req *http.Request) (bool, int) {
+ ed := util.Describe("IsAdmin")
+ userid, err := h.session.Get(req)
+ err = ed.Eout(err, "getting userid from session cookie")
+ if err != nil {
+ dump(err)
+ return false, -1
+ }
+
+ // make sure the user from the cookie actually exists
+ userExists, err := h.db.CheckUserExists(userid)
+ if err != nil {
+ dump(ed.Eout(err, "check userid in db"))
+ return false, -1
+ } else if !userExists {
+ return false, -1
+ }
+ // make sure the user id is actually an admin
+ userIsAdmin, err := h.db.IsUserAdmin(userid)
+ if err != nil {
+ dump(ed.Eout(err, "IsUserAdmin in db"))
+ return false, -1
+ } else if !userIsAdmin {
+ return false, -1
+ }
+ return true, userid
+}
+
// establish closure over config + translator so that it's present in templates during render
func generateTemplates(config types.Config, translator i18n.Translator) (*template.Template, error) {
// only read logo contents once when generating
@@ -224,6 +261,7 @@ func generateTemplates(config types.Config, translator i18n.Translator) (*templa
"register",
"register-success",
"thread",
+ "admin",
"password-reset",
"change-password",
"change-password-success",
@@ -265,9 +303,147 @@ 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{
+ Data: incomingData,
+ // the following two fields are defaults that usually are not set and which are cumbersome to set each time since
+ // they don't really matter / vary across invocations
+ HasRSS: h.config.RSS.URL != "",
+ LoggedIn: loggedIn,
+ }
+ h.renderView(res, "generic-message", data)
+ return
+}
+
+func (h *RequestHandler) AdminRemoveUser(res http.ResponseWriter, req *http.Request) {
+ 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"
+ }
+}
+
+func (h *RequestHandler) AdminMakeUserAdmin(res http.ResponseWriter, req *http.Request, targetUserid int) {
+ loggedIn, _ := h.IsLoggedIn(req)
+ isAdmin, _ := 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)
+ fmt.Println(errMsg)
+ data := GenericMessageData{
+ Title: "Make admin",
+ Message: errMsg,
+ }
+ 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))
+
+ // output copy-pastable credentials page for admin to send to the user
+ data := GenericMessageData{
+ Title: "Make admin success",
+ Message: fmt.Sprintf("User %s is now a fellow admin user!", username),
+ LinkMessage: "Go back to the",
+ LinkText: "admin view",
+ Link: "/admin",
+ }
+ h.renderGenericMessage(res, req, data)
+ return
+}
+
+func (h *RequestHandler) AdminResetUserPassword(res http.ResponseWriter, req *http.Request, targetUserid int) {
+ loggedIn, _ := h.IsLoggedIn(req)
+ isAdmin, _ := 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)
+ fmt.Println(errMsg)
+ data := GenericMessageData{
+ Title: "Admin reset password",
+ Message: errMsg,
+ }
+ 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))
+
+ username, _ := h.db.GetUsername(targetUserid)
+
+ // output copy-pastable credentials page for admin to send to the user
+ data := GenericMessageData{
+ Title: "Password reset successful!",
+ Message: fmt.Sprintf("Instructions: %s's password was reset to: %s. After logging in, please change your password by going to /reset", username, newPassword),
+ LinkMessage: "Go back to the",
+ LinkText: "admin view",
+ Link: "/admin",
+ }
+ h.renderGenericMessage(res, req, data)
+ return
+}
+// TODO (2023-12-10): introduce 2-quorum for consequential actions like
+// * make admin
+// * remove account
+// * (later: demote admin)
+// note: only make a 2-quorum if there are actually 2 admins
+func (h *RequestHandler) AdminRoute(res http.ResponseWriter, req *http.Request) {
+ loggedIn, userid := h.IsLoggedIn(req)
+ isAdmin, _ := h.IsAdmin(req)
+
+ if req.Method == "POST" && loggedIn && isAdmin {
+ action := req.PostFormValue("admin-action")
+ useridString := req.PostFormValue("userid")
+ 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)
+ case "make-admin":
+ h.AdminMakeUserAdmin(res, req, targetUserId)
+ fmt.Println("make admin!")
+ case "remove-account":
+ fmt.Println("gone with the account!")
+ }
+ return
+ }
+ if req.Method == "GET" && loggedIn {
+ if !isAdmin {
+ // TODO (2023-12-10): redirect to /admins
+ IndexRedirect(res, req)
+ return
+ }
+ admins := h.db.GetAdmins()
+ normalUsers := h.db.GetUsers(false) // do not include admins
+ data := AdminsData{Admins: admins, Users: normalUsers}
+ view := TemplateData{Title: "Forum Administration", Data: &data, HasRSS: false, LoggedIn: loggedIn, LoggedInID: userid}
+ h.renderView(res, "admin", view)
+ }
+}
+
func (h *RequestHandler) ThreadRoute(res http.ResponseWriter, req *http.Request) {
threadid, ok := util.GetURLPortion(req, 2)
loggedIn, userid := h.IsLoggedIn(req)
+ isAdmin, _ := h.IsAdmin(req)
if !ok {
title := h.translator.Translate("ErrThread404")
@@ -275,7 +451,7 @@ func (h *RequestHandler) ThreadRoute(res http.ResponseWriter, req *http.Request)
Title: title,
Message: h.translator.Translate("ErrThread404Message"),
}
- h.renderView(res, "generic-message", TemplateData{Data: data, HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn})
+ h.renderGenericMessage(res, req, data)
return
}
@@ -311,7 +487,7 @@ func (h *RequestHandler) ThreadRoute(res http.ResponseWriter, req *http.Request)
thread[i].Content = template.HTML(content)
}
data := ThreadData{Posts: thread, ThreadURL: req.URL.Path}
- view := TemplateData{Data: &data, QuickNav: loggedIn, HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn, LoggedInID: userid}
+ view := TemplateData{Data: &data, IsAdmin: isAdmin, QuickNav: loggedIn, HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn, LoggedInID: userid}
if len(thread) > 0 {
data.Title = thread[0].ThreadTitle
view.Title = data.Title
@@ -325,7 +501,7 @@ func (h RequestHandler) ErrorRoute(res http.ResponseWriter, req *http.Request, s
Title: title,
Message: fmt.Sprintf(h.translator.Translate("ErrGeneric404Message"), status),
}
- h.renderView(res, "generic-message", TemplateData{Data: data, Title: title})
+ h.renderGenericMessage(res, req, data)
}
func (h RequestHandler) IndexRoute(res http.ResponseWriter, req *http.Request) {
@@ -336,6 +512,7 @@ func (h RequestHandler) IndexRoute(res http.ResponseWriter, req *http.Request) {
}
loggedIn, _ := h.IsLoggedIn(req)
var mostRecentPost bool
+ isAdmin, _ := h.IsAdmin(req)
params := req.URL.Query()
if q, exists := params["sort"]; exists {
@@ -344,7 +521,7 @@ func (h RequestHandler) IndexRoute(res http.ResponseWriter, req *http.Request) {
}
// show index listing
threads := h.db.ListThreads(mostRecentPost)
- view := TemplateData{Data: IndexData{threads}, HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn, Title: h.translator.Translate("Threads")}
+ view := TemplateData{Data: IndexData{threads}, IsAdmin: isAdmin, HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn, Title: h.translator.Translate("Threads")}
h.renderView(res, "index", view)
}
@@ -458,7 +635,7 @@ func (h RequestHandler) handleChangePassword(res http.ResponseWriter, req *http.
Link: "/reset",
LinkText: h.translator.Translate("GoBack"),
}
- h.renderView(res, "generic-message", TemplateData{Data: data, Title: title})
+ h.renderGenericMessage(res, req, data)
}
_, uid := h.IsLoggedIn(req)
@@ -551,7 +728,7 @@ func (h RequestHandler) RegisterRoute(res http.ResponseWriter, req *http.Request
LinkMessage: h.translator.Translate("RegisterLinkMessage"),
LinkText: h.translator.Translate("Index"),
}
- h.renderView(res, "generic-message", TemplateData{Data: data, HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn, Title: h.translator.Translate("Register")})
+ h.renderGenericMessage(res, req, data)
return
}
@@ -655,7 +832,7 @@ func (h RequestHandler) GenericRoute(res http.ResponseWriter, req *http.Request)
LinkMessage: "Generic link messsage",
LinkText: "with link",
}
- h.renderView(res, "generic-message", TemplateData{Data: data})
+ h.renderGenericMessage(res, req, data)
}
func (h RequestHandler) AboutRoute(res http.ResponseWriter, req *http.Request) {
@@ -683,7 +860,7 @@ func (h *RequestHandler) NewThreadRoute(res http.ResponseWriter, req *http.Reque
LinkMessage: h.translator.Translate("NewThreadLinkMessage"),
LinkText: h.translator.Translate("LogIn"),
}
- h.renderView(res, "generic-message", TemplateData{Data: data, Title: title})
+ h.renderGenericMessage(res, req, data)
return
}
h.renderView(res, "new-thread", TemplateData{HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn, Title: h.translator.Translate("ThreadNew")})
@@ -699,7 +876,7 @@ func (h *RequestHandler) NewThreadRoute(res http.ResponseWriter, req *http.Reque
Title: h.translator.Translate("NewThreadCreateError"),
Message: h.translator.Translate("NewThreadCreateErrorMessage"),
}
- h.renderView(res, "generic-message", TemplateData{Data: data, Title: h.translator.Translate("ThreadNew")})
+ h.renderGenericMessage(res, req, data)
return
}
// update the rss feed
@@ -733,7 +910,7 @@ func (h *RequestHandler) DeletePostRoute(res http.ResponseWriter, req *http.Requ
renderErr := func(msg string) {
fmt.Println(msg)
genericErr.Message = msg
- h.renderView(res, "generic-message", TemplateData{Data: genericErr, HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn})
+ h.renderGenericMessage(res, req, genericErr)
}
if !loggedIn || !ok {
@@ -855,6 +1032,7 @@ func NewServer(allowlist []string, sessionKey, dir string, config types.Config)
/* note: be careful with trailing slashes; go's default handler is a bit sensitive */
// 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("/about", handler.AboutRoute)
s.ServeMux.HandleFunc("/logout", handler.LogoutRoute)
s.ServeMux.HandleFunc("/login", handler.LoginRoute)