Admin Actions
* Add impersonate feature * Add ban/unban user feature * Add promote/demote admin status feature * Add admin user deletion feature * Admin ability to see other status certification pics * Nav bar indicator of pending admin actions such as cert pics needing approval * Admin ability to search cert pics for specific user
This commit is contained in:
parent
0caf12eb00
commit
dbca37977e
|
@ -56,6 +56,13 @@ func Login() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Is their account banned or disabled?
|
||||
if user.Status != models.UserStatusActive {
|
||||
session.FlashError(w, r, "Your account has been %s. If you believe this was done in error, please contact support.", user.Status)
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// OK. Log in the user's session.
|
||||
session.LoginUser(w, r, user)
|
||||
|
||||
|
|
|
@ -37,6 +37,12 @@ func Profile() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Banned or disabled? Only admin can view then.
|
||||
if user.Status != models.UserStatusActive && !currentUser.IsAdmin {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
vars := map[string]interface{}{
|
||||
"User": user,
|
||||
"IsFriend": models.FriendStatus(currentUser.ID, user.ID),
|
||||
|
|
126
pkg/controller/admin/user_actions.go
Normal file
126
pkg/controller/admin/user_actions.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||
"git.kirsle.net/apps/gosocial/pkg/models/deletion"
|
||||
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||
)
|
||||
|
||||
// Admin actions against a user account.
|
||||
func UserActions() http.HandlerFunc {
|
||||
tmpl := templates.Must("admin/user_actions.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
intent = r.FormValue("intent")
|
||||
confirm = r.Method == http.MethodPost
|
||||
userId uint64
|
||||
)
|
||||
|
||||
// Get current user.
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Failed to get current user: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
if idInt, err := strconv.Atoi(r.FormValue("user_id")); err == nil {
|
||||
userId = uint64(idInt)
|
||||
} else {
|
||||
session.FlashError(w, r, "Invalid or missing user_id parameter: %s", err)
|
||||
templates.Redirect(w, "/admin")
|
||||
return
|
||||
}
|
||||
|
||||
// Get this user.
|
||||
user, err := models.GetUser(userId)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Didn't find user ID in database: %s", err)
|
||||
templates.Redirect(w, "/admin")
|
||||
return
|
||||
}
|
||||
|
||||
switch intent {
|
||||
case "impersonate":
|
||||
if confirm {
|
||||
if err := session.ImpersonateUser(w, r, user, currentUser); err != nil {
|
||||
session.FlashError(w, r, "Failed to impersonate user: %s", err)
|
||||
} else {
|
||||
session.Flash(w, r, "You are now impersonating %s", user.Username)
|
||||
templates.Redirect(w, "/me")
|
||||
return
|
||||
}
|
||||
}
|
||||
case "ban":
|
||||
if confirm {
|
||||
status := r.PostFormValue("status")
|
||||
if status == "active" {
|
||||
user.Status = models.UserStatusActive
|
||||
} else if status == "banned" {
|
||||
user.Status = models.UserStatusBanned
|
||||
}
|
||||
|
||||
user.Save()
|
||||
session.Flash(w, r, "User ban status updated!")
|
||||
templates.Redirect(w, "/u/"+user.Username)
|
||||
return
|
||||
}
|
||||
case "promote":
|
||||
if confirm {
|
||||
action := r.PostFormValue("action")
|
||||
user.IsAdmin = action == "promote"
|
||||
user.Save()
|
||||
session.Flash(w, r, "User admin status updated!")
|
||||
templates.Redirect(w, "/u/"+user.Username)
|
||||
return
|
||||
}
|
||||
case "delete":
|
||||
if confirm {
|
||||
if err := deletion.DeleteUser(user); err != nil {
|
||||
session.FlashError(w, r, "Failed when deleting the user: %s", err)
|
||||
} else {
|
||||
session.Flash(w, r, "User has been deleted!")
|
||||
}
|
||||
templates.Redirect(w, "/admin")
|
||||
return
|
||||
}
|
||||
default:
|
||||
session.FlashError(w, r, "Unsupported admin user intent: %s", intent)
|
||||
templates.Redirect(w, "/admin")
|
||||
return
|
||||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"Intent": intent,
|
||||
"User": user,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Un-impersonate a user account.
|
||||
func Unimpersonate() http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sess := session.Get(r)
|
||||
if sess.Impersonator > 0 {
|
||||
user, err := models.GetUser(sess.Impersonator)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't unimpersonate: impersonator (%d) is not an admin!", user.ID)
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
session.LoginUser(w, r, user)
|
||||
session.Flash(w, r, "No longer impersonating.")
|
||||
templates.Redirect(w, "/")
|
||||
}
|
||||
templates.Redirect(w, "/")
|
||||
})
|
||||
}
|
|
@ -167,6 +167,40 @@ func Certification() http.HandlerFunc {
|
|||
func AdminCertification() http.HandlerFunc {
|
||||
tmpl := templates.Must("admin/certification.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// View status
|
||||
var view = r.FormValue("view")
|
||||
if view == "" {
|
||||
view = "pending"
|
||||
}
|
||||
|
||||
// Short circuit the GET view for username/email search (exact match)
|
||||
if username := r.FormValue("username"); username != "" {
|
||||
user, err := models.FindUser(username)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Username or email '%s' not found.", username)
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := models.GetCertificationPhoto(user.ID)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't get their certification photo: %s", err)
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"View": view,
|
||||
"Photos": []*models.CertificationPhoto{cert},
|
||||
"UserMap": &models.UserMap{user.ID: user},
|
||||
"FoundUser": user,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Making a verdict?
|
||||
if r.Method == http.MethodPost {
|
||||
var (
|
||||
|
@ -275,7 +309,7 @@ func AdminCertification() http.HandlerFunc {
|
|||
Sort: "updated_at desc",
|
||||
}
|
||||
pager.ParsePage(r)
|
||||
photos, err := models.CertificationPhotosNeedingApproval(models.CertificationPhotoPending, pager)
|
||||
photos, err := models.CertificationPhotosNeedingApproval(models.CertificationPhotoStatus(view), pager)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't load certification photos from DB: %s", err)
|
||||
}
|
||||
|
@ -291,6 +325,7 @@ func AdminCertification() http.HandlerFunc {
|
|||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"View": view,
|
||||
"Photos": photos,
|
||||
"UserMap": userMap,
|
||||
"Pager": pager,
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||
"git.kirsle.net/apps/gosocial/pkg/controller/photo"
|
||||
"git.kirsle.net/apps/gosocial/pkg/log"
|
||||
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||
)
|
||||
|
@ -24,6 +25,19 @@ func LoginRequired(handler http.Handler) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
// Are they banned or disabled?
|
||||
if user.Status == models.UserStatusDisabled {
|
||||
session.LogoutUser(w, r)
|
||||
session.FlashError(w, r, "Your account has been disabled and you are now logged out.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
} else if user.Status == models.UserStatusBanned {
|
||||
session.LogoutUser(w, r)
|
||||
session.FlashError(w, r, "Your account has been banned and you are now logged out.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
// Ping LastLoginAt for long lived sessions.
|
||||
if time.Since(user.LastLoginAt) > config.LastLoginAtCooldown {
|
||||
user.LastLoginAt = time.Now()
|
||||
|
|
|
@ -63,6 +63,13 @@ func CertificationPhotosNeedingApproval(status CertificationPhotoStatus, pager *
|
|||
return p, result.Error
|
||||
}
|
||||
|
||||
// CountCertificationPhotosNeedingApproval gets the count of pending photos for admin alert.
|
||||
func CountCertificationPhotosNeedingApproval() int64 {
|
||||
var count int64
|
||||
DB.Where("status = ?", CertificationPhotoPending).Model(&CertificationPhoto{}).Count(&count)
|
||||
return count
|
||||
}
|
||||
|
||||
// Save photo.
|
||||
func (p *CertificationPhoto) Save() error {
|
||||
result := DB.Save(p)
|
||||
|
|
|
@ -47,6 +47,7 @@ type UserStatus string
|
|||
const (
|
||||
UserStatusActive = "active"
|
||||
UserStatusDisabled = "disabled"
|
||||
UserStatusBanned = "banned"
|
||||
)
|
||||
|
||||
// CreateUser. It is assumed username and email are correctly formatted.
|
||||
|
|
|
@ -41,6 +41,7 @@ func New() http.Handler {
|
|||
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
||||
mux.Handle("/friends", middleware.LoginRequired(friend.Friends()))
|
||||
mux.Handle("/friends/add", middleware.LoginRequired(friend.AddFriend()))
|
||||
mux.Handle("/admin/unimpersonate", middleware.LoginRequired(admin.Unimpersonate()))
|
||||
|
||||
// Certification Required. Pages that only full (verified) members can access.
|
||||
mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery()))
|
||||
|
@ -49,6 +50,7 @@ func New() http.Handler {
|
|||
// Admin endpoints.
|
||||
mux.Handle("/admin", middleware.AdminRequired(admin.Dashboard()))
|
||||
mux.Handle("/admin/photo/certification", middleware.AdminRequired(photo.AdminCertification()))
|
||||
mux.Handle("/admin/user-action", middleware.AdminRequired(admin.UserActions()))
|
||||
|
||||
// JSON API endpoints.
|
||||
mux.HandleFunc("/v1/version", api.Version())
|
||||
|
|
|
@ -16,12 +16,13 @@ import (
|
|||
|
||||
// Session cookie object that is kept server side in Redis.
|
||||
type Session struct {
|
||||
UUID string `json:"-"` // not stored
|
||||
LoggedIn bool `json:"loggedIn"`
|
||||
UserID uint64 `json:"userId,omitempty"`
|
||||
Flashes []string `json:"flashes,omitempty"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
UUID string `json:"-"` // not stored
|
||||
LoggedIn bool `json:"loggedIn"`
|
||||
UserID uint64 `json:"userId,omitempty"`
|
||||
Flashes []string `json:"flashes,omitempty"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
Impersonator uint64 `json:"impersonator,omitempty"`
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -88,6 +89,7 @@ func (s *Session) Save(w http.ResponseWriter) {
|
|||
Name: config.SessionCookieName,
|
||||
Value: s.UUID,
|
||||
MaxAge: config.SessionCookieMaxAge,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
|
@ -144,6 +146,7 @@ func LoginUser(w http.ResponseWriter, r *http.Request, u *models.User) error {
|
|||
sess := Get(r)
|
||||
sess.LoggedIn = true
|
||||
sess.UserID = u.ID
|
||||
sess.Impersonator = 0
|
||||
sess.Save(w)
|
||||
|
||||
// Ping the user's last login time.
|
||||
|
@ -151,6 +154,32 @@ func LoginUser(w http.ResponseWriter, r *http.Request, u *models.User) error {
|
|||
return u.Save()
|
||||
}
|
||||
|
||||
// ImpersonateUser assumes the role of the user impersonated by an admin uid.
|
||||
func ImpersonateUser(w http.ResponseWriter, r *http.Request, u *models.User, impersonator *models.User) error {
|
||||
if u == nil || u.ID == 0 {
|
||||
return errors.New("not a valid user account")
|
||||
}
|
||||
if impersonator == nil || impersonator.ID == 0 || !impersonator.IsAdmin {
|
||||
return errors.New("impersonator not a valid admin account")
|
||||
}
|
||||
|
||||
sess := Get(r)
|
||||
sess.LoggedIn = true
|
||||
sess.UserID = u.ID
|
||||
sess.Impersonator = impersonator.ID
|
||||
sess.Save(w)
|
||||
|
||||
// Ping the user's last login time.
|
||||
u.LastLoginAt = time.Now()
|
||||
return u.Save()
|
||||
}
|
||||
|
||||
// Impersonated returns if the current session has an impersonator.
|
||||
func Impersonated(r *http.Request) bool {
|
||||
sess := Get(r)
|
||||
return sess.Impersonator > 0
|
||||
}
|
||||
|
||||
// LogoutUser signs a user out.
|
||||
func LogoutUser(w http.ResponseWriter, r *http.Request) {
|
||||
sess := Get(r)
|
||||
|
|
|
@ -30,11 +30,16 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
|||
m["CurrentUser"] = nil
|
||||
m["NavUnreadMessages"] = 0
|
||||
m["NavFriendRequests"] = 0
|
||||
m["NavAdminNotifications"] = 0 // total count of admin notifications for nav
|
||||
m["NavCertificationPhotos"] = 0 // admin indicator for certification photos
|
||||
m["SessionImpersonated"] = false
|
||||
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m["SessionImpersonated"] = session.Impersonated(r)
|
||||
|
||||
if user, err := session.CurrentUser(r); err == nil {
|
||||
m["LoggedIn"] = true
|
||||
m["CurrentUser"] = user
|
||||
|
@ -52,5 +57,13 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
|||
} else {
|
||||
log.Error("MergeUserVars: couldn't CountFriendRequests for %d: %s", user.ID, err)
|
||||
}
|
||||
|
||||
// Are we admin?
|
||||
if user.IsAdmin {
|
||||
// Any pending certification photos?
|
||||
m["NavCertificationPhotos"] = models.CountCertificationPhotosNeedingApproval()
|
||||
}
|
||||
}
|
||||
|
||||
m["NavAdminNotifications"] = m["NavCertificationPhotos"]
|
||||
}
|
||||
|
|
|
@ -111,6 +111,14 @@
|
|||
Log out
|
||||
</a>
|
||||
</li>
|
||||
{{if .SessionImpersonated}}
|
||||
<li>
|
||||
<a href="/admin/unimpersonate" class="has-text-danger">
|
||||
<span class="icon"><i class="fa fa-ghost"></i></span>
|
||||
<span>Unimpersonate</span>
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<a href="/account/delete">
|
||||
<span class="icon"><i class="fa fa-trash"></i></span>
|
||||
|
|
|
@ -34,6 +34,11 @@
|
|||
{{.User.Username}}
|
||||
{{end}}
|
||||
</h1>
|
||||
{{if ne .User.Status "active"}}
|
||||
<h2 class="subtitle">
|
||||
({{.User.Status}})
|
||||
</h2>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="column is-narrow">
|
||||
|
@ -305,6 +310,47 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Actions-->
|
||||
{{if .CurrentUser.IsAdmin}}
|
||||
<div class="card block">
|
||||
<header class="card-header has-background-danger">
|
||||
<p class="card-header-title has-text-light">
|
||||
<i class="fa fa-gavel pr-2"></i>
|
||||
Admin Actions
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="card-content">
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<a href="/admin/user-action?intent=impersonate&user_id={{.User.ID}}">
|
||||
<span class="icon"><i class="fa fa-ghost"></i></span>
|
||||
<span>Impersonate this user</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/admin/user-action?intent=ban&user_id={{.User.ID}}">
|
||||
<span class="icon"><i class="fa fa-ban"></i></span>
|
||||
<span>Ban/unban this user</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/admin/user-action?intent=promote&user_id={{.User.ID}}">
|
||||
<span class="icon"><i class="fa fa-gavel"></i></span>
|
||||
<span>Add/Remove admin rights</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/admin/user-action?intent=delete&user_id={{.User.ID}}">
|
||||
<span class="icon"><i class="fa fa-trash"></i></span>
|
||||
<span>Delete user account</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{{define "title"}}Admin - Certification Photos{{end}}
|
||||
{{define "content"}}
|
||||
{{$Root := .}}
|
||||
<div class="container">
|
||||
<section class="hero is-danger is-bold">
|
||||
<div class="hero-body">
|
||||
|
@ -12,11 +13,69 @@
|
|||
</section>
|
||||
|
||||
<div class="block p-4">
|
||||
<div class="block">
|
||||
There {{Pluralize64 .Pager.Total "is" "are"}} <strong>{{.Pager.Total}}</strong> Certification Photo{{Pluralize64 .Pager.Total}} needing approval.
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
{{if .Pager}}
|
||||
There {{Pluralize64 .Pager.Total "is" "are"}} <strong>{{.Pager.Total}}</strong> Certification Photo{{Pluralize64 .Pager.Total}}
|
||||
{{if eq .View "pending"}}
|
||||
needing approval.
|
||||
{{else}}
|
||||
at status "{{.View}}."
|
||||
{{end}}
|
||||
{{else if .FoundUser}}
|
||||
Found user <strong><a href="/u/{{.FoundUser.Username}}" class="has-text-dark">{{.FoundUser.Username}}</a></strong>
|
||||
(<a href="mailto:{{.FoundUser.Email}}">{{.FoundUser.Email}}</a>)
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="tabs is-toggle">
|
||||
<ul>
|
||||
<li{{if eq .View "pending"}} class="is-active"{{end}}>
|
||||
<a href="{{.Request.URL.Path}}?view=pending">Needing Approval</a>
|
||||
</li>
|
||||
<li{{if eq .View "approved"}} class="is-active"{{end}}>
|
||||
<a href="{{.Request.URL.Path}}?view=approved">Approved</a>
|
||||
</li>
|
||||
<li{{if eq .View "rejected"}} class="is-active"{{end}}>
|
||||
<a href="{{.Request.URL.Path}}?view=rejected">Rejected</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{$Root := .}}
|
||||
<div class="block">
|
||||
<form method="GET" action="{{.Request.URL.Path}}">
|
||||
<div class="field block">
|
||||
<div class="label" for="username">Search username or email:</div>
|
||||
<input type="text" class="input"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Press Enter to search">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{if .Pager}}
|
||||
<nav class="pagination" role="navigation" aria-label="pagination">
|
||||
<a class="pagination-previous{{if not .Pager.HasPrevious}} is-disabled{{end}}" title="Previous"
|
||||
href="{{.Request.URL.Path}}?view={{.View}}&page={{.Pager.Previous}}">Previous</a>
|
||||
<a class="pagination-next{{if not .Pager.HasNext}} is-disabled{{end}}" title="Next"
|
||||
href="{{.Request.URL.Path}}?view={{.View}}&page={{.Pager.Next}}">Next page</a>
|
||||
<ul class="pagination-list">
|
||||
{{range .Pager.Iter}}
|
||||
<li>
|
||||
<a class="pagination-link{{if .IsCurrent}} is-current{{end}}"
|
||||
aria-label="Page {{.Page}}"
|
||||
href="{{$Root.Request.URL.Path}}?view={{$Root.View}}&page={{.Page}}">
|
||||
{{.Page}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</nav>
|
||||
{{end}}
|
||||
|
||||
<div class="columns is-multiline">
|
||||
{{range .Photos}}
|
||||
<div class="column is-one-third">
|
||||
|
@ -57,21 +116,45 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="columns">
|
||||
<div class="column is-narrow">
|
||||
<strong>Status:
|
||||
</div>
|
||||
<div class="column">
|
||||
{{if eq .Status "pending"}}
|
||||
<strong class="has-text-warning">Pending Approval</strong>
|
||||
{{else if eq .Status "approved"}}
|
||||
<strong class="has-text-success">Approved</strong>
|
||||
{{else if eq .Status "rejected"}}
|
||||
<strong class="has-text-danger">Rejected</strong>
|
||||
{{else}}
|
||||
<strong>{{.Status}}</strong>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<textarea class="textarea" name="comment"
|
||||
cols="60" rows="2"
|
||||
placeholder="Admin comment (for rejection)"></textarea>
|
||||
placeholder="Admin comment (for rejection)">{{.AdminComment}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
{{if not (eq .Status "rejected")}}
|
||||
<button type="submit" name="verdict" value="reject" class="card-footer-item button is-danger">
|
||||
<span class="icon"><i class="fa fa-xmark"></i></span>
|
||||
<span>Reject</span>
|
||||
</button>
|
||||
{{end}}
|
||||
|
||||
{{if not (eq .Status "approved")}}
|
||||
<button type="submit" name="verdict" value="approve" class="card-footer-item button is-success">
|
||||
<span class="icon"><i class="fa fa-check"></i></span>
|
||||
<span>Approve</span>
|
||||
</button>
|
||||
{{end}}
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<a href="/admin/photo/certification">
|
||||
<span class="icon"><i class="fa fa-certificate"></i></span>
|
||||
Certification Photos
|
||||
{{if .NavCertificationPhotos}}<span class="tag is-danger ml-1">{{.NavCertificationPhotos}}</span>{{end}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
166
web/templates/admin/user_actions.html
Normal file
166
web/templates/admin/user_actions.html
Normal file
|
@ -0,0 +1,166 @@
|
|||
{{define "title"}}Compose a Message{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
<section class="hero is-info is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Admin Action
|
||||
</h1>
|
||||
<h2 class="subtitle">On user {{.User.Username}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="block p-4">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half">
|
||||
|
||||
<div class="card" style="width: 100%; max-width: 640px">
|
||||
<header class="card-header has-background-link">
|
||||
<p class="card-header-title has-text-light">
|
||||
{{if eq .Intent "impersonate"}}
|
||||
<span class="icon"><i class="fa fa-ghost"></i></span>
|
||||
Impersonate User
|
||||
{{else if eq .Intent "ban"}}
|
||||
<span class="icon"><i class="fa fa-ban"></i></span>
|
||||
Ban User
|
||||
{{else if eq .Intent "promote"}}
|
||||
<span class="icon"><i class="fa fa-gavel"></i></span>
|
||||
Promote User
|
||||
{{else if eq .Intent "delete"}}
|
||||
<span class="icon"><i class="fa fa-trash"></i></span>
|
||||
Delete User
|
||||
{{end}}
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
|
||||
<div class="media block">
|
||||
<div class="media-left">
|
||||
<figure class="image is-64x64">
|
||||
{{if .User.ProfilePhoto.ID}}
|
||||
<img src="{{PhotoURL .User.ProfilePhoto.CroppedFilename}}">
|
||||
{{else}}
|
||||
<img src="/static/img/shy.png">
|
||||
{{end}}
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">{{or .User.Name "(no name)"}}</p>
|
||||
<p class="subtitle is-6">
|
||||
<span class="icon"><i class="fa fa-user"></i></span>
|
||||
<a href="/u/{{.User.Username}}" target="_blank">{{.User.Username}}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="/admin/user-action" method="POST">
|
||||
{{InputCSRF}}
|
||||
<input type="hidden" name="intent" value="{{.Intent}}">
|
||||
<input type="hidden" name="user_id" value="{{.User.ID}}">
|
||||
|
||||
{{if eq .Intent "impersonate"}}
|
||||
<div class="block content">
|
||||
<h3>With great power...</h3>
|
||||
<p>
|
||||
By <strong>impersonating</strong> this user, you will be considered as "logged in"
|
||||
to their account and have access to their messages, profile, photos and settings.
|
||||
</p>
|
||||
<p>
|
||||
Please respect user privacy and only impersonate an account as needed to diagnose
|
||||
a customer support issue or similar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field has-text-centered">
|
||||
<button type="submit" class="button is-success">
|
||||
Log in as {{.User.Username}}
|
||||
</button>
|
||||
</div>
|
||||
{{else if eq .Intent "ban"}}
|
||||
<div class="block content">
|
||||
<p>
|
||||
This user is currently:
|
||||
{{if eq .User.Status "active"}}
|
||||
<strong class="has-text-success">Active (not banned)</strong>
|
||||
{{else if eq .User.Status "disabled"}}
|
||||
<strong class="has-text-warning">Disabled</strong>
|
||||
{{else if eq .User.Status "banned"}}
|
||||
<strong class="has-text-danger">Banned</strong>
|
||||
{{end}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Select a new status for them below:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field has-text-centered">
|
||||
<button type="submit" name="status" value="active" class="button is-success">
|
||||
Active
|
||||
</button>
|
||||
<button type="submit" name="status" value="banned" class="button is-danger">
|
||||
Banned
|
||||
</button>
|
||||
</div>
|
||||
{{else if eq .Intent "promote"}}
|
||||
<div class="block content">
|
||||
<p>
|
||||
This user is currently:
|
||||
{{if .User.IsAdmin}}
|
||||
<strong class="has-text-danger">Admin</strong>
|
||||
{{else}}
|
||||
<strong class="has-text-success">NOT Admin</strong>
|
||||
{{end}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Select a new status for them below:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field has-text-centered">
|
||||
<button type="submit" name="action" value="promote" class="button is-success">
|
||||
Make Admin
|
||||
</button>
|
||||
<button type="submit" name="action" value="demote" class="button is-danger">
|
||||
Remove Admin
|
||||
</button>
|
||||
</div>
|
||||
{{else if eq .Intent "delete"}}
|
||||
<div class="block content">
|
||||
<p>
|
||||
Click the button below to <strong>deep delete</strong> this user account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field has-text-centered">
|
||||
<button type="submit" class="button is-danger">
|
||||
Delete User Account
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.addEventListener("DOMContentLoaded", (event) => {
|
||||
let $file = document.querySelector("#file"),
|
||||
$fileName = document.querySelector("#fileName");
|
||||
|
||||
$file.addEventListener("change", function() {
|
||||
let file = this.files[0];
|
||||
$fileName.innerHTML = file.name;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
|
@ -116,7 +116,10 @@
|
|||
{{end}}
|
||||
</figure>
|
||||
</div>
|
||||
<div class="column">{{.CurrentUser.Username}}</div>
|
||||
<div class="column">
|
||||
{{.CurrentUser.Username}}
|
||||
{{if .NavAdminNotifications}}<span class="tag is-danger ml-1">{{.NavAdminNotifications}}</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
@ -127,7 +130,16 @@
|
|||
<a class="navbar-item" href="/photo/upload">Upload Photo</a>
|
||||
<a class="navbar-item" href="/settings">Settings</a>
|
||||
{{if .CurrentUser.IsAdmin}}
|
||||
<a class="navbar-item has-text-danger" href="/admin">Admin</a>
|
||||
<a class="navbar-item has-text-danger" href="/admin">
|
||||
Admin
|
||||
{{if .NavAdminNotifications}}<span class="tag is-danger ml-1">{{.NavAdminNotifications}}</span>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .SessionImpersonated}}
|
||||
<a href="/admin/unimpersonate" class="navbar-item has-text-danger">
|
||||
<span class="icon"><i class="fa fa-ghost"></i></span>
|
||||
<span>Unimpersonate</span>
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="navbar-item" href="/logout">Log out</a>
|
||||
</div>
|
||||
|
|
Reference in New Issue
Block a user