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:
Noah Petherbridge 2022-12-24 23:00:59 -08:00
parent 76963ca514
commit 345285d7a3
8 changed files with 225 additions and 25 deletions

View File

@ -80,7 +80,7 @@ func Feedback() http.HandlerFunc {
if err != nil {
session.FlashError(w, r, "Couldn't get reporting user ID %d: %s", fb.UserID, err)
} 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)
} else {
// Redirect to the thread.

View File

@ -17,6 +17,7 @@ func UserActions() http.HandlerFunc {
var (
intent = r.FormValue("intent")
confirm = r.Method == http.MethodPost
reason = r.FormValue("reason") // for impersonation
userId uint64
)
@ -47,7 +48,7 @@ func UserActions() http.HandlerFunc {
switch intent {
case "impersonate":
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)
} else {
session.Flash(w, r, "You are now impersonating %s", user.Username)

View File

@ -108,6 +108,7 @@ func UserPhotos() http.HandlerFunc {
var vars = map[string]interface{}{
"IsOwnPhotos": currentUser.ID == user.ID,
"IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics?
"AreWeGrantedPrivate": isGrantee, // have THEY granted US private photo access.
"User": user,
"Photos": photos,
"PhotoCount": models.CountPhotos(user.ID),

View File

@ -11,6 +11,7 @@ import (
"code.nonshy.com/nonshy/website/pkg/config"
"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/redis"
"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.
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 {
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.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()
}

View File

@ -11,12 +11,112 @@
<div class="block p-4">
<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="card block">
<header class="card-header has-background-link">
<p class="card-header-title has-text-light">
<span class="icon"><i class="fa fa-gavel"></i></span>
Admin Dashboard
<i class="fa fa-gavel mr-2"></i>
Admin Menu
</p>
</header>
@ -24,34 +124,28 @@
<ul class="menu-list">
<li>
<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
{{if .NavCertificationPhotos}}<span class="tag is-danger ml-1">{{.NavCertificationPhotos}}</span>{{end}}
</a>
</li>
<li>
<a href="/admin/feedback">
<span class="icon"><i class="fa fa-message"></i></span>
<i class="fa fa-message mr-2"></i>
Feedback &amp; User Reports
{{if .NavAdminFeedback}}<span class="tag is-danger ml-1">{{.NavAdminFeedback}}</span>{{end}}
</a>
</li>
<li>
<a href="/photo/gallery?admin_view=true">
<i class="fa fa-image mr-2"></i>
Gallery: Admin View
</a>
</li>
</ul>
</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>

View File

@ -65,8 +65,37 @@
Please respect user privacy and only impersonate an account as needed to diagnose
a customer support issue or similar.
</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>
<textarea class="textarea mb-4"
cols="80" rows="4"
name="reason"
placeholder="Reason"
required></textarea>
<div class="field has-text-centered">
<button type="submit" class="button is-success">
Log in as {{.User.Username}}

View 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}}

View File

@ -285,18 +285,26 @@
<span>Manage who can see <strong>my</strong> private photos</span>
</a>
</div>
{{else if and (not .IsSiteGallery) (not .IsMyPrivateUnlockedFor)}}
{{else if not .IsSiteGallery}}
<div class="block">
{{if not .IsMyPrivateUnlockedFor}}
<a href="/photo/private/share?to={{.User.Username}}" class="has-text-private">
<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>
</a>
</div>
{{else if and (not .IsSiteGallery) .IsMyPrivateUnlockedFor}}
<div class="block">
{{else}}
<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>
<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>
{{end}}