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
|
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.
|
// OK. Log in the user's session.
|
||||||
session.LoginUser(w, r, user)
|
session.LoginUser(w, r, user)
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,12 @@ func Profile() http.HandlerFunc {
|
||||||
return
|
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{}{
|
vars := map[string]interface{}{
|
||||||
"User": user,
|
"User": user,
|
||||||
"IsFriend": models.FriendStatus(currentUser.ID, user.ID),
|
"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 {
|
func AdminCertification() http.HandlerFunc {
|
||||||
tmpl := templates.Must("admin/certification.html")
|
tmpl := templates.Must("admin/certification.html")
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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?
|
// Making a verdict?
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
var (
|
var (
|
||||||
|
@ -275,7 +309,7 @@ func AdminCertification() http.HandlerFunc {
|
||||||
Sort: "updated_at desc",
|
Sort: "updated_at desc",
|
||||||
}
|
}
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
photos, err := models.CertificationPhotosNeedingApproval(models.CertificationPhotoPending, pager)
|
photos, err := models.CertificationPhotosNeedingApproval(models.CertificationPhotoStatus(view), pager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Couldn't load certification photos from DB: %s", err)
|
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{}{
|
var vars = map[string]interface{}{
|
||||||
|
"View": view,
|
||||||
"Photos": photos,
|
"Photos": photos,
|
||||||
"UserMap": userMap,
|
"UserMap": userMap,
|
||||||
"Pager": pager,
|
"Pager": pager,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"git.kirsle.net/apps/gosocial/pkg/config"
|
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/photo"
|
"git.kirsle.net/apps/gosocial/pkg/controller/photo"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/log"
|
"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/session"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/templates"
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
)
|
)
|
||||||
|
@ -24,6 +25,19 @@ func LoginRequired(handler http.Handler) http.Handler {
|
||||||
return
|
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.
|
// Ping LastLoginAt for long lived sessions.
|
||||||
if time.Since(user.LastLoginAt) > config.LastLoginAtCooldown {
|
if time.Since(user.LastLoginAt) > config.LastLoginAtCooldown {
|
||||||
user.LastLoginAt = time.Now()
|
user.LastLoginAt = time.Now()
|
||||||
|
|
|
@ -63,6 +63,13 @@ func CertificationPhotosNeedingApproval(status CertificationPhotoStatus, pager *
|
||||||
return p, result.Error
|
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.
|
// Save photo.
|
||||||
func (p *CertificationPhoto) Save() error {
|
func (p *CertificationPhoto) Save() error {
|
||||||
result := DB.Save(p)
|
result := DB.Save(p)
|
||||||
|
|
|
@ -47,6 +47,7 @@ type UserStatus string
|
||||||
const (
|
const (
|
||||||
UserStatusActive = "active"
|
UserStatusActive = "active"
|
||||||
UserStatusDisabled = "disabled"
|
UserStatusDisabled = "disabled"
|
||||||
|
UserStatusBanned = "banned"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateUser. It is assumed username and email are correctly formatted.
|
// 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("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
||||||
mux.Handle("/friends", middleware.LoginRequired(friend.Friends()))
|
mux.Handle("/friends", middleware.LoginRequired(friend.Friends()))
|
||||||
mux.Handle("/friends/add", middleware.LoginRequired(friend.AddFriend()))
|
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.
|
// Certification Required. Pages that only full (verified) members can access.
|
||||||
mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery()))
|
mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery()))
|
||||||
|
@ -49,6 +50,7 @@ func New() http.Handler {
|
||||||
// Admin endpoints.
|
// Admin endpoints.
|
||||||
mux.Handle("/admin", middleware.AdminRequired(admin.Dashboard()))
|
mux.Handle("/admin", middleware.AdminRequired(admin.Dashboard()))
|
||||||
mux.Handle("/admin/photo/certification", middleware.AdminRequired(photo.AdminCertification()))
|
mux.Handle("/admin/photo/certification", middleware.AdminRequired(photo.AdminCertification()))
|
||||||
|
mux.Handle("/admin/user-action", middleware.AdminRequired(admin.UserActions()))
|
||||||
|
|
||||||
// JSON API endpoints.
|
// JSON API endpoints.
|
||||||
mux.HandleFunc("/v1/version", api.Version())
|
mux.HandleFunc("/v1/version", api.Version())
|
||||||
|
|
|
@ -21,6 +21,7 @@ type Session struct {
|
||||||
UserID uint64 `json:"userId,omitempty"`
|
UserID uint64 `json:"userId,omitempty"`
|
||||||
Flashes []string `json:"flashes,omitempty"`
|
Flashes []string `json:"flashes,omitempty"`
|
||||||
Errors []string `json:"errors,omitempty"`
|
Errors []string `json:"errors,omitempty"`
|
||||||
|
Impersonator uint64 `json:"impersonator,omitempty"`
|
||||||
LastSeen time.Time `json:"lastSeen"`
|
LastSeen time.Time `json:"lastSeen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +89,7 @@ func (s *Session) Save(w http.ResponseWriter) {
|
||||||
Name: config.SessionCookieName,
|
Name: config.SessionCookieName,
|
||||||
Value: s.UUID,
|
Value: s.UUID,
|
||||||
MaxAge: config.SessionCookieMaxAge,
|
MaxAge: config.SessionCookieMaxAge,
|
||||||
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
}
|
}
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
|
@ -144,6 +146,7 @@ func LoginUser(w http.ResponseWriter, r *http.Request, u *models.User) error {
|
||||||
sess := Get(r)
|
sess := Get(r)
|
||||||
sess.LoggedIn = true
|
sess.LoggedIn = true
|
||||||
sess.UserID = u.ID
|
sess.UserID = u.ID
|
||||||
|
sess.Impersonator = 0
|
||||||
sess.Save(w)
|
sess.Save(w)
|
||||||
|
|
||||||
// Ping the user's last login time.
|
// 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()
|
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.
|
// LogoutUser signs a user out.
|
||||||
func LogoutUser(w http.ResponseWriter, r *http.Request) {
|
func LogoutUser(w http.ResponseWriter, r *http.Request) {
|
||||||
sess := Get(r)
|
sess := Get(r)
|
||||||
|
|
|
@ -30,11 +30,16 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
||||||
m["CurrentUser"] = nil
|
m["CurrentUser"] = nil
|
||||||
m["NavUnreadMessages"] = 0
|
m["NavUnreadMessages"] = 0
|
||||||
m["NavFriendRequests"] = 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 {
|
if r == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m["SessionImpersonated"] = session.Impersonated(r)
|
||||||
|
|
||||||
if user, err := session.CurrentUser(r); err == nil {
|
if user, err := session.CurrentUser(r); err == nil {
|
||||||
m["LoggedIn"] = true
|
m["LoggedIn"] = true
|
||||||
m["CurrentUser"] = user
|
m["CurrentUser"] = user
|
||||||
|
@ -52,5 +57,13 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
||||||
} else {
|
} else {
|
||||||
log.Error("MergeUserVars: couldn't CountFriendRequests for %d: %s", user.ID, err)
|
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
|
Log out
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<a href="/account/delete">
|
<a href="/account/delete">
|
||||||
<span class="icon"><i class="fa fa-trash"></i></span>
|
<span class="icon"><i class="fa fa-trash"></i></span>
|
||||||
|
|
|
@ -34,6 +34,11 @@
|
||||||
{{.User.Username}}
|
{{.User.Username}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</h1>
|
</h1>
|
||||||
|
{{if ne .User.Status "active"}}
|
||||||
|
<h2 class="subtitle">
|
||||||
|
({{.User.Status}})
|
||||||
|
</h2>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
|
@ -305,6 +310,47 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{{define "title"}}Admin - Certification Photos{{end}}
|
{{define "title"}}Admin - Certification Photos{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
|
{{$Root := .}}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section class="hero is-danger is-bold">
|
<section class="hero is-danger is-bold">
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
|
@ -12,11 +13,69 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="block p-4">
|
<div class="block p-4">
|
||||||
<div class="block">
|
<div class="columns">
|
||||||
There {{Pluralize64 .Pager.Total "is" "are"}} <strong>{{.Pager.Total}}</strong> Certification Photo{{Pluralize64 .Pager.Total}} needing approval.
|
<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>
|
</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">
|
<div class="columns is-multiline">
|
||||||
{{range .Photos}}
|
{{range .Photos}}
|
||||||
<div class="column is-one-third">
|
<div class="column is-one-third">
|
||||||
|
@ -57,21 +116,45 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="field">
|
||||||
<textarea class="textarea" name="comment"
|
<textarea class="textarea" name="comment"
|
||||||
cols="60" rows="2"
|
cols="60" rows="2"
|
||||||
placeholder="Admin comment (for rejection)"></textarea>
|
placeholder="Admin comment (for rejection)">{{.AdminComment}}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="card-footer">
|
<footer class="card-footer">
|
||||||
|
{{if not (eq .Status "rejected")}}
|
||||||
<button type="submit" name="verdict" value="reject" class="card-footer-item button is-danger">
|
<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 class="icon"><i class="fa fa-xmark"></i></span>
|
||||||
<span>Reject</span>
|
<span>Reject</span>
|
||||||
</button>
|
</button>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if not (eq .Status "approved")}}
|
||||||
<button type="submit" name="verdict" value="approve" class="card-footer-item button is-success">
|
<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 class="icon"><i class="fa fa-check"></i></span>
|
||||||
<span>Approve</span>
|
<span>Approve</span>
|
||||||
</button>
|
</button>
|
||||||
|
{{end}}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
<a href="/admin/photo/certification">
|
<a href="/admin/photo/certification">
|
||||||
<span class="icon"><i class="fa fa-certificate"></i></span>
|
<span class="icon"><i class="fa fa-certificate"></i></span>
|
||||||
Certification Photos
|
Certification Photos
|
||||||
|
{{if .NavCertificationPhotos}}<span class="tag is-danger ml-1">{{.NavCertificationPhotos}}</span>{{end}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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}}
|
{{end}}
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@ -127,7 +130,16 @@
|
||||||
<a class="navbar-item" href="/photo/upload">Upload Photo</a>
|
<a class="navbar-item" href="/photo/upload">Upload Photo</a>
|
||||||
<a class="navbar-item" href="/settings">Settings</a>
|
<a class="navbar-item" href="/settings">Settings</a>
|
||||||
{{if .CurrentUser.IsAdmin}}
|
{{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}}
|
{{end}}
|
||||||
<a class="navbar-item" href="/logout">Log out</a>
|
<a class="navbar-item" href="/logout">Log out</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
Reference in New Issue
Block a user