Certification photo admin updates
This commit is contained in:
parent
7ef22db5e2
commit
3d4c728d75
|
@ -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)
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user