website/pkg/models/certification.go
Noah Petherbridge 1c013aa8d8 Alert/Confirm Modals + Auto Revoke Certification Photo
* If a Certified member deletes the final picture from their gallery page, their
  Certification Photo will be automatically rejected and they are instructed to
  begin the process again from the beginning.
* Add nice Alert and Confirm modals around the website in place of the standard
  browser feature. Note: the inline confirm on submit buttons are still using
  the standard feature for now, as intercepting submit buttons named "intent"
  causes problems in getting the final form to submit.
2024-12-23 14:58:39 -08:00

174 lines
5.3 KiB
Go

package models
import (
"errors"
"fmt"
"time"
"code.nonshy.com/nonshy/website/pkg/log"
"gorm.io/gorm"
)
// CertificationPhoto table.
type CertificationPhoto struct {
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
const (
CertificationPhotoNeeded CertificationPhotoStatus = "needed"
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.
func GetCertificationPhoto(userID uint64) (*CertificationPhoto, error) {
p := &CertificationPhoto{}
result := DB.Where("user_id = ?", userID).First(&p)
if result.Error == gorm.ErrRecordNotFound {
p = &CertificationPhoto{
UserID: userID,
Status: CertificationPhotoNeeded,
}
result = DB.Create(p)
return p, result.Error
}
return p, result.Error
}
// CertifiedSince retrieve's the last updated date of the user's certification photo, if approved.
//
// This incurs a DB query for their cert photo.
func (u *User) CertifiedSince() (time.Time, error) {
if !u.Certified {
return time.Time{}, errors.New("user is not certified")
}
cert, err := GetCertificationPhoto(u.ID)
if err != nil {
return time.Time{}, err
}
if cert.Status != CertificationPhotoApproved {
// The edge case can come up if a user was manually certified but didn't have an approved picture.
// Return their CreatedAt instead.
return u.CreatedAt, nil
}
return cert.UpdatedAt, nil
}
// CertificationPhotosNeedingApproval returns a pager of the pictures that require admin approval.
func CertificationPhotosNeedingApproval(status CertificationPhotoStatus, pager *Pagination) ([]*CertificationPhoto, error) {
var p = []*CertificationPhoto{}
query := DB.Where(
"status = ?",
status,
).Order(
pager.Sort,
)
// Get the total count.
query.Model(&CertificationPhoto{}).Count(&pager.Total)
result := query.Offset(
pager.GetOffset(),
).Limit(pager.PerPage).Find(&p)
return p, result.Error
}
// CountCertificationPhotosNeedingApproval gets the count of pending photos for admin alert.
func CountCertificationPhotosNeedingApproval() int64 {
var count int64
DB.Where("status = ?", CertificationPhotoPending).Model(&CertificationPhoto{}).Count(&count)
return count
}
// MaybeRevokeCertificationForEmptyGallery will delete a user's certification photo if they delete every picture from their gallery.
//
// Returns true if their certification was revoked.
func MaybeRevokeCertificationForEmptyGallery(user *User) bool {
cert, err := GetCertificationPhoto(user.ID)
if err != nil {
return false
}
// Ignore if their cert photo status is not applicable to be revoked.
if cert.Status == CertificationPhotoNeeded || cert.Status == CertificationPhotoRejected {
return false
}
if count := CountPhotos(user.ID); count == 0 {
// Revoke their cert status.
cert.Status = CertificationPhotoRejected
cert.SecondaryVerified = false
cert.AdminComment = "Your certification photo has been automatically rejected because you have deleted every photo on your gallery. " +
"To restore your certified status, please upload photos to your gallery and submit a new Certification Photo for approval."
if err := cert.Save(); err != nil {
log.Error("MaybeRevokeCertificationForEmptyGallery(%s): %s", user.Username, err)
}
// Update the user's Certified flag. Note: we freshly query the user here in case they had JUST deleted
// their default profile picture - so that we don't (re)set their old ProfilePhotoID by accident!
if user, err := GetUser(user.ID); err == nil {
user.Certified = false
if err := user.Save(); err != nil {
log.Error("MaybeRevokeCertificationForEmptyGallery(%s): saving user certified flag: %s", user.Username, err)
}
}
// Notify the site admin for visibility.
fb := &Feedback{
Intent: "report",
Subject: "A certified user has deleted all their pictures",
UserID: user.ID,
TableName: "users",
TableID: user.ID,
Message: fmt.Sprintf(
"The username **@%s** has deleted every picture in their gallery, and so their Certification Photo status has been revoked.",
user.Username,
),
}
// Save the feedback.
if err := CreateFeedback(fb); err != nil {
log.Error("Couldn't save feedback from user auto-revoking their cert photo: %s", err)
}
return true
}
return false
}
// Save photo.
func (p *CertificationPhoto) Save() error {
result := DB.Save(p)
return result.Error
}
// Delete the DB entry.
func (p *CertificationPhoto) Delete() error {
return DB.Delete(p).Error
}