Add Admin Guidelines to dashboard
* A reason must be entered to impersonate a user, and it triggers a Report and email notification to the admin. * User gallery pages will show at the top whether the user had granted you access to their private photos.
This commit is contained in:
parent
76963ca514
commit
345285d7a3
|
@ -80,7 +80,7 @@ func Feedback() http.HandlerFunc {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Couldn't get reporting user ID %d: %s", fb.UserID, err)
|
session.FlashError(w, r, "Couldn't get reporting user ID %d: %s", fb.UserID, err)
|
||||||
} else {
|
} else {
|
||||||
if err := session.ImpersonateUser(w, r, user, currentUser); err != nil {
|
if err := session.ImpersonateUser(w, r, user, currentUser, "Clicked from user reported Message via admin dashboard"); err != nil {
|
||||||
session.FlashError(w, r, "Couldn't impersonate user: %s", err)
|
session.FlashError(w, r, "Couldn't impersonate user: %s", err)
|
||||||
} else {
|
} else {
|
||||||
// Redirect to the thread.
|
// Redirect to the thread.
|
||||||
|
|
|
@ -17,6 +17,7 @@ func UserActions() http.HandlerFunc {
|
||||||
var (
|
var (
|
||||||
intent = r.FormValue("intent")
|
intent = r.FormValue("intent")
|
||||||
confirm = r.Method == http.MethodPost
|
confirm = r.Method == http.MethodPost
|
||||||
|
reason = r.FormValue("reason") // for impersonation
|
||||||
userId uint64
|
userId uint64
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ func UserActions() http.HandlerFunc {
|
||||||
switch intent {
|
switch intent {
|
||||||
case "impersonate":
|
case "impersonate":
|
||||||
if confirm {
|
if confirm {
|
||||||
if err := session.ImpersonateUser(w, r, user, currentUser); err != nil {
|
if err := session.ImpersonateUser(w, r, user, currentUser, reason); err != nil {
|
||||||
session.FlashError(w, r, "Failed to impersonate user: %s", err)
|
session.FlashError(w, r, "Failed to impersonate user: %s", err)
|
||||||
} else {
|
} else {
|
||||||
session.Flash(w, r, "You are now impersonating %s", user.Username)
|
session.Flash(w, r, "You are now impersonating %s", user.Username)
|
||||||
|
|
|
@ -108,6 +108,7 @@ func UserPhotos() http.HandlerFunc {
|
||||||
var vars = map[string]interface{}{
|
var vars = map[string]interface{}{
|
||||||
"IsOwnPhotos": currentUser.ID == user.ID,
|
"IsOwnPhotos": currentUser.ID == user.ID,
|
||||||
"IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics?
|
"IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics?
|
||||||
|
"AreWeGrantedPrivate": isGrantee, // have THEY granted US private photo access.
|
||||||
"User": user,
|
"User": user,
|
||||||
"Photos": photos,
|
"Photos": photos,
|
||||||
"PhotoCount": models.CountPhotos(user.ID),
|
"PhotoCount": models.CountPhotos(user.ID),
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"code.nonshy.com/nonshy/website/pkg/config"
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||||||
"code.nonshy.com/nonshy/website/pkg/log"
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/mail"
|
||||||
"code.nonshy.com/nonshy/website/pkg/models"
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
"code.nonshy.com/nonshy/website/pkg/redis"
|
"code.nonshy.com/nonshy/website/pkg/redis"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -175,7 +176,7 @@ func LoginUser(w http.ResponseWriter, r *http.Request, u *models.User) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImpersonateUser assumes the role of the user impersonated by an admin uid.
|
// 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 {
|
func ImpersonateUser(w http.ResponseWriter, r *http.Request, u *models.User, impersonator *models.User, reason string) error {
|
||||||
if u == nil || u.ID == 0 {
|
if u == nil || u.ID == 0 {
|
||||||
return errors.New("not a valid user account")
|
return errors.New("not a valid user account")
|
||||||
}
|
}
|
||||||
|
@ -189,6 +190,40 @@ func ImpersonateUser(w http.ResponseWriter, r *http.Request, u *models.User, imp
|
||||||
sess.Impersonator = impersonator.ID
|
sess.Impersonator = impersonator.ID
|
||||||
sess.Save(w)
|
sess.Save(w)
|
||||||
|
|
||||||
|
// Issue an admin notification that this has happened.
|
||||||
|
// NOTE: not DRY compared to contact.go
|
||||||
|
fb := &models.Feedback{
|
||||||
|
Intent: "report",
|
||||||
|
Subject: "'Impersonate user' has been used",
|
||||||
|
TableName: "users",
|
||||||
|
TableID: impersonator.ID,
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"The admin user **%s** (id:%d) has impersonated user **%s** (id:%d)\n\n"+
|
||||||
|
"The reason they have given:\n\n%s",
|
||||||
|
impersonator.Username, impersonator.ID,
|
||||||
|
u.Username, u.ID, reason,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.CreateFeedback(fb); err != nil {
|
||||||
|
FlashError(w, r, "Couldn't create admin notification: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email the admins.
|
||||||
|
if err := mail.Send(mail.Message{
|
||||||
|
To: config.Current.AdminEmail,
|
||||||
|
Subject: "Admin 'user impersonate' has been used",
|
||||||
|
Template: "email/admin_impersonate.html",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Impersonator": impersonator,
|
||||||
|
"User": u,
|
||||||
|
"Reason": reason,
|
||||||
|
"AdminURL": config.Current.BaseURL + "/admin/feedback",
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
log.Error("/contact page: couldn't send email: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return u.Save()
|
return u.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,12 +11,112 @@
|
||||||
|
|
||||||
<div class="block p-4">
|
<div class="block p-4">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header has-background-warning">
|
||||||
|
<p class="card-header-title has-text-dark-dark">
|
||||||
|
<i class="fa fa-book mr-2"></i>
|
||||||
|
Admin Guidelines <span class="tag is-success ml-3">NEW!</span>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="card-content content">
|
||||||
|
<h2>Respect the privacy of our users</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
We do not snoop on our users' Direct Messages unless they <strong>report</strong> a conversation
|
||||||
|
for us to check out. The only way to access DMs is to <a href="#impersonate">impersonate</a> a
|
||||||
|
user, which can't be done in secret.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
We treat the Certification Photos as sensitive information. Go there only when a certification
|
||||||
|
photo is pending approval (red notification badges will guide the way). Do not download or leak
|
||||||
|
these images; be respectful.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>What we moderate</h2>
|
||||||
|
|
||||||
|
Admin users are only expected to help moderate the following areas of the site:
|
||||||
|
|
||||||
|
<h3>1. User profile photos</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Every picture uploaded to a user's profile page can be seen by admin users. The
|
||||||
|
<a href="/photo/gallery?admin_view=true">admin gallery view</a> can find <strong>all</strong>
|
||||||
|
user photos, whether private or friends-only, whether opted-in for the Site Gallery or not.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Be careful</strong> not to "Like" or comment on a picture if the user marked it
|
||||||
|
"Friends only" or "Private" and they wouldn't expect you to have been able to see it. "Like"
|
||||||
|
and "Comment" buttons are hidden in the admin gallery view to reduce accidents but they are
|
||||||
|
functional on the user's own gallery page.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>2. The Forums</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Keep up with the <a href="/forum/newest">newest</a> forum posts and generally make sure
|
||||||
|
people aren't fighting or uploading inappropriate photos to one of the few photo boards.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>3. Reported DMs only</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If a user reports a Direct Message conversation
|
||||||
|
they're having, a link to view that chat thread will be available from the report.
|
||||||
|
This will <a href="#impersonate">impersonate</a> the reporter and will be logged
|
||||||
|
- see "Impersonating users," below.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
DMs are text-based only, so users won't be sending any image attachments that need
|
||||||
|
moderating and their privacy is to be respected. A user may report a problematic
|
||||||
|
conversation for us to take a look at.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Impersonating users</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
From a user's profile page you can "impersonate," or log in as them. You should almost
|
||||||
|
never need to do this, and only to diagnose a reported issue from their account or
|
||||||
|
something like that.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You will need to write a <strong>reason</strong> for impersonating a user. The event is
|
||||||
|
logged and will be e-mailed to the admin team along with your reason. The admin team is
|
||||||
|
alerted any time an Impersonate action is performed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Note: when you impersonate, their "Last logged in at" time is not updated by your actions.
|
||||||
|
So if a user were last seen a month ago, they will still appear last seen a month ago.
|
||||||
|
But other interactions you make as their account (e.g. marking notifications read, reading
|
||||||
|
their unread DMs) will work and may be noticed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="card block">
|
<div class="card block">
|
||||||
<header class="card-header has-background-link">
|
<header class="card-header has-background-link">
|
||||||
<p class="card-header-title has-text-light">
|
<p class="card-header-title has-text-light">
|
||||||
<span class="icon"><i class="fa fa-gavel"></i></span>
|
<i class="fa fa-gavel mr-2"></i>
|
||||||
Admin Dashboard
|
Admin Menu
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@ -24,34 +124,28 @@
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
<a href="/admin/photo/certification">
|
<a href="/admin/photo/certification">
|
||||||
<span class="icon"><i class="fa fa-certificate"></i></span>
|
<i class="fa fa-certificate mr-2"></i>
|
||||||
Certification Photos
|
Certification Photos
|
||||||
{{if .NavCertificationPhotos}}<span class="tag is-danger ml-1">{{.NavCertificationPhotos}}</span>{{end}}
|
{{if .NavCertificationPhotos}}<span class="tag is-danger ml-1">{{.NavCertificationPhotos}}</span>{{end}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/admin/feedback">
|
<a href="/admin/feedback">
|
||||||
<span class="icon"><i class="fa fa-message"></i></span>
|
<i class="fa fa-message mr-2"></i>
|
||||||
Feedback & User Reports
|
Feedback & User Reports
|
||||||
{{if .NavAdminFeedback}}<span class="tag is-danger ml-1">{{.NavAdminFeedback}}</span>{{end}}
|
{{if .NavAdminFeedback}}<span class="tag is-danger ml-1">{{.NavAdminFeedback}}</span>{{end}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/photo/gallery?admin_view=true">
|
||||||
|
<i class="fa fa-image mr-2"></i>
|
||||||
|
Gallery: Admin View
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column">
|
|
||||||
<div class="card">
|
|
||||||
<header class="card-header has-background-warning">
|
|
||||||
<p class="card-header-title">Notifications</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="card-content">
|
|
||||||
TBD.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -65,8 +65,37 @@
|
||||||
Please respect user privacy and only impersonate an account as needed to diagnose
|
Please respect user privacy and only impersonate an account as needed to diagnose
|
||||||
a customer support issue or similar.
|
a customer support issue or similar.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="has-text-danger">
|
||||||
|
This event is logged and will be noticed.
|
||||||
|
</strong>
|
||||||
|
Write an explanation below why you are impersonating this user. It will
|
||||||
|
be e-mailed to the admin mailing list and trigger an admin notification
|
||||||
|
and be logged as a <a href="/admin/feedback?intent=report">Report</a> to
|
||||||
|
the admin dashboard. Reports can be acknowledged, but not deleted.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Good reasons may include:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
I need to diagnose a bug report given by one of our users
|
||||||
|
(briefly describe what the bug is; e.g. user saw a database error
|
||||||
|
at the top of a page).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
A user has reported a Direct Message conversation and I need to
|
||||||
|
take a look at the context. (There is no other way to read user DMs)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<textarea class="textarea mb-4"
|
||||||
|
cols="80" rows="4"
|
||||||
|
name="reason"
|
||||||
|
placeholder="Reason"
|
||||||
|
required></textarea>
|
||||||
|
|
||||||
<div class="field has-text-centered">
|
<div class="field has-text-centered">
|
||||||
<button type="submit" class="button is-success">
|
<button type="submit" class="button is-success">
|
||||||
Log in as {{.User.Username}}
|
Log in as {{.User.Username}}
|
||||||
|
|
32
web/templates/email/admin_impersonate.html
Normal file
32
web/templates/email/admin_impersonate.html
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{{define "content"}}
|
||||||
|
<html>
|
||||||
|
<body bakground="#ffffff" color="#000000" link="#0000FF" vlink="#990099" alink="#FF0000">
|
||||||
|
<basefont face="Arial,Helvetica,sans-serif" size="3" color="#000000"></basefont>
|
||||||
|
|
||||||
|
<h1>User impersonate</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Dear website administrators,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
An admin user <strong>{{.Data.Impersonator.Username}}</strong> has used the "Impersonate user"
|
||||||
|
feature to become user <strong>{{.Data.User.Username}}</strong>. The reason they have listed
|
||||||
|
for doing so is as follows:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{.Data.Reason}}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To view this message on the admin dashboard, please visit:
|
||||||
|
<a href="{{.Data.AdminURL}}">{{.Data.AdminURL}}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is an automated e-mail; do not reply to this message.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -285,18 +285,26 @@
|
||||||
<span>Manage who can see <strong>my</strong> private photos</span>
|
<span>Manage who can see <strong>my</strong> private photos</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{else if and (not .IsSiteGallery) (not .IsMyPrivateUnlockedFor)}}
|
{{else if not .IsSiteGallery}}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
|
{{if not .IsMyPrivateUnlockedFor}}
|
||||||
<a href="/photo/private/share?to={{.User.Username}}" class="has-text-private">
|
<a href="/photo/private/share?to={{.User.Username}}" class="has-text-private">
|
||||||
<span class="icon"><i class="fa fa-unlock"></i></span>
|
<span class="icon"><i class="fa fa-unlock"></i></span>
|
||||||
<span>Grant <strong>{{.User.Username}}</strong> access to see <strong>my</strong> private photos</span>
|
<span>Grant <strong>{{.User.Username}}</strong> access to see <strong>my</strong> private photos</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
{{else}}
|
||||||
{{else if and (not .IsSiteGallery) .IsMyPrivateUnlockedFor}}
|
|
||||||
<div class="block">
|
|
||||||
<span class="icon"><i class="fa fa-unlock has-text-private"></i></span>
|
<span class="icon"><i class="fa fa-unlock has-text-private"></i></span>
|
||||||
<span>You had granted <strong>{{.User.Username}}</strong> access to see <strong>your</strong> private photos.</span>
|
<span>You had granted <strong>{{.User.Username}}</strong> access to see <strong>your</strong> private photos.</span>
|
||||||
<a href="/photo/private">Manage that here.</a>
|
<a href="/photo/private">Manage that here.</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .AreWeGrantedPrivate}}
|
||||||
|
<div class="block mt-0">
|
||||||
|
<span class="icon"><i class="fa fa-eye has-text-private"></i></span>
|
||||||
|
<strong>{{.User.Username}}</strong> has <span class="has-text-private">granted</span> you
|
||||||
|
access to see their private photos.
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user