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,21 +387,27 @@ 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)
}
var vars = map[string]interface{}{
"View": view,
"Photos": photos,
"UserMap": userMap,
"Pager": pager,
"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)

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

@ -100,7 +100,7 @@ abbr {
/* 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
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;
}
@ -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}}