f4d176a538
* 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
430 lines
13 KiB
Go
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
|
|
}
|
|
})
|
|
}
|