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) {
|
func TestAdmin(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildAdminAtomFeed(), RegexAdminAtomFeed, nil)
|
AssertRegexMatch(t, BuildAdminAtomFeed(), RegexAdminAtomFeed, nil)
|
||||||
AssertRegexMatch(t, BuildAdminApprovalQueue(), RegexAdminApprovalQueue, nil)
|
AssertRegexMatch(t, BuildAdminApprovalQueue(), RegexAdminApprovalQueue, nil)
|
||||||
|
AssertRegexMatch(t, BuildAdminSetUserStatus(), RegexAdminSetUserStatus, nil)
|
||||||
|
AssertRegexMatch(t, BuildAdminNukeUser(), RegexAdminNukeUser, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSnippet(t *testing.T) {
|
func TestSnippet(t *testing.T) {
|
||||||
|
|
|
@ -216,6 +216,20 @@ func BuildAdminApprovalQueue() string {
|
||||||
return Url("/admin/approvals", nil)
|
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
|
* Snippets
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -193,6 +193,7 @@ func UserToTemplate(u *models.User, currentTheme string) User {
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
Email: email,
|
Email: email,
|
||||||
IsStaff: u.IsStaff,
|
IsStaff: u.IsStaff,
|
||||||
|
Status: int(u.Status),
|
||||||
|
|
||||||
Name: u.BestName(),
|
Name: u.BestName(),
|
||||||
Bio: u.Bio,
|
Bio: u.Bio,
|
||||||
|
|
|
@ -1,5 +1,32 @@
|
||||||
{{ template "base.html" . }}
|
{{ 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" }}
|
{{ define "content" }}
|
||||||
<div class="flex flex-column flex-row-l">
|
<div class="flex flex-column flex-row-l">
|
||||||
<div class="
|
<div class="
|
||||||
|
@ -34,6 +61,68 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
<div class="flex-grow-1 overflow-hidden">
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
{{ if or .OwnProfile .ProfileUserProjects }}
|
{{ if or .OwnProfile .ProfileUserProjects }}
|
||||||
|
|
|
@ -157,6 +157,7 @@ type User struct {
|
||||||
Username string
|
Username string
|
||||||
Email string
|
Email string
|
||||||
IsStaff bool
|
IsStaff bool
|
||||||
|
Status int
|
||||||
|
|
||||||
Name string
|
Name string
|
||||||
Blurb string
|
Blurb string
|
||||||
|
|
|
@ -190,6 +190,8 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool) ht
|
||||||
hmnOnly.GET(hmnurl.RegexAdminAtomFeed, AdminAtomFeed)
|
hmnOnly.GET(hmnurl.RegexAdminAtomFeed, AdminAtomFeed)
|
||||||
hmnOnly.GET(hmnurl.RegexAdminApprovalQueue, adminMiddleware(AdminApprovalQueue))
|
hmnOnly.GET(hmnurl.RegexAdminApprovalQueue, adminMiddleware(AdminApprovalQueue))
|
||||||
hmnOnly.POST(hmnurl.RegexAdminApprovalQueue, adminMiddleware(csrfMiddleware(AdminApprovalQueueSubmit)))
|
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.RegexFeed, Feed)
|
||||||
hmnOnly.GET(hmnurl.RegexAtomFeed, AtomFeed)
|
hmnOnly.GET(hmnurl.RegexAtomFeed, AtomFeed)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -32,6 +33,9 @@ type UserProfileTemplateData struct {
|
||||||
|
|
||||||
CanAddProject bool
|
CanAddProject bool
|
||||||
NewProjectUrl string
|
NewProjectUrl string
|
||||||
|
|
||||||
|
AdminSetStatusUrl string
|
||||||
|
AdminNukeUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
func UserProfile(c *RequestContext) ResponseData {
|
func UserProfile(c *RequestContext) ResponseData {
|
||||||
|
@ -191,6 +195,9 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
CanAddProject: numPersonalProjects < maxPersonalProjects,
|
CanAddProject: numPersonalProjects < maxPersonalProjects,
|
||||||
NewProjectUrl: hmnurl.BuildProjectNew(),
|
NewProjectUrl: hmnurl.BuildProjectNew(),
|
||||||
|
|
||||||
|
AdminSetStatusUrl: hmnurl.BuildAdminSetUserStatus(),
|
||||||
|
AdminNukeUrl: hmnurl.BuildAdminNukeUser(),
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -440,6 +447,64 @@ func UserSettingsSave(c *RequestContext) ResponseData {
|
||||||
return res
|
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 {
|
func updatePassword(c *RequestContext, tx pgx.Tx, old, new, confirm string) *ResponseData {
|
||||||
if new != confirm {
|
if new != confirm {
|
||||||
res := RejectRequest(c, "Your password and password confirmation did not match.")
|
res := RejectRequest(c, "Your password and password confirmation did not match.")
|
||||||
|
|
Loading…
Reference in New Issue