Admin actions on user profile

This commit is contained in:
Asaf Gartner 2021-12-15 03:17:42 +02:00
parent 1c48aab863
commit c84b6842e2
7 changed files with 174 additions and 0 deletions

View File

@ -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) {

View File

@ -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
*/

View File

@ -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,

View File

@ -1,5 +1,32 @@
{{ template "base.html" . }}
{{ define "extrahead" }}
<style>
.led {
aspect-ratio: 1;
border-radius: 50%;
border-style: solid;
border-width: 1.5px;
display: inline-block;
}
.led.yellow {
background-color: #64501f;
border-color: #4f3700;
}
.led.yellow.on {
background-color: #fdf2d8;
border-color: #f9ad04;
box-shadow: 0 0 7px #ee9e06;
}
.admin .cover {
background: repeating-linear-gradient( -45deg, #ff6c00, #ff6c00 12px, #000000 5px, #000000 25px );
}
</style>
{{ end }}
{{ define "content" }}
<div class="flex flex-column flex-row-l">
<div class="
@ -34,6 +61,68 @@
</div>
</div>
</div>
{{ if .User }}
{{ if .User.IsStaff }}
<div class="mt3 mt0-ns mt3-l ml3-ns ml0-l flex flex-column items-start bg--card pa2 br2 admin">
<div class="flex flex-row w-100 items-center">
<b class="flex-grow-1">Admin actions</b>
<div class="led yellow" style="height: 12px; margin: 3px;"></div>
<a href="javascript:;" class="unlock">Unlock</a>
</div>
<div class="relative w-100">
<div class="bg--card cover absolute w-100 h-100 br2"></div>
<div class="mt3">
<div>User status:</div>
<form id="admin_set_status_form" method="POST" action="{{ .AdminSetStatusUrl }}">
{{ csrftoken .Session }}
<input type="hidden" name="user_id" value="{{ .ProfileUser.ID }}" />
<input type="hidden" name="username" value="{{ .ProfileUser.Username }}" />
<select name="status">
<option value="inactive" {{ if eq .ProfileUser.Status 1 }}selected{{ end }}>Brand new</option>
<option value="confirmed" {{ if eq .ProfileUser.Status 2 }}selected{{ end }}>Email confirmed</option>
<option value="approved" {{ if eq .ProfileUser.Status 3 }}selected{{ end }}>Admin approved</option>
<option value="banned" {{ if eq .ProfileUser.Status 4 }}selected{{ end }}>Banned</option>
</select>
<input type="submit" value="Set" />
<div class="c--dim f7">Only sets status. Doesn't delete anything.</div>
</form>
</div>
<div class="mt3">
<div>Danger zone:</div>
<form id="admin_nuke_form" method="POST" action="{{ .AdminNukeUrl }}">
{{ csrftoken .Session }}
<input type="hidden" name="user_id" value="{{ .ProfileUser.ID }}" />
<input type="hidden" name="username" value="{{ .ProfileUser.Username }}" />
<input type="submit" value="Nuke posts" />
</form>
</div>
</div>
<script>
let unlockEl = document.querySelector(".admin .unlock");
let adminUnlockLed = document.querySelector(".admin .led");
let adminUnlocked = false;
let panelEl = document.querySelector(".admin .cover");
unlockEl.addEventListener("click", function() {
adminUnlocked = true;
adminUnlockLed.classList.add("on");
panelEl.style.display = "none";
});
document.querySelector("#admin_set_status_form").addEventListener("submit", function(ev) {
if (!adminUnlocked) {
ev.preventDefault();
}
});
document.querySelector("#admin_nuke_form").addEventListener("submit", function(ev) {
if (!adminUnlocked) {
ev.preventDefault();
}
});
</script>
</div>
{{ end }}
{{ end }}
</div>
<div class="flex-grow-1 overflow-hidden">
{{ if or .OwnProfile .ProfileUserProjects }}

View File

@ -157,6 +157,7 @@ type User struct {
Username string
Email string
IsStaff bool
Status int
Name string
Blurb string

View File

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

View File

@ -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.")