website/pkg/controller/photo/certification.go
Noah 400a256ec8 Certification Photo Workflow
* Add "Site Gallery" page showing all public+gallery member photos.
* Add "Certification Required" decorator for gallery and other main pages.
* Add the Certification Photo workflow:
  * Users have a checklist on their dashboard to upload a profile pic
    and post a certification selfie (two requirements)
  * Admins notified by email when a new certification pic comes in.
  * Admin can reject (w/ comment) or approve the pic.
  * Users can re-upload or delete their pic at the cost of losing
    certification status if they make any such changes.
  * Users are emailed when their photo is either approved or rejected.
* User Preferences: can now save the explicit pref to your account.
* Explicit photos on user pages and site gallery are hidden if the
  current user hasn't opted-in (user can always see their own explicit
  photos regardless of the setting)
* If a user is viewing a member gallery and explicit pics are hidden, a
  count of the number of explicit pics is shown to inform the user that
  more DO exist, they just don't see them. The site gallery does not do
  this and simply hides explicit photos.
2022-08-13 15:39:31 -07:00

304 lines
8.9 KiB
Go

package photo
import (
"bytes"
"io"
"net/http"
"path/filepath"
"strconv"
"git.kirsle.net/apps/gosocial/pkg/config"
"git.kirsle.net/apps/gosocial/pkg/log"
"git.kirsle.net/apps/gosocial/pkg/mail"
"git.kirsle.net/apps/gosocial/pkg/models"
"git.kirsle.net/apps/gosocial/pkg/photo"
"git.kirsle.net/apps/gosocial/pkg/session"
"git.kirsle.net/apps/gosocial/pkg/templates"
)
// CertificationRequiredError handles the error page when a user is denied due to lack of certification.
func CertificationRequiredError() http.HandlerFunc {
tmpl := templates.Must("errors/certification_required.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "Unexpected error: could not get currentUser.")
templates.Redirect(w, "/")
return
}
// Get the current user's cert photo (or create the DB record).
cert, err := models.GetCertificationPhoto(currentUser.ID)
if err != nil {
session.FlashError(w, r, "Unexpected error: could not get or create CertificationPhoto record.")
templates.Redirect(w, "/")
return
}
var vars = map[string]interface{}{
"CertificationPhoto": cert,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}
// Certification photo controller.
func Certification() http.HandlerFunc {
tmpl := templates.Must("photo/certification.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "Unexpected error: could not get currentUser.")
templates.Redirect(w, "/")
return
}
// Get the current user's cert photo (or create the DB record).
cert, err := models.GetCertificationPhoto(currentUser.ID)
if err != nil {
session.FlashError(w, r, "Unexpected error: could not get or create CertificationPhoto record.")
templates.Redirect(w, "/")
return
}
// Uploading?
if r.Method == http.MethodPost {
// Are they deleting their photo?
if r.PostFormValue("delete") == "true" {
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)
}
cert.Filename = ""
}
cert.Status = models.CertificationPhotoNeeded
cert.Save()
// Removing your photo = not certified again.
currentUser.Certified = false
if err := currentUser.Save(); err != nil {
session.FlashError(w, r, "Error saving your User data: %s", err)
}
session.Flash(w, r, "Your certification photo has been deleted.")
templates.Redirect(w, r.URL.Path)
return
}
// Get the uploaded file.
file, header, err := r.FormFile("file")
if err != nil {
session.FlashError(w, r, "Error receiving your file: %s", err)
templates.Redirect(w, r.URL.Path)
return
}
var buf bytes.Buffer
io.Copy(&buf, file)
filename, _, err := photo.UploadPhoto(photo.UploadConfig{
User: currentUser,
Extension: filepath.Ext(header.Filename),
Data: buf.Bytes(),
})
if err != nil {
session.FlashError(w, r, "Error processing your upload: %s", err)
templates.Redirect(w, r.URL.Path)
return
}
// Are they replacing their old 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)
}
}
// Update their certification photo.
cert.Status = models.CertificationPhotoPending
cert.Filename = filename
cert.AdminComment = ""
if err := cert.Save(); err != nil {
session.FlashError(w, r, "Error saving your CertificationPhoto: %s", err)
templates.Redirect(w, r.URL.Path)
return
}
// Set their approval status back to false.
currentUser.Certified = false
if err := currentUser.Save(); err != nil {
session.FlashError(w, r, "Error saving your User data: %s", err)
}
// 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)
}
session.Flash(w, r, "Your certification photo has been uploaded and is now awaiting approval.")
templates.Redirect(w, r.URL.Path)
return
}
var vars = map[string]interface{}{
"CertificationPhoto": cert,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}
// AdminCertification controller (/admin/photo/certification)
func AdminCertification() http.HandlerFunc {
tmpl := templates.Must("admin/certification.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Making a verdict?
if r.Method == http.MethodPost {
var (
comment = r.PostFormValue("comment")
verdict = r.PostFormValue("verdict")
)
userID, err := strconv.Atoi(r.PostFormValue("user_id"))
if err != nil {
session.FlashError(w, r, "Invalid user_id data type.")
templates.Redirect(w, r.URL.Path)
return
}
// Look up the user in case we'll toggle their Certified state.
user, err := models.GetUser(uint64(userID))
if err != nil {
session.FlashError(w, r, "Couldn't get user ID %d: %s", userID, err)
templates.Redirect(w, r.URL.Path)
return
}
// Look up this photo.
cert, err := models.GetCertificationPhoto(uint64(userID))
if err != nil {
session.FlashError(w, r, "Couldn't get certification photo.")
templates.Redirect(w, r.URL.Path)
return
} else if cert.Filename == "" {
session.FlashError(w, r, "That photo has no filename anymore??")
templates.Redirect(w, r.URL.Path)
return
}
switch verdict {
case "reject":
if comment == "" {
session.FlashError(w, r, "An admin comment is required when rejecting a photo.")
} else {
cert.Status = models.CertificationPhotoRejected
cert.AdminComment = comment
if err := cert.Save(); err != nil {
session.FlashError(w, r, "Failed to save CertificationPhoto: %s", err)
templates.Redirect(w, r.URL.Path)
return
}
// Uncertify the user just in case.
user.Certified = false
user.Save()
// Notify the user via email.
if err := mail.Send(mail.Message{
To: user.Email,
Subject: "Your certification photo has been rejected",
Template: "email/certification_rejected.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, "Certification photo rejected!")
case "approve":
cert.Status = models.CertificationPhotoApproved
cert.AdminComment = ""
if err := cert.Save(); err != nil {
session.FlashError(w, r, "Failed to save CertificationPhoto: %s", err)
templates.Redirect(w, r.URL.Path)
return
}
// Certify the user!
user.Certified = true
user.Save()
// Notify the user via email.
if err := mail.Send(mail.Message{
To: user.Email,
Subject: "Your certification photo has been approved!",
Template: "email/certification_approved.html",
Data: map[string]interface{}{
"Username": user.Username,
"URL": config.Current.BaseURL,
},
}); err != nil {
session.FlashError(w, r, "Note: failed to email user about the approval: %s", err)
}
session.Flash(w, r, "Certification photo approved!")
default:
session.FlashError(w, r, "Unsupported verdict option: %s", verdict)
}
templates.Redirect(w, r.URL.Path)
return
}
// Get the pending photos.
pager := &models.Pagination{
Page: 1,
PerPage: 20,
Sort: "updated_at desc",
}
pager.ParsePage(r)
photos, err := models.CertificationPhotosNeedingApproval(models.CertificationPhotoPending, pager)
if err != nil {
session.FlashError(w, r, "Couldn't load certification photos from DB: %s", err)
}
// Map user IDs.
var userIDs = []uint64{}
for _, p := range photos {
userIDs = append(userIDs, p.UserID)
}
userMap, err := models.MapUsers(userIDs)
if err != nil {
session.FlashError(w, r, "Couldn't map user IDs: %s", err)
}
var vars = map[string]interface{}{
"Photos": photos,
"UserMap": userMap,
"Pager": pager,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}