diff --git a/pkg/controller/photo/certification.go b/pkg/controller/photo/certification.go index 064d231..3b54c77 100644 --- a/pkg/controller/photo/certification.go +++ b/pkg/controller/photo/certification.go @@ -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) diff --git a/pkg/models/certification.go b/pkg/models/certification.go index 98a72d8..de44fce 100644 --- a/pkg/models/certification.go +++ b/pkg/models/certification.go @@ -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. diff --git a/pkg/models/notification.go b/pkg/models/notification.go index a04acb3..83901a3 100644 --- a/pkg/models/notification.go +++ b/pkg/models/notification.go @@ -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" diff --git a/web/static/img/photoid-example.jpg b/web/static/img/photoid-example.jpg new file mode 100644 index 0000000..7c51d1c Binary files /dev/null and b/web/static/img/photoid-example.jpg differ diff --git a/web/templates/account/dashboard.html b/web/templates/account/dashboard.html index a9173ca..a26758d 100644 --- a/web/templates/account/dashboard.html +++ b/web/templates/account/dashboard.html @@ -582,6 +582,11 @@ Your certification photo was approved! + {{else if eq .Type "cert_secondary"}} + + + About your certification photo: + {{else if eq .Type "cert_rejected"}} diff --git a/web/templates/admin/certification.html b/web/templates/admin/certification.html index 0c4caf7..53a4e7a 100644 --- a/web/templates/admin/certification.html +++ b/web/templates/admin/certification.html @@ -117,10 +117,30 @@ {{else}} {{.Status}} {{end}} + + + {{if .SecondaryVerified}} + + ID Verified + + {{end}} + + {{if .SecondaryFilename}} +
+
+ + + Secondary Photo ID Attached + +
+ +
+ {{end}} +
GeoIP Insights:
@@ -136,6 +156,15 @@
+ + {{if .SecondaryNeeded}} +
+ 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. +
+ {{end}} +