website/pkg/controller/photo/certification.go

418 lines
12 KiB
Go
Raw Permalink Normal View History

package photo
import (
"bytes"
"io"
"net/http"
"path/filepath"
"strconv"
2022-08-26 04:21:46 +00:00
"code.nonshy.com/nonshy/website/pkg/config"
2023-11-25 22:28:16 +00:00
"code.nonshy.com/nonshy/website/pkg/geoip"
2022-08-26 04:21:46 +00:00
"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"
2023-11-25 22:28:16 +00:00
"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)
}
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 = ""
2023-11-25 22:28:16 +00:00
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)
}
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)
}
Admin Groups & Permissions Add a permission system for admin users so you can lock down specific admins to a narrower set of features instead of them all having omnipotent powers. * New page: Admin Dashboard -> Admin Permissions Management * Permissions are handled in the form of 'scopes' relevant to each feature or action on the site. Scopes are assigned to Groups, and in turn, admin user accounts are placed in those Groups. * The Superusers group (scope '*') has wildcard permission to all scopes. The permissions dashboard has a create-once action to initialize the Superusers for the first admin who clicks on it, and places that admin in the group. The following are the exhaustive list of permission changes on the site: * Moderator scopes: * Chat room (enter the room with Operator permission) * Forums (can edit or delete user posts on the forum) * Photo Gallery (can see all private/friends-only photos on the site gallery or user profile pages) * Certification photos (with nuanced sub-action permissions) * Approve: has access to the Pending tab to act on incoming pictures * List: can paginate thru past approved/rejected photos * View: can bring up specific user cert photo from their profile * The minimum requirement is Approve or else no cert photo page will load for your admin user. * User Actions (each action individually scoped) * Impersonate * Ban * Delete * Promote to admin * Inner circle whitelist: no longer are admins automatically part of the inner circle unless they have a specialized scope attached. The AdminRequired decorator may also apply scopes on an entire admin route. The following routes have scopes to limit them: * Forum Admin (manage forums and their settings) * Remove from inner circle
2023-08-02 03:39:48 +00:00
// 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 != "" {
Admin Groups & Permissions Add a permission system for admin users so you can lock down specific admins to a narrower set of features instead of them all having omnipotent powers. * New page: Admin Dashboard -> Admin Permissions Management * Permissions are handled in the form of 'scopes' relevant to each feature or action on the site. Scopes are assigned to Groups, and in turn, admin user accounts are placed in those Groups. * The Superusers group (scope '*') has wildcard permission to all scopes. The permissions dashboard has a create-once action to initialize the Superusers for the first admin who clicks on it, and places that admin in the group. The following are the exhaustive list of permission changes on the site: * Moderator scopes: * Chat room (enter the room with Operator permission) * Forums (can edit or delete user posts on the forum) * Photo Gallery (can see all private/friends-only photos on the site gallery or user profile pages) * Certification photos (with nuanced sub-action permissions) * Approve: has access to the Pending tab to act on incoming pictures * List: can paginate thru past approved/rejected photos * View: can bring up specific user cert photo from their profile * The minimum requirement is Approve or else no cert photo page will load for your admin user. * User Actions (each action individually scoped) * Impersonate * Ban * Delete * Promote to admin * Inner circle whitelist: no longer are admins automatically part of the inner circle unless they have a specialized scope attached. The AdminRequired decorator may also apply scopes on an entire admin route. The following routes have scopes to limit them: * Forum Admin (manage forums and their settings) * Remove from inner circle
2023-08-02 03:39:48 +00:00
// 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 {
Admin Groups & Permissions Add a permission system for admin users so you can lock down specific admins to a narrower set of features instead of them all having omnipotent powers. * New page: Admin Dashboard -> Admin Permissions Management * Permissions are handled in the form of 'scopes' relevant to each feature or action on the site. Scopes are assigned to Groups, and in turn, admin user accounts are placed in those Groups. * The Superusers group (scope '*') has wildcard permission to all scopes. The permissions dashboard has a create-once action to initialize the Superusers for the first admin who clicks on it, and places that admin in the group. The following are the exhaustive list of permission changes on the site: * Moderator scopes: * Chat room (enter the room with Operator permission) * Forums (can edit or delete user posts on the forum) * Photo Gallery (can see all private/friends-only photos on the site gallery or user profile pages) * Certification photos (with nuanced sub-action permissions) * Approve: has access to the Pending tab to act on incoming pictures * List: can paginate thru past approved/rejected photos * View: can bring up specific user cert photo from their profile * The minimum requirement is Approve or else no cert photo page will load for your admin user. * User Actions (each action individually scoped) * Impersonate * Ban * Delete * Promote to admin * Inner circle whitelist: no longer are admins automatically part of the inner circle unless they have a specialized scope attached. The AdminRequired decorator may also apply scopes on an entire admin route. The following routes have scopes to limit them: * Forum Admin (manage forums and their settings) * Remove from inner circle
2023-08-02 03:39:48 +00:00
// 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
2023-11-25 22:28:16 +00:00
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()
2023-11-25 22:28:16 +00:00
// 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)
}
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,
2022-08-14 05:44:57 +00:00
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)
}
2023-11-25 22:28:16 +00:00
// Map user IDs and GeoIP insights.
var (
userIDs = []uint64{}
ipAddresses = []string{}
)
for _, p := range photos {
userIDs = append(userIDs, p.UserID)
2023-11-25 22:28:16 +00:00
ipAddresses = append(ipAddresses, p.IPAddress)
}
2023-11-25 22:28:16 +00:00
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{}{
2023-11-25 22:28:16 +00:00
"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
}
})
}