Certification photo admin updates

face-detect
Noah Petherbridge 2023-11-25 14:28:16 -08:00
parent 7ef22db5e2
commit 3d4c728d75
6 changed files with 121 additions and 9 deletions

View File

@ -8,12 +8,14 @@ import (
"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.
@ -123,6 +125,7 @@ func Certification() http.HandlerFunc {
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)
@ -281,6 +284,9 @@ func AdminCertification() http.HandlerFunc {
} 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)
@ -291,6 +297,13 @@ func AdminCertification() http.HandlerFunc {
user.Certified = false
user.Save()
// 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,
@ -374,11 +387,16 @@ func AdminCertification() http.HandlerFunc {
session.FlashError(w, r, "Couldn't load certification photos from DB: %s", err)
}
// Map user IDs.
var userIDs = []uint64{}
// 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)
@ -388,6 +406,7 @@ func AdminCertification() http.HandlerFunc {
"View": view,
"Photos": photos,
"UserMap": userMap,
"InsightsMap": insightsMap,
"Pager": pager,
}
if err := tmpl.Execute(w, r, vars); err != nil {

View File

@ -9,6 +9,7 @@ import (
"strings"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/utility"
"github.com/oschwald/geoip2-golang"
)
@ -22,6 +23,7 @@ type Insights struct {
PostalCode string
Latitude float64
Longitude float64
FlagEmoji string
}
// IsZero checks if the insights are unpopulated.
@ -53,6 +55,15 @@ func (i Insights) Short() string {
return strings.Join(parts, "; ")
}
// Medium prints a summary including country flag emoji with all fields except lat/long.
func (i Insights) Medium() string {
var parts = []string{
strings.Join([]string{i.FlagEmoji, i.CountryCode}, " "),
}
parts = append(parts, i.Short())
return strings.Join(parts, "; ")
}
// GetRequestInsights returns structured insights based on the current HTTP request.
func GetRequestInsights(r *http.Request) (Insights, error) {
var (
@ -69,6 +80,12 @@ func GetInsights(ip net.IP) (Insights, error) {
return Insights{}, err
}
// Country flag emoji.
emoji, err := CountryFlagEmoji(city.Country.IsoCode)
if err != nil {
emoji = "🏴‍☠️"
}
var result = Insights{
City: city.City.Names["en"],
CountryCode: city.Country.IsoCode,
@ -77,6 +94,7 @@ func GetInsights(ip net.IP) (Insights, error) {
PostalCode: city.Postal.Code,
Latitude: city.Location.Latitude,
Longitude: city.Location.Longitude,
FlagEmoji: emoji,
}
for _, sub := range city.Subdivisions {
if name, ok := sub.Names["en"]; ok {
@ -87,6 +105,30 @@ func GetInsights(ip net.IP) (Insights, error) {
return result, nil
}
type InsightsMap map[string]Insights
func (i InsightsMap) Get(key string) Insights {
return i[key]
}
// MapInsights returns a hash map of IP address (strings) to their Insights.
func MapInsights(addrs []string) InsightsMap {
var result = map[string]Insights{}
for _, addr := range addrs {
if _, ok := result[addr]; ok {
continue
}
ip := net.ParseIP(addr)
insights, err := GetInsights(ip)
if err != nil {
log.Error("MapInsights(%s): %s", addr, err)
}
result[addr] = insights
}
return result
}
// GetRequestCity returns the GeoIP City result for the current HTTP request.
func GetRequestCity(r *http.Request) (*geoip2.City, error) {
var (

View File

@ -14,6 +14,7 @@ type CertificationPhoto struct {
Filesize int64
Status CertificationPhotoStatus
AdminComment string
IPAddress string // the IP they uploaded the photo from
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -131,15 +131,28 @@ abbr {
white-space: nowrap;
margin-bottom: auto;
}
.nonshy-navbar-notification-tag.is-warning {
background-color: #ffd324;
color: rgba(0, 0, 0, 0.7);
}
.nonshy-navbar-notification-tag.is-info {
background-color: #0f81cc;
color: #fff;
}
.nonshy-navbar-notification-tag.is-danger {
background-color: #ff0537;
color: #fff;
}
.nonshy-navbar-notification-tag.is-success {
background-color: #3ec487;
color: #fff;
}
.nonshy-navbar-notification-tag.is-mixed {
background: linear-gradient(141deg, #ff0537 0, #3ec487 100%);
color: #fff;
}

View File

@ -121,6 +121,21 @@
</div>
</div>
<div class="block">
<strong><i class="fa fa-location-dot mr-1"></i> GeoIP Insights:</strong>
<div>
{{$Insights := $Root.InsightsMap.Get .IPAddress}}
{{if $Insights.IsZero}}
<span class="has-text-danger">No GeoIP insights available for this IP address!</span>
{{else}}
{{$Insights.Medium}}
{{end}}
</div>
<div>
IP: {{.IPAddress}}
</div>
</div>
<div class="field">
<textarea class="textarea" name="comment"
cols="60" rows="2"
@ -134,7 +149,12 @@
</option>
<option value="The sheet of paper must also include the website name: nonshy">Website name not visible</option>
<option value="Please take a clearer picture that shows your arm and hand holding onto the sheet of paper">Unclear picture (hand not visible enough)</option>
<option value="This photo has been digitally altered, please take a new certification picture and upload it as it comes off your camera">Photoshopped or digitally altered</option>
<option value="You had a previous account on nonshy which was suspended and you are not welcome with a new account.">User was previously banned from nonshy</option>
<option value="This is not an acceptable certification photo.">Not acceptable</option>
<optgroup label="Other Actions">
<option value="(ignore)">(Silently reject this photo without sending an e-mail)</option>
</optgroup>
</select>
</div>
</div>

View File

@ -140,7 +140,15 @@
<div class="column">
{{.CurrentUser.Username}}
{{if .NavUnreadNotifications}}<span class="nonshy-navbar-notification-tag is-warning ml-1">{{.NavUnreadNotifications}}</span>{{end}}
{{if .NavAdminNotifications}}<span class="nonshy-navbar-notification-tag is-danger ml-1">{{.NavAdminNotifications}}</span>{{end}}
{{if .NavAdminNotifications}}
{{if and (.NavCertificationPhotos) (not .NavAdminFeedback)}}
<span class="nonshy-navbar-notification-tag is-success ml-1">{{.NavAdminNotifications}}</span>
{{else if and .NavCertificationPhotos .NavAdminFeedback}}
<span class="nonshy-navbar-notification-tag is-mixed ml-1">{{.NavAdminNotifications}}</span>
{{else}}
<span class="nonshy-navbar-notification-tag is-danger ml-1">{{.NavAdminNotifications}}</span>
{{end}}
{{end}}
</div>
</div>
</a>
@ -186,7 +194,16 @@
<a class="navbar-item has-text-danger" href="/admin">
<span class="icon"><i class="fa fa-peace"></i></span>
<span>Admin</span>
{{if .NavAdminNotifications}}<span class="nonshy-navbar-notification-tag is-danger ml-1">{{.NavAdminNotifications}}</span>{{end}}
{{if .NavAdminNotifications}}
<!-- Color code them by the type -->
{{if and (.NavCertificationPhotos) (not .NavAdminFeedback)}}
<span class="nonshy-navbar-notification-tag is-success ml-1">{{.NavAdminNotifications}}</span>
{{else if and .NavCertificationPhotos .NavAdminFeedback}}
<span class="nonshy-navbar-notification-tag is-mixed ml-1">{{.NavAdminNotifications}}</span>
{{else}}
<span class="nonshy-navbar-notification-tag is-danger ml-1">{{.NavAdminNotifications}}</span>
{{end}}
{{end}}
</a>
{{end}}
{{if .SessionImpersonated}}