website/pkg/controller/photo/certification.go
Noah Petherbridge f4d176a538 Change Logs
* Add a ChangeLog table to collect historic updates to various database tables.
* Created, Updated (with field diffs) and Deleted actions are logged, as well
  as certification photo approves/denies.
* Specific items added to the change log:
  * When a user photo is marked Explicit by an admin
  * When users block/unblock each other
  * When photo comments are posted, edited, and deleted
  * When forums are created, edited, and deleted
  * When forum comments are created, edited and deleted
  * When a new forum thread is created
  * When a user uploads or removes their own certification photo
  * When an admin approves or rejects a certification photo
  * When a user uploads, modifies or deletes their gallery photos
  * When a friend request is sent
  * When a friend request is accepted, ignored, or rejected
  * When a friendship is removed
2024-02-25 17:03:36 -08:00

430 lines
13 KiB
Go

package photo
import (
"bytes"
"io"
"net/http"
"path/filepath"
"strconv"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/geoip"
"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/photo"
"code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/templates"
"code.nonshy.com/nonshy/website/pkg/utility"
)
// 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.AdminComment = ""
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)
}
// Log the change.
models.LogDeleted(currentUser, nil, "certification_photos", currentUser.ID, "Removed their certification photo.", cert)
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 = ""
cert.IPAddress = utility.IPAddress(r)
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)
}
// Log the change.
models.LogCreated(currentUser, "certification_photos", currentUser.ID, "Uploaded a new certification photo.")
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) {
// View status
var view = r.FormValue("view")
if view == "" {
view = "pending"
}
// Get the current user.
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "Couldn't get CurrentUser: %s", err)
}
// Scope check based on view.
switch view {
case "pending":
// Scope check.
if !currentUser.HasAdminScope(config.ScopeCertificationApprove) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeCertificationApprove)
templates.Redirect(w, "/admin")
return
}
case "approved", "rejected":
// Scope check.
if !currentUser.HasAdminScope(config.ScopeCertificationList) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeCertificationList)
templates.Redirect(w, "/admin")
return
}
}
// Short circuit the GET view for username/email search (exact match)
if username := r.FormValue("username"); username != "" {
// Scope check.
if !currentUser.HasAdminScope(config.ScopeCertificationView) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeCertificationView)
templates.Redirect(w, "/admin")
return
}
user, err := models.FindUser(username)
if err != nil {
session.FlashError(w, r, "Username or email '%s' not found.", username)
templates.Redirect(w, r.URL.Path)
return
}
cert, err := models.GetCertificationPhoto(user.ID)
if err != nil {
session.FlashError(w, r, "Couldn't get their certification photo: %s", err)
templates.Redirect(w, r.URL.Path)
return
}
var vars = map[string]interface{}{
"View": view,
"Photos": []*models.CertificationPhoto{cert},
"UserMap": &models.UserMap{user.ID: user},
"FoundUser": user,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
// Making a verdict?
if r.Method == http.MethodPost {
// Scope check.
if !currentUser.HasAdminScope(config.ScopeCertificationApprove) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeCertificationApprove)
templates.Redirect(w, r.URL.Path)
return
}
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 comment == "(ignore)" {
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
}
// Uncertify the user just in case.
user.Certified = false
user.Save()
// Log the change.
models.LogEvent(user, currentUser, models.ChangeLogRejected, "certification_photos", user.ID, "Rejected the certification photo with comment: "+comment)
// Did we silently ignore it?
if comment == "(ignore)" {
session.FlashError(w, r, "The certification photo was ignored with no comment, and will not notify the sender.")
templates.Redirect(w, r.URL.Path)
return
}
// Notify the user about this rejection.
notif := &models.Notification{
UserID: user.ID,
AboutUser: *user,
Type: models.NotificationCertRejected,
Message: comment,
}
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: "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 about this approval.
notif := &models.Notification{
UserID: user.ID,
AboutUser: *user,
Type: models.NotificationCertApproved,
}
if err := models.CreateNotification(notif); err != nil {
log.Error("Couldn't create approval notification: %s", err)
}
// 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)
}
// Log the change.
models.LogEvent(user, currentUser, models.ChangeLogApproved, "certification_photos", user.ID, "Approved the certification photo.")
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: config.PageSizeAdminCertification,
Sort: "updated_at desc",
}
pager.ParsePage(r)
photos, err := models.CertificationPhotosNeedingApproval(models.CertificationPhotoStatus(view), pager)
if err != nil {
session.FlashError(w, r, "Couldn't load certification photos from DB: %s", err)
}
// Map user IDs and GeoIP insights.
var (
userIDs = []uint64{}
ipAddresses = []string{}
)
for _, p := range photos {
userIDs = append(userIDs, p.UserID)
ipAddresses = append(ipAddresses, p.IPAddress)
}
insightsMap := geoip.MapInsights(ipAddresses)
userMap, err := models.MapUsers(currentUser, userIDs)
if err != nil {
session.FlashError(w, r, "Couldn't map user IDs: %s", err)
}
var vars = map[string]interface{}{
"View": view,
"Photos": photos,
"UserMap": userMap,
"InsightsMap": insightsMap,
"Pager": pager,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}