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 }