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 } }) }