Admin actions on user profile
This commit is contained in:
parent
1c48aab863
commit
c84b6842e2
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -157,6 +157,7 @@ type User struct {
|
|||
Username string
|
||||
Email string
|
||||
IsStaff bool
|
||||
Status int
|
||||
|
||||
Name string
|
||||
Blurb string
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.")
|
||||
|
|
Loading…
Reference in New Issue