Certification: Secondary Photo ID Workflow
* 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.
This commit is contained in:
parent
af76c251c6
commit
f0e69f78da
|
@ -72,6 +72,8 @@ func Certification() http.HandlerFunc {
|
|||
if r.Method == http.MethodPost {
|
||||
// Are they deleting their photo?
|
||||
if r.PostFormValue("delete") == "true" {
|
||||
|
||||
// Primary cert photo
|
||||
if cert.Filename != "" {
|
||||
if err := photo.Delete(cert.Filename); err != nil {
|
||||
log.Error("Failed to delete old cert photo for %s (%s): %s", currentUser.Username, cert.Filename, err)
|
||||
|
@ -79,8 +81,17 @@ func Certification() http.HandlerFunc {
|
|||
cert.Filename = ""
|
||||
}
|
||||
|
||||
// Secondary cert photo
|
||||
if cert.SecondaryFilename != "" {
|
||||
if err := photo.Delete(cert.SecondaryFilename); err != nil {
|
||||
log.Error("Failed to delete old cert photo for %s (%s): %s", currentUser.Username, cert.SecondaryFilename, err)
|
||||
}
|
||||
cert.SecondaryFilename = ""
|
||||
}
|
||||
|
||||
cert.Status = models.CertificationPhotoNeeded
|
||||
cert.AdminComment = ""
|
||||
cert.SecondaryVerified = false
|
||||
cert.Save()
|
||||
|
||||
// Removing your photo = not certified again.
|
||||
|
@ -102,6 +113,9 @@ func Certification() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Is it their secondary form of ID being uploaded?
|
||||
isSecondary := r.PostFormValue("secondary") == "true"
|
||||
|
||||
// Get the uploaded file.
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
|
@ -125,17 +139,38 @@ func Certification() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// Are they replacing their old photo?
|
||||
if cert.Filename != "" {
|
||||
if cert.Filename != "" && !isSecondary {
|
||||
if err := photo.Delete(cert.Filename); err != nil {
|
||||
log.Error("Failed to delete old cert photo for %s (%s): %s", currentUser.Username, cert.Filename, err)
|
||||
}
|
||||
} else if isSecondary && cert.SecondaryFilename != "" {
|
||||
if err := photo.Delete(cert.SecondaryFilename); err != nil {
|
||||
log.Error("Failed to delete old cert photo for %s (%s): %s", currentUser.Username, cert.SecondaryFilename, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update their certification photo.
|
||||
cert.Status = models.CertificationPhotoPending
|
||||
cert.Filename = filename
|
||||
if isSecondary {
|
||||
cert.SecondaryFilename = filename
|
||||
cert.SecondaryNeeded = true
|
||||
cert.SecondaryVerified = false
|
||||
} else {
|
||||
cert.Filename = filename
|
||||
}
|
||||
cert.AdminComment = ""
|
||||
cert.IPAddress = utility.IPAddress(r)
|
||||
|
||||
// Secondary ID workflow: if the user
|
||||
// 1. Uploads a regular cert photo
|
||||
// 2. An admin marks secondary ID as needed
|
||||
// 3. They remove everything and reupload a new cert photo, without a secondary ID attached
|
||||
// Then we don't e-mail the admin for approval yet, and move straight to Secondary ID Requested
|
||||
// for the user to upload their secondary ID now.
|
||||
if cert.Status == models.CertificationPhotoPending && cert.SecondaryNeeded && cert.SecondaryFilename == "" {
|
||||
cert.Status = models.CertificationPhotoSecondary
|
||||
}
|
||||
|
||||
if err := cert.Save(); err != nil {
|
||||
session.FlashError(w, r, "Error saving your CertificationPhoto: %s", err)
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
|
@ -154,16 +189,18 @@ func Certification() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// Notify the admin email to check out this photo.
|
||||
if err := mail.Send(mail.Message{
|
||||
To: config.Current.AdminEmail,
|
||||
Subject: "New Certification Photo Needs Approval",
|
||||
Template: "email/certification_admin.html",
|
||||
Data: map[string]interface{}{
|
||||
"User": currentUser,
|
||||
"URL": config.Current.BaseURL + "/admin/photo/certification",
|
||||
},
|
||||
}); err != nil {
|
||||
log.Error("Certification: failed to notify admins of pending photo: %s", err)
|
||||
if cert.Status == models.CertificationPhotoPending {
|
||||
if err := mail.Send(mail.Message{
|
||||
To: config.Current.AdminEmail,
|
||||
Subject: "New Certification Photo Needs Approval",
|
||||
Template: "email/certification_admin.html",
|
||||
Data: map[string]interface{}{
|
||||
"User": currentUser,
|
||||
"URL": config.Current.BaseURL + "/admin/photo/certification",
|
||||
},
|
||||
}); err != nil {
|
||||
log.Error("Certification: failed to notify admins of pending photo: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Log the change. Note the original IP and GeoIP insights - we once saw a spammer upload
|
||||
|
@ -319,9 +356,20 @@ func AdminCertification() http.HandlerFunc {
|
|||
} else {
|
||||
cert.Status = models.CertificationPhotoRejected
|
||||
cert.AdminComment = comment
|
||||
if comment == "(ignore)" {
|
||||
if comment == "(ignore)" || comment == "(secondary)" {
|
||||
cert.AdminComment = ""
|
||||
}
|
||||
|
||||
// With a secondary photo ID? Remove the photo ID immediately.
|
||||
if cert.SecondaryFilename != "" {
|
||||
// Delete it immediately.
|
||||
if err := photo.Delete(cert.SecondaryFilename); err != nil {
|
||||
session.FlashError(w, r, "Failed to delete old secondary ID cert photo for %s (%s): %s", currentUser.Username, cert.SecondaryFilename, err)
|
||||
}
|
||||
cert.SecondaryFilename = ""
|
||||
cert.SecondaryVerified = false
|
||||
}
|
||||
|
||||
if err := cert.Save(); err != nil {
|
||||
session.FlashError(w, r, "Failed to save CertificationPhoto: %s", err)
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
|
@ -347,6 +395,46 @@ func AdminCertification() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Secondary verification required: the user will be asked to upload a blacked-out
|
||||
// photo ID to be certified again.
|
||||
if comment == "(secondary)" {
|
||||
cert.Status = models.CertificationPhotoSecondary
|
||||
cert.SecondaryNeeded = true
|
||||
cert.SecondaryVerified = false
|
||||
if err := cert.Save(); err != nil {
|
||||
log.Error("Error saving cert photo: %s", err)
|
||||
}
|
||||
|
||||
// Notify the user about this rejection.
|
||||
notif := &models.Notification{
|
||||
UserID: user.ID,
|
||||
AboutUser: *user,
|
||||
Type: models.NotificationCertSecondary,
|
||||
Message: "A secondary form of photo ID is requested. Please [click here](/photo/certification) to learn more.",
|
||||
}
|
||||
if err := models.CreateNotification(notif); err != nil {
|
||||
log.Error("Couldn't create rejection notification: %s", err)
|
||||
}
|
||||
|
||||
// Notify the user via email.
|
||||
if err := mail.Send(mail.Message{
|
||||
To: user.Email,
|
||||
Subject: "Regarding your nonshy certification photo",
|
||||
Template: "email/certification_secondary.html",
|
||||
Data: map[string]interface{}{
|
||||
"Username": user.Username,
|
||||
"AdminComment": comment,
|
||||
"URL": config.Current.BaseURL + "/photo/certification",
|
||||
},
|
||||
}); err != nil {
|
||||
session.FlashError(w, r, "Note: failed to email user about the rejection: %s", err)
|
||||
}
|
||||
|
||||
session.Flash(w, r, "The user will be asked to provide a secondary form of ID.")
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify the user about this rejection.
|
||||
notif := &models.Notification{
|
||||
UserID: user.ID,
|
||||
|
@ -377,6 +465,17 @@ func AdminCertification() http.HandlerFunc {
|
|||
case "approve":
|
||||
cert.Status = models.CertificationPhotoApproved
|
||||
cert.AdminComment = ""
|
||||
|
||||
// With a secondary photo ID?
|
||||
if cert.SecondaryFilename != "" {
|
||||
// Delete it immediately.
|
||||
if err := photo.Delete(cert.SecondaryFilename); err != nil {
|
||||
session.FlashError(w, r, "Failed to delete old secondary ID cert photo for %s (%s): %s", currentUser.Username, cert.SecondaryFilename, err)
|
||||
}
|
||||
cert.SecondaryFilename = ""
|
||||
cert.SecondaryVerified = true
|
||||
}
|
||||
|
||||
if err := cert.Save(); err != nil {
|
||||
session.FlashError(w, r, "Failed to save CertificationPhoto: %s", err)
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
|
|
|
@ -8,15 +8,18 @@ import (
|
|||
|
||||
// CertificationPhoto table.
|
||||
type CertificationPhoto struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
UserID uint64 `gorm:"uniqueIndex"`
|
||||
Filename string
|
||||
Filesize int64
|
||||
Status CertificationPhotoStatus
|
||||
AdminComment string
|
||||
IPAddress string // the IP they uploaded the photo from
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
UserID uint64 `gorm:"uniqueIndex"`
|
||||
Filename string
|
||||
Filesize int64
|
||||
Status CertificationPhotoStatus
|
||||
AdminComment string
|
||||
SecondaryNeeded bool // a secondary form of ID has been requested
|
||||
SecondaryFilename string // photo ID upload
|
||||
SecondaryVerified bool // mark true when ID checked so original can be deleted
|
||||
IPAddress string // the IP they uploaded the photo from
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CertificationPhotoStatus string
|
||||
|
@ -26,6 +29,10 @@ const (
|
|||
CertificationPhotoPending CertificationPhotoStatus = "pending"
|
||||
CertificationPhotoApproved CertificationPhotoStatus = "approved"
|
||||
CertificationPhotoRejected CertificationPhotoStatus = "rejected"
|
||||
|
||||
// If a photo is pending approval but the admin wants to engage the
|
||||
// secondary check (prompt user for a photo ID upload)
|
||||
CertificationPhotoSecondary CertificationPhotoStatus = "secondary"
|
||||
)
|
||||
|
||||
// GetCertificationPhoto retrieves the user's record from the DB or upserts their initial record.
|
||||
|
|
|
@ -41,6 +41,7 @@ const (
|
|||
NotificationAlsoCommented NotificationType = "also_comment"
|
||||
NotificationAlsoPosted NotificationType = "also_posted" // forum replies
|
||||
NotificationCertRejected NotificationType = "cert_rejected"
|
||||
NotificationCertSecondary NotificationType = "cert_secondary" // secondary cert photo requested
|
||||
NotificationCertApproved NotificationType = "cert_approved"
|
||||
NotificationPrivatePhoto NotificationType = "private_photo" // private photo grants
|
||||
NotificationNewPhoto NotificationType = "new_photo"
|
||||
|
|
BIN
web/static/img/photoid-example.jpg
Normal file
BIN
web/static/img/photoid-example.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
|
@ -582,6 +582,11 @@
|
|||
<span>
|
||||
Your <strong>certification photo</strong> was approved!
|
||||
</span>
|
||||
{{else if eq .Type "cert_secondary"}}
|
||||
<span class="icon"><i class="fa fa-certificate has-text-warning"></i></span>
|
||||
<span>
|
||||
About your <strong>certification photo:</strong>
|
||||
</span>
|
||||
{{else if eq .Type "cert_rejected"}}
|
||||
<span class="icon"><i class="fa fa-certificate has-text-danger"></i></span>
|
||||
<span>
|
||||
|
|
|
@ -117,10 +117,30 @@
|
|||
{{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>
|
||||
|
@ -136,6 +156,15 @@
|
|||
</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"
|
||||
|
@ -143,15 +172,21 @@
|
|||
|
||||
<div class="select is-fullwidth">
|
||||
<select class="common-reasons">
|
||||
<option value="">(Common Rejection Reasons)</option>
|
||||
<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 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>
|
||||
|
|
34
web/templates/email/certification_secondary.html
Normal file
34
web/templates/email/certification_secondary.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{{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>A secondary form of photo ID verification is requested</h1>
|
||||
|
||||
<p>Dear {{.Data.Username}},</p>
|
||||
|
||||
<p>
|
||||
An admin has reviewed your certification photo and is concerned that you may be
|
||||
too young to sign up for nonshy.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To get your certification photo approved, you will need to upload a copy of an
|
||||
official photo ID that shows your <strong>face</strong> and your <strong>date of birth</strong>.
|
||||
All other information on your ID should be blacked out before sharing it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please see the Certification Photo upload page for more information:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="{{.Data.URL}}" target="_blank">{{.Data.URL}}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This is an automated e-mail; do not reply to this message.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
|
@ -31,6 +31,8 @@
|
|||
<span class="tag is-info">Pending Approval</span>
|
||||
{{else if eq .CertificationPhoto.Status "approved"}}
|
||||
<span class="tag is-success">Approved</span>
|
||||
{{else if eq .CertificationPhoto.Status "secondary"}}
|
||||
<span class="tag is-info">Secondary ID Requested</span>
|
||||
{{else if eq .CertificationPhoto.Status "rejected"}}
|
||||
<span class="tag is-danger">Rejected</span>
|
||||
{{else}}
|
||||
|
@ -38,6 +40,111 @@
|
|||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Secondary form of ID requested? -->
|
||||
{{if eq .CertificationPhoto.Status "secondary"}}
|
||||
<div class="notification is-info is-light content">
|
||||
<p>
|
||||
A {{PrettyTitle}} admin has reviewed your certification photo and has requested
|
||||
that you show a secondary form of photo ID to verify your age.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please upload a scan or photo of an official, government issued ID which shows
|
||||
a photo of your face and your date of birth. <strong>Please black out any other
|
||||
personal information from your ID.</strong> We do not want your name, address,
|
||||
ID number, or any sensitive information - <em>just</em> your <strong>photo</strong>
|
||||
and your <strong>date of birth</strong>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please refer to this <strong>example photo ID:</strong>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<img src="/static/img/photoid-example.jpg">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Acceptable forms of ID may include the following:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>A government issued passport</li>
|
||||
<li>A government issued driver's license or photo ID card</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Please prepare this scan of your photo ID and upload it below.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For your privacy, <strong>we will delete this image</strong> from
|
||||
the {{PrettyTitle}} database after it has been reviewed.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="/photo/certification" enctype="multipart/form-data">
|
||||
{{InputCSRF}}
|
||||
<input type="hidden" name="secondary" value="true">
|
||||
|
||||
<div class="field">
|
||||
<label for="file" class="label">Browse and select your photo ID scan:</label>
|
||||
<div class="file has-name is-fullwidth">
|
||||
<label class="file-label">
|
||||
<input class="file-input" type="file"
|
||||
name="file"
|
||||
id="file"
|
||||
accept=".jpg,.jpeg,.jpe,.png"
|
||||
required>
|
||||
<span class="file-cta">
|
||||
<span class="file-icon">
|
||||
<i class="fas fa-upload"></i>
|
||||
</span>
|
||||
<span class="file-label">
|
||||
Choose a file…
|
||||
</span>
|
||||
</span>
|
||||
<span class="file-name" id="fileName">
|
||||
Select a file
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block has-text-centered">
|
||||
<button type="submit" class="button is-primary">Upload Photo ID</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
{{else if ne .CertificationPhoto.SecondaryFilename ""}}
|
||||
<div class="notification is-success is-light content">
|
||||
<p>
|
||||
Your <strong>secondary form of photo ID</strong> is pending approval. It will
|
||||
be deleted from the website's database after it has been reviewed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you wish to delete it immediately, please use the button below to
|
||||
"Delete My Certification Photo." It will remove your original certification
|
||||
photo as well as the secondary photo ID that you had uploaded before.
|
||||
</p>
|
||||
</div>
|
||||
{{else if .CertificationPhoto.SecondaryVerified}}
|
||||
<div class="notification is-success is-light content">
|
||||
<p>
|
||||
Your <i class="fa fa-id-card"></i> <strong>secondary form of photo ID</strong> has
|
||||
been reviewed and approved by an administrator. We have removed the image itself
|
||||
from the server.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Note: if you delete your Certification Photo at this point, or upload a new one,
|
||||
you will be requested to re-upload your photo ID again as well.
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Admin rejection comment -->
|
||||
{{if .CertificationPhoto.AdminComment}}
|
||||
<div class="notification is-warning content">
|
||||
<p>
|
||||
|
@ -77,7 +184,7 @@
|
|||
onclick="return window.confirm('Removing this photo will mark your account as Not Certified.\n\nYou will then need to upload a new certification photo for approval to certify your account again.\n\nAre you sure you want to do this?')"
|
||||
{{end}}>
|
||||
<span class="icon"><i class="fa fa-trash"></i></span>
|
||||
<span>Delete This Photo</span>
|
||||
<span>Delete My Certification Photo</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
Loading…
Reference in New Issue
Block a user