diff --git a/src/hmnurl/hmnurl_test.go b/src/hmnurl/hmnurl_test.go index 77dba56..9c18d64 100644 --- a/src/hmnurl/hmnurl_test.go +++ b/src/hmnurl/hmnurl_test.go @@ -105,6 +105,8 @@ func TestUserSettings(t *testing.T) { func TestAdmin(t *testing.T) { AssertRegexMatch(t, BuildAdminAtomFeed(), RegexAdminAtomFeed, nil) AssertRegexMatch(t, BuildAdminApprovalQueue(), RegexAdminApprovalQueue, nil) + AssertRegexMatch(t, BuildAdminSetUserStatus(), RegexAdminSetUserStatus, nil) + AssertRegexMatch(t, BuildAdminNukeUser(), RegexAdminNukeUser, nil) } func TestSnippet(t *testing.T) { diff --git a/src/hmnurl/urls.go b/src/hmnurl/urls.go index 4e8e8ba..560f5db 100644 --- a/src/hmnurl/urls.go +++ b/src/hmnurl/urls.go @@ -216,6 +216,20 @@ func BuildAdminApprovalQueue() string { return Url("/admin/approvals", nil) } +var RegexAdminSetUserStatus = regexp.MustCompile(`^/admin/setuserstatus$`) + +func BuildAdminSetUserStatus() string { + defer CatchPanic() + return Url("/admin/setuserstatus", nil) +} + +var RegexAdminNukeUser = regexp.MustCompile(`^/admin/nukeuser$`) + +func BuildAdminNukeUser() string { + defer CatchPanic() + return Url("/admin/nukeuser", nil) +} + /* * Snippets */ diff --git a/src/templates/mapping.go b/src/templates/mapping.go index f6f178c..b2848db 100644 --- a/src/templates/mapping.go +++ b/src/templates/mapping.go @@ -193,6 +193,7 @@ func UserToTemplate(u *models.User, currentTheme string) User { Username: u.Username, Email: email, IsStaff: u.IsStaff, + Status: int(u.Status), Name: u.BestName(), Bio: u.Bio, diff --git a/src/templates/src/user_profile.html b/src/templates/src/user_profile.html index 88c24af..adffc7d 100644 --- a/src/templates/src/user_profile.html +++ b/src/templates/src/user_profile.html @@ -1,5 +1,32 @@ {{ template "base.html" . }} +{{ define "extrahead" }} + +{{ end }} + {{ define "content" }}
+
+ Admin actions +
+ Unlock +
+
+
+
+
User status:
+
+ {{ csrftoken .Session }} + + + + +
Only sets status. Doesn't delete anything.
+
+
+
+
Danger zone:
+
+ {{ csrftoken .Session }} + + + +
+
+
+ +
+ {{ end }} + {{ end }}
{{ if or .OwnProfile .ProfileUserProjects }} diff --git a/src/templates/types.go b/src/templates/types.go index 1d95d7f..73bda64 100644 --- a/src/templates/types.go +++ b/src/templates/types.go @@ -157,6 +157,7 @@ type User struct { Username string Email string IsStaff bool + Status int Name string Blurb string diff --git a/src/website/routes.go b/src/website/routes.go index e4b3a13..e9c948d 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -190,6 +190,8 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool) ht hmnOnly.GET(hmnurl.RegexAdminAtomFeed, AdminAtomFeed) hmnOnly.GET(hmnurl.RegexAdminApprovalQueue, adminMiddleware(AdminApprovalQueue)) hmnOnly.POST(hmnurl.RegexAdminApprovalQueue, adminMiddleware(csrfMiddleware(AdminApprovalQueueSubmit))) + hmnOnly.POST(hmnurl.RegexAdminSetUserStatus, adminMiddleware(csrfMiddleware(UserProfileAdminSetStatus))) + hmnOnly.POST(hmnurl.RegexAdminNukeUser, adminMiddleware(csrfMiddleware(UserProfileAdminNuke))) hmnOnly.GET(hmnurl.RegexFeed, Feed) hmnOnly.GET(hmnurl.RegexAtomFeed, AtomFeed) diff --git a/src/website/user.go b/src/website/user.go index 9d48c2f..7d05530 100644 --- a/src/website/user.go +++ b/src/website/user.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "sort" + "strconv" "strings" "time" @@ -32,6 +33,9 @@ type UserProfileTemplateData struct { CanAddProject bool NewProjectUrl string + + AdminSetStatusUrl string + AdminNukeUrl string } func UserProfile(c *RequestContext) ResponseData { @@ -191,6 +195,9 @@ func UserProfile(c *RequestContext) ResponseData { CanAddProject: numPersonalProjects < maxPersonalProjects, NewProjectUrl: hmnurl.BuildProjectNew(), + + AdminSetStatusUrl: hmnurl.BuildAdminSetUserStatus(), + AdminNukeUrl: hmnurl.BuildAdminNukeUser(), }, c.Perf) return res } @@ -440,6 +447,64 @@ func UserSettingsSave(c *RequestContext) ResponseData { return res } +func UserProfileAdminSetStatus(c *RequestContext) ResponseData { + c.Req.ParseForm() + + userIdStr := c.Req.Form.Get("user_id") + userId, err := strconv.Atoi(userIdStr) + if err != nil { + return RejectRequest(c, "No user id provided") + } + + status := c.Req.Form.Get("status") + var desiredStatus models.UserStatus + switch status { + case "inactive": + desiredStatus = models.UserStatusInactive + case "confirmed": + desiredStatus = models.UserStatusConfirmed + case "approved": + desiredStatus = models.UserStatusApproved + case "banned": + desiredStatus = models.UserStatusBanned + default: + return RejectRequest(c, "No legal user status provided") + } + + _, err = c.Conn.Exec(c.Context(), + ` + UPDATE auth_user + SET status = $1 + WHERE id = $2 + `, + desiredStatus, + userId, + ) + if err != nil { + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user status")) + } + res := c.Redirect(hmnurl.BuildUserProfile(c.Req.Form.Get("username")), http.StatusSeeOther) + res.AddFutureNotice("success", "Successfully set status") + return res +} + +func UserProfileAdminNuke(c *RequestContext) ResponseData { + c.Req.ParseForm() + userIdStr := c.Req.Form.Get("user_id") + userId, err := strconv.Atoi(userIdStr) + if err != nil { + return RejectRequest(c, "No user id provided") + } + + err = deleteAllPostsForUser(c.Context(), c.Conn, userId) + if err != nil { + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete user posts")) + } + res := c.Redirect(hmnurl.BuildUserProfile(c.Req.Form.Get("username")), http.StatusSeeOther) + res.AddFutureNotice("success", "Successfully nuked user") + return res +} + func updatePassword(c *RequestContext, tx pgx.Tx, old, new, confirm string) *ResponseData { if new != confirm { res := RejectRequest(c, "Your password and password confirmation did not match.")