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" "strconv"
"code.nonshy.com/nonshy/website/pkg/config" "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/log"
"code.nonshy.com/nonshy/website/pkg/mail" "code.nonshy.com/nonshy/website/pkg/mail"
"code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/photo" "code.nonshy.com/nonshy/website/pkg/photo"
"code.nonshy.com/nonshy/website/pkg/session" "code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/templates" "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. // 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.Status = models.CertificationPhotoPending
cert.Filename = filename cert.Filename = filename
cert.AdminComment = "" cert.AdminComment = ""
cert.IPAddress = utility.IPAddress(r)
if err := cert.Save(); err != nil { if err := cert.Save(); err != nil {
session.FlashError(w, r, "Error saving your CertificationPhoto: %s", err) session.FlashError(w, r, "Error saving your CertificationPhoto: %s", err)
templates.Redirect(w, r.URL.Path) templates.Redirect(w, r.URL.Path)
@ -281,6 +284,9 @@ func AdminCertification() http.HandlerFunc {
} else { } else {
cert.Status = models.CertificationPhotoRejected cert.Status = models.CertificationPhotoRejected
cert.AdminComment = comment cert.AdminComment = comment
if comment == "(ignore)" {
cert.AdminComment = ""
}
if err := cert.Save(); err != nil { if err := cert.Save(); err != nil {
session.FlashError(w, r, "Failed to save CertificationPhoto: %s", err) session.FlashError(w, r, "Failed to save CertificationPhoto: %s", err)
templates.Redirect(w, r.URL.Path) templates.Redirect(w, r.URL.Path)
@ -291,6 +297,13 @@ func AdminCertification() http.HandlerFunc {
user.Certified = false user.Certified = false
user.Save() 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. // Notify the user about this rejection.
notif := &models.Notification{ notif := &models.Notification{
UserID: user.ID, UserID: user.ID,
@ -374,21 +387,27 @@ func AdminCertification() http.HandlerFunc {
session.FlashError(w, r, "Couldn't load certification photos from DB: %s", err) session.FlashError(w, r, "Couldn't load certification photos from DB: %s", err)
} }
// Map user IDs. // Map user IDs and GeoIP insights.
var userIDs = []uint64{} var (
userIDs = []uint64{}
ipAddresses = []string{}
)
for _, p := range photos { for _, p := range photos {
userIDs = append(userIDs, p.UserID) userIDs = append(userIDs, p.UserID)
ipAddresses = append(ipAddresses, p.IPAddress)
} }
insightsMap := geoip.MapInsights(ipAddresses)
userMap, err := models.MapUsers(currentUser, userIDs) userMap, err := models.MapUsers(currentUser, userIDs)
if err != nil { if err != nil {
session.FlashError(w, r, "Couldn't map user IDs: %s", err) session.FlashError(w, r, "Couldn't map user IDs: %s", err)
} }
var vars = map[string]interface{}{ var vars = map[string]interface{}{
"View": view, "View": view,
"Photos": photos, "Photos": photos,
"UserMap": userMap, "UserMap": userMap,
"Pager": pager, "InsightsMap": insightsMap,
"Pager": pager,
} }
if err := tmpl.Execute(w, r, vars); err != nil { if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/utility" "code.nonshy.com/nonshy/website/pkg/utility"
"github.com/oschwald/geoip2-golang" "github.com/oschwald/geoip2-golang"
) )
@ -22,6 +23,7 @@ type Insights struct {
PostalCode string PostalCode string
Latitude float64 Latitude float64
Longitude float64 Longitude float64
FlagEmoji string
} }
// IsZero checks if the insights are unpopulated. // IsZero checks if the insights are unpopulated.
@ -53,6 +55,15 @@ func (i Insights) Short() string {
return strings.Join(parts, "; ") 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. // GetRequestInsights returns structured insights based on the current HTTP request.
func GetRequestInsights(r *http.Request) (Insights, error) { func GetRequestInsights(r *http.Request) (Insights, error) {
var ( var (
@ -69,6 +80,12 @@ func GetInsights(ip net.IP) (Insights, error) {
return Insights{}, err return Insights{}, err
} }
// Country flag emoji.
emoji, err := CountryFlagEmoji(city.Country.IsoCode)
if err != nil {
emoji = "🏴‍☠️"
}
var result = Insights{ var result = Insights{
City: city.City.Names["en"], City: city.City.Names["en"],
CountryCode: city.Country.IsoCode, CountryCode: city.Country.IsoCode,
@ -77,6 +94,7 @@ func GetInsights(ip net.IP) (Insights, error) {
PostalCode: city.Postal.Code, PostalCode: city.Postal.Code,
Latitude: city.Location.Latitude, Latitude: city.Location.Latitude,
Longitude: city.Location.Longitude, Longitude: city.Location.Longitude,
FlagEmoji: emoji,
} }
for _, sub := range city.Subdivisions { for _, sub := range city.Subdivisions {
if name, ok := sub.Names["en"]; ok { if name, ok := sub.Names["en"]; ok {
@ -87,6 +105,30 @@ func GetInsights(ip net.IP) (Insights, error) {
return result, nil 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. // GetRequestCity returns the GeoIP City result for the current HTTP request.
func GetRequestCity(r *http.Request) (*geoip2.City, error) { func GetRequestCity(r *http.Request) (*geoip2.City, error) {
var ( var (

View File

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

View File

@ -100,7 +100,7 @@ abbr {
/* Bulma hack: smaller tag size inside of tab buttons. The default tag height /* Bulma hack: smaller tag size inside of tab buttons. The default tag height
is set to 2em which makes the boxed tabs too tall and the bottom line doesn't is set to 2em which makes the boxed tabs too tall and the bottom line doesn't
draw correctly. Seen on e.g. Profile Pages for the tag # of photos. */ draw correctly. Seen on e.g. Profile Pages for the tag # of photos. */
.tabs.is-boxed > ul > li > a .tag { .tabs.is-boxed>ul>li>a .tag {
height: 1.5em !important; height: 1.5em !important;
} }
@ -131,15 +131,28 @@ abbr {
white-space: nowrap; white-space: nowrap;
margin-bottom: auto; margin-bottom: auto;
} }
.nonshy-navbar-notification-tag.is-warning { .nonshy-navbar-notification-tag.is-warning {
background-color: #ffd324; background-color: #ffd324;
color: rgba(0, 0, 0, 0.7); color: rgba(0, 0, 0, 0.7);
} }
.nonshy-navbar-notification-tag.is-info { .nonshy-navbar-notification-tag.is-info {
background-color: #0f81cc; background-color: #0f81cc;
color: #fff; color: #fff;
} }
.nonshy-navbar-notification-tag.is-danger { .nonshy-navbar-notification-tag.is-danger {
background-color: #ff0537; background-color: #ff0537;
color: #fff; 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> </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"> <div class="field">
<textarea class="textarea" name="comment" <textarea class="textarea" name="comment"
cols="60" rows="2" cols="60" rows="2"
@ -134,7 +149,12 @@
</option> </option>
<option value="The sheet of paper must also include the website name: nonshy">Website name not visible</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="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> <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> </select>
</div> </div>
</div> </div>

View File

@ -140,7 +140,15 @@
<div class="column"> <div class="column">
{{.CurrentUser.Username}} {{.CurrentUser.Username}}
{{if .NavUnreadNotifications}}<span class="nonshy-navbar-notification-tag is-warning ml-1">{{.NavUnreadNotifications}}</span>{{end}} {{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>
</div> </div>
</a> </a>
@ -186,7 +194,16 @@
<a class="navbar-item has-text-danger" href="/admin"> <a class="navbar-item has-text-danger" href="/admin">
<span class="icon"><i class="fa fa-peace"></i></span> <span class="icon"><i class="fa fa-peace"></i></span>
<span>Admin</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> </a>
{{end}} {{end}}
{{if .SessionImpersonated}} {{if .SessionImpersonated}}