Age gate: collect birthdates retroactively and on new signup
This commit is contained in:
parent
3d34306c7e
commit
d36e71549a
96
pkg/controller/account/age_gate.go
Normal file
96
pkg/controller/account/age_gate.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/utility"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User age gate page to collect birthdates retroactively (/settings/age-gate)
|
||||||
|
func AgeGate() http.HandlerFunc {
|
||||||
|
tmpl := templates.Must("account/age_gate.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := map[string]interface{}{
|
||||||
|
"Enum": config.ProfileEnums,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the current user in case of updates.
|
||||||
|
user, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't get CurrentUser: %s", err)
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we POSTing?
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
var dob = r.PostFormValue("dob")
|
||||||
|
|
||||||
|
birthdate, err := time.Parse("2006-01-02", dob)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Incorrect format for birthdate; should be in yyyy-mm-dd format but got: %s", dob)
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate birthdate is at least age 18.
|
||||||
|
if utility.Age(birthdate) < 18 {
|
||||||
|
// Lock their account and notify the admins.
|
||||||
|
fb := &models.Feedback{
|
||||||
|
Intent: "report",
|
||||||
|
Subject: "Age Gate has auto-banned a user account",
|
||||||
|
TableName: "users",
|
||||||
|
TableID: user.ID,
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"The user **%s** (id:%d) has seen the Age Gate page and entered their birthdate which was under 18 years old (their entry: %s, %d years old), and their account has been banned automatically.",
|
||||||
|
user.Username, user.ID,
|
||||||
|
birthdate.Format("2006-01-02"), utility.Age(birthdate),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.CreateFeedback(fb); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't create admin notification: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.FlashError(w, r,
|
||||||
|
"You must be 18 years old to use this site and you have entered a birthdate that looks to be %d. "+
|
||||||
|
"If this was done by mistake, please contact support to resolve this issue. In the meantime, your "+
|
||||||
|
"account will be locked and you have been logged out.",
|
||||||
|
utility.Age(birthdate),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ban the account now.
|
||||||
|
user.Status = models.UserStatusBanned
|
||||||
|
if err := user.Save(); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't save update to your user account!")
|
||||||
|
}
|
||||||
|
|
||||||
|
session.LogoutUser(w, r)
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Birthdate = birthdate
|
||||||
|
|
||||||
|
if err := user.Save(); err != nil {
|
||||||
|
session.FlashError(w, r, "Failed to save user to database: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Flash(w, r, "Thank you for entering your birthdate!")
|
||||||
|
|
||||||
|
templates.Redirect(w, "/me")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
nm "net/mail"
|
nm "net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"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/log"
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
"code.nonshy.com/nonshy/website/pkg/redis"
|
"code.nonshy.com/nonshy/website/pkg/redis"
|
||||||
"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"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -77,6 +79,7 @@ func Signup() http.HandlerFunc {
|
||||||
username = strings.TrimSpace(strings.ToLower(r.PostFormValue("username")))
|
username = strings.TrimSpace(strings.ToLower(r.PostFormValue("username")))
|
||||||
password = strings.TrimSpace(r.PostFormValue("password"))
|
password = strings.TrimSpace(r.PostFormValue("password"))
|
||||||
password2 = strings.TrimSpace(r.PostFormValue("password2"))
|
password2 = strings.TrimSpace(r.PostFormValue("password2"))
|
||||||
|
dob = r.PostFormValue("dob")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Don't let them sneakily change their verified email address on us.
|
// Don't let them sneakily change their verified email address on us.
|
||||||
|
@ -157,6 +160,29 @@ func Signup() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DOB check.
|
||||||
|
birthdate, err := time.Parse("2006-01-02", dob)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Incorrect format for birthdate; should be in yyyy-mm-dd format but got: %s", dob)
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// Validate birthdate is at least age 18.
|
||||||
|
if utility.Age(birthdate) < 18 {
|
||||||
|
session.FlashError(w, r, "You must be at least 18 years old to use this site.")
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
|
||||||
|
// Burn the signup token.
|
||||||
|
if token.Token != "" {
|
||||||
|
if err := token.Delete(); err != nil {
|
||||||
|
log.Error("SignupToken.Delete(%s): %s", token.Token, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Full sign-up step (w/ email verification token), validate more things.
|
// Full sign-up step (w/ email verification token), validate more things.
|
||||||
var hasError bool
|
var hasError bool
|
||||||
if len(password) < 3 {
|
if len(password) < 3 {
|
||||||
|
@ -187,6 +213,10 @@ func Signup() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Put their birthdate in.
|
||||||
|
user.Birthdate = birthdate
|
||||||
|
user.Save()
|
||||||
|
|
||||||
// Log in the user and send them to their dashboard.
|
// Log in the user and send them to their dashboard.
|
||||||
session.LoginUser(w, r, user)
|
session.LoginUser(w, r, user)
|
||||||
templates.Redirect(w, "/me")
|
templates.Redirect(w, "/me")
|
||||||
|
|
45
pkg/middleware/age_gate.go
Normal file
45
pkg/middleware/age_gate.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/utility"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgeGate: part of LoginRequired that verifies the user has a birthdate on file.
|
||||||
|
func AgeGate(user *models.User, w http.ResponseWriter, r *http.Request) (handled bool) {
|
||||||
|
// Whitelisted endpoints where we won't redirect them away
|
||||||
|
var whitelistedPaths = []string{
|
||||||
|
"/me",
|
||||||
|
"/settings",
|
||||||
|
"/messages",
|
||||||
|
"/friends",
|
||||||
|
"/u/",
|
||||||
|
"/photo/upload",
|
||||||
|
"/photo/certification",
|
||||||
|
"/photo/private",
|
||||||
|
"/photo/view",
|
||||||
|
"/photo/u/",
|
||||||
|
"/comments",
|
||||||
|
"/users/blocked",
|
||||||
|
"/users/block",
|
||||||
|
"/account/delete",
|
||||||
|
"/v1/", // API endpoints like the Like buttons
|
||||||
|
}
|
||||||
|
for _, path := range whitelistedPaths {
|
||||||
|
if strings.HasPrefix(r.URL.Path, path) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User has no age set? Redirect them to the age gate prompt.
|
||||||
|
if user.Birthdate.IsZero() || utility.Age(user.Birthdate) < 18 {
|
||||||
|
templates.Redirect(w, "/settings/age-gate")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -48,6 +48,11 @@ func LoginRequired(handler http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ask the user for their birthdate?
|
||||||
|
if AgeGate(user, w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Stick the CurrentUser in the request context so future calls to session.CurrentUser can read it.
|
// Stick the CurrentUser in the request context so future calls to session.CurrentUser can read it.
|
||||||
ctx := context.WithValue(r.Context(), session.CurrentUserKey, user)
|
ctx := context.WithValue(r.Context(), session.CurrentUserKey, user)
|
||||||
handler.ServeHTTP(w, r.WithContext(ctx))
|
handler.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
@ -115,6 +120,11 @@ func CertRequired(handler http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ask the user for their birthdate?
|
||||||
|
if AgeGate(currentUser, w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
handler.ServeHTTP(w, r)
|
handler.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ func New() http.Handler {
|
||||||
// Login Required. Pages that non-certified users can access.
|
// Login Required. Pages that non-certified users can access.
|
||||||
mux.Handle("/me", middleware.LoginRequired(account.Dashboard()))
|
mux.Handle("/me", middleware.LoginRequired(account.Dashboard()))
|
||||||
mux.Handle("/settings", middleware.LoginRequired(account.Settings()))
|
mux.Handle("/settings", middleware.LoginRequired(account.Settings()))
|
||||||
|
mux.Handle("/settings/age-gate", middleware.LoginRequired(account.AgeGate()))
|
||||||
mux.Handle("/account/delete", middleware.LoginRequired(account.Delete()))
|
mux.Handle("/account/delete", middleware.LoginRequired(account.Delete()))
|
||||||
mux.Handle("/u/", account.Profile()) // public access OK
|
mux.Handle("/u/", account.Profile()) // public access OK
|
||||||
mux.Handle("/photo/upload", middleware.LoginRequired(photo.Upload()))
|
mux.Handle("/photo/upload", middleware.LoginRequired(photo.Upload()))
|
||||||
|
|
|
@ -4,6 +4,10 @@ abbr {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor-not-allowed {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* Container for large profile pic on user pages */
|
/* Container for large profile pic on user pages */
|
||||||
.profile-photo {
|
.profile-photo {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
|
80
web/templates/account/age_gate.html
Normal file
80
web/templates/account/age_gate.html
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
{{define "title"}}Please complete your profile{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container">
|
||||||
|
<section class="hero is-light is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">
|
||||||
|
<i class="fa fa-address-card mr-2"></i>
|
||||||
|
Please complete your profile
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="block p-4">
|
||||||
|
<div class="columns is-centered">
|
||||||
|
<div class="column is-half">
|
||||||
|
|
||||||
|
<div class="card" style="width: 100%; max-width: 640px">
|
||||||
|
<header class="card-header has-background-link">
|
||||||
|
<p class="card-header-title has-text-light">
|
||||||
|
<span class="icon"><i class="fa-regular fa-calendar mr-2"></i></span>
|
||||||
|
Your birthdate is requested
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content content">
|
||||||
|
|
||||||
|
<form action="/settings/age-gate" method="POST">
|
||||||
|
{{InputCSRF}}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To continue using {{PrettyTitle}}, please enter your date of birth below so we can
|
||||||
|
store it in your profile settings. Going forward, we are asking for this on all new
|
||||||
|
account signups but your account was created before we began doing so, and it is
|
||||||
|
needed now.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Your birthdate is <strong>not</strong> displayed to other members on this site, and
|
||||||
|
is used only to show your current age on your profile page.
|
||||||
|
<strong>Please enter your correct birthdate.</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="field block">
|
||||||
|
<label class="label" for="dob">Date of birth:</label>
|
||||||
|
<input type="date" class="input"
|
||||||
|
placeholder="password"
|
||||||
|
name="dob"
|
||||||
|
id="dob"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field has-text-centered">
|
||||||
|
<button type="submit" class="button is-success">
|
||||||
|
Save and continue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.addEventListener("DOMContentLoaded", (event) => {
|
||||||
|
let $file = document.querySelector("#file"),
|
||||||
|
$fileName = document.querySelector("#fileName");
|
||||||
|
|
||||||
|
$file.addEventListener("change", function() {
|
||||||
|
let file = this.files[0];
|
||||||
|
$fileName.innerHTML = file.name;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
|
@ -56,12 +56,17 @@
|
||||||
|
|
||||||
<div class="column field is-half">
|
<div class="column field is-half">
|
||||||
<label class="label" for="dob">Birthdate <i class="fa fa-lock"></i></label>
|
<label class="label" for="dob">Birthdate <i class="fa fa-lock"></i></label>
|
||||||
<input type="date" class="input"
|
<input type="date" class="input{{if not $User.Birthdate.IsZero}} cursor-not-allowed{{end}}"
|
||||||
id="dob"
|
id="dob"
|
||||||
name="dob"
|
name="dob"
|
||||||
value="{{if not $User.Birthdate.IsZero}}{{$User.Birthdate.Format "2006-01-02"}}{{end}}">
|
value="{{if not $User.Birthdate.IsZero}}{{$User.Birthdate.Format "2006-01-02"}}{{end}}"
|
||||||
|
required
|
||||||
|
{{if not $User.Birthdate.IsZero}}readonly{{end}}>
|
||||||
<p class="help">
|
<p class="help">
|
||||||
Used to show your age on your profile.
|
Used to show your age on your profile.
|
||||||
|
{{if not $User.Birthdate.IsZero}}
|
||||||
|
If you entered a wrong birthdate, <a href="/contact">contact</a> support to change it.
|
||||||
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -120,6 +120,18 @@
|
||||||
id="password2"
|
id="password2"
|
||||||
required>
|
required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="dob">Date of birth:</label>
|
||||||
|
<input type="date" class="input"
|
||||||
|
placeholder="password"
|
||||||
|
name="dob"
|
||||||
|
id="dob"
|
||||||
|
required>
|
||||||
|
<p class="help">
|
||||||
|
Your birthdate won't be shown to other members and is used to show
|
||||||
|
your current age on your profile. Please enter your correct birthdate.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user