diff --git a/pkg/controller/photo/certification.go b/pkg/controller/photo/certification.go index a61bc17..d3bb8cc 100644 --- a/pkg/controller/photo/certification.go +++ b/pkg/controller/photo/certification.go @@ -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) diff --git a/pkg/geoip/geoip.go b/pkg/geoip/geoip.go index d952e74..bae21ce 100644 --- a/pkg/geoip/geoip.go +++ b/pkg/geoip/geoip.go @@ -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 ( diff --git a/pkg/models/certification.go b/pkg/models/certification.go index a072cee..98a72d8 100644 --- a/pkg/models/certification.go +++ b/pkg/models/certification.go @@ -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 } diff --git a/web/static/css/theme.css b/web/static/css/theme.css index 66468f9..735c6ec 100644 --- a/web/static/css/theme.css +++ b/web/static/css/theme.css @@ -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; +} \ No newline at end of file diff --git a/web/templates/admin/certification.html b/web/templates/admin/certification.html index b3748c7..5c8c7dc 100644 --- a/web/templates/admin/certification.html +++ b/web/templates/admin/certification.html @@ -121,6 +121,21 @@ +
+ GeoIP Insights: +
+ {{$Insights := $Root.InsightsMap.Get .IPAddress}} + {{if $Insights.IsZero}} + No GeoIP insights available for this IP address! + {{else}} + {{$Insights.Medium}} + {{end}} +
+
+ IP: {{.IPAddress}} +
+
+