f0e69f78da
* Add an Admin Certification Photo workflow where we can request the user to upload a secondary form of ID (government issued photo ID showing their face and date of birth). * An admin rejection option can request secondary photo ID. * It sends a distinct e-mail to the user apart from the regular rejection email * It flags their cert photo as "Secondary Needed" forever: even if the user removes their cert photo and starts from scratch, it will immediately request secondary ID when uploading a new primary photo. * Secondary photos are deleted from the server on both Approve and Reject by the admin account, for user privacy. * If approved, a Secondary Approved=true boolean is stored in the database. This boolean is set to False if the user deletes their cert photo in the future.
254 lines
13 KiB
HTML
254 lines
13 KiB
HTML
{{define "title"}}Admin - Certification Photos{{end}}
|
|
{{define "content"}}
|
|
{{$Root := .}}
|
|
<div class="container">
|
|
<section class="hero is-danger is-bold">
|
|
<div class="hero-body">
|
|
<div class="container">
|
|
<h1 class="title">
|
|
Admin / Certification Photos
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="block p-4">
|
|
<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>
|
|
|
|
<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}}
|
|
{{SimplePager .Pager}}
|
|
{{end}}
|
|
|
|
<div class="columns is-multiline">
|
|
{{range .Photos}}
|
|
<div class="column is-one-third">
|
|
{{$User := $Root.UserMap.Get .UserID}}
|
|
<form action="{{$Root.Request.URL.Path}}" method="POST">
|
|
{{InputCSRF}}
|
|
<input type="hidden" name="user_id" value="{{$User.ID}}">
|
|
|
|
<div class="card" style="max-width: 512px">
|
|
<header class="card-header has-background-link">
|
|
<p class="card-header-title has-text-light">
|
|
<span class="icon"><i class="fa fa-user"></i></span>
|
|
<span>{{or $User.Username "[deleted]"}}</span>
|
|
</p>
|
|
</header>
|
|
{{if .Filename}}
|
|
<div class="card-image">
|
|
<figure class="image">
|
|
<img src="{{PhotoURL .Filename}}">
|
|
</figure>
|
|
</div>
|
|
{{end}}
|
|
<div class="card-content">
|
|
<div class="media block">
|
|
<div class="media-left">
|
|
<figure class="image is-48x48">
|
|
{{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">{{$User.NameOrUsername}}</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>
|
|
|
|
<div class="block">
|
|
<div class="columns">
|
|
<div class="column is-narrow">
|
|
<strong>Status:</strong>
|
|
</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}}
|
|
|
|
<!-- Secondary approved? -->
|
|
{{if .SecondaryVerified}}
|
|
<strong class="has-text-info">
|
|
<i class="fa fa-id-card ml-3 mr-1"></i> ID Verified
|
|
</strong>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Secondary photo ID attached? -->
|
|
{{if .SecondaryFilename}}
|
|
<div class="block">
|
|
<div class="mb-2">
|
|
<strong class="has-text-success">
|
|
<i class="fa fa-id-card"></i>
|
|
Secondary Photo ID Attached
|
|
</strong>
|
|
</div>
|
|
<img src="{{PhotoURL .SecondaryFilename}}">
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="block">
|
|
<strong><i class="fa fa-location-dot mr-1"></i> GeoIP Insights:</strong>
|
|
<div>
|
|
{{$Insights := $Root.InsightsMap.Get .IPAddress}}
|
|
{{if $Insights.IsZero}}
|
|
<span class="has-text-danger">No GeoIP insights available for this IP address!</span>
|
|
{{else}}
|
|
{{$Insights.Medium}}
|
|
{{end}}
|
|
</div>
|
|
<div>
|
|
IP: {{.IPAddress}}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Secondary ID was needed in the past for this user -->
|
|
{{if .SecondaryNeeded}}
|
|
<div class="block is-size-7 has-text-warning">
|
|
<i class="fa fa-id-card"></i> A secondary form of ID was requested from
|
|
this user once before. They will always be asked for a secondary ID if
|
|
they replace their cert photo in the future.
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="field">
|
|
<textarea class="textarea" name="comment"
|
|
cols="60" rows="2"
|
|
placeholder="Admin comment (for rejection)">{{.AdminComment}}</textarea>
|
|
|
|
<div class="select is-fullwidth">
|
|
<select class="common-reasons">
|
|
<optgroup label="Common Rejection Reasons">
|
|
<option value="Your certification pic should depict you holding onto a sheet of paper with your username, site name, and current date written on it.">
|
|
Didn't follow directions
|
|
</option>
|
|
<option value="The sheet of paper must also include the website name: nonshy">Website name not visible</option>
|
|
<option value="Please take a clearer picture that shows your arm and hand holding onto the sheet of paper">Unclear picture (hand not visible enough)</option>
|
|
<option value="This photo has been digitally altered, please take a new certification picture and upload it as it comes off your camera">Photoshopped or digitally altered</option>
|
|
<option value="You had a previous account on nonshy which was suspended and you are not welcome with a new account.">User was previously banned from nonshy</option>
|
|
<option value="This is not an acceptable certification photo.">Not acceptable</option>
|
|
</optgroup>
|
|
<optgroup label="Secondary Verification">
|
|
<option value="(secondary)">
|
|
User appears underage, ask for secondary ID
|
|
</option>
|
|
</optgroup>
|
|
<optgroup label="Other Actions">
|
|
<option value="(ignore)">(Silently reject this photo without sending an e-mail)</option>
|
|
</optgroup>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<a href="/admin/changelog?table_name=certification_photos&about_user_id={{$User.ID}}" class="button is-small has-text-warning">
|
|
<span class="icon"><i class="fa fa-clipboard-list"></i></span>
|
|
<span>Change Log</span>
|
|
</a>
|
|
</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>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
{{end}}
|
|
{{define "scripts"}}
|
|
<script>
|
|
window.addEventListener("DOMContentLoaded", (event) => {
|
|
document.querySelectorAll("select.common-reasons").forEach(elem => {
|
|
let textarea = elem.parentNode.parentNode.getElementsByTagName("textarea")[0],
|
|
approveButton = elem.parentNode.parentNode.parentNode.parentNode.querySelector("button.is-success");
|
|
|
|
// Grey out the Approve button if a rejection reason is filled out.
|
|
let setApproveState = () => {
|
|
if (textarea.value.length > 0) {
|
|
approveButton.disabled = "disabled";
|
|
} else {
|
|
approveButton.disabled = null;
|
|
}
|
|
};
|
|
|
|
textarea.addEventListener("change", setApproveState);
|
|
textarea.addEventListener("keyup", setApproveState);
|
|
elem.addEventListener("change", (e) => {
|
|
textarea.value = elem.value;
|
|
setApproveState();
|
|
});
|
|
})
|
|
});
|
|
</script>
|
|
{{end}}
|