Age gate: collect birthdates retroactively and on new signup

face-detect
Noah Petherbridge 2023-06-15 21:38:09 -07:00
parent 3d34306c7e
commit d36e71549a
9 changed files with 285 additions and 2 deletions

View 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
}
})
}

View File

@ -5,6 +5,7 @@ import (
"net/http"
nm "net/mail"
"strings"
"time"
"code.nonshy.com/nonshy/website/pkg/config"
"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/session"
"code.nonshy.com/nonshy/website/pkg/templates"
"code.nonshy.com/nonshy/website/pkg/utility"
"github.com/google/uuid"
)
@ -77,6 +79,7 @@ func Signup() http.HandlerFunc {
username = strings.TrimSpace(strings.ToLower(r.PostFormValue("username")))
password = strings.TrimSpace(r.PostFormValue("password"))
password2 = strings.TrimSpace(r.PostFormValue("password2"))
dob = r.PostFormValue("dob")
)
// Don't let them sneakily change their verified email address on us.
@ -157,6 +160,29 @@ func Signup() http.HandlerFunc {
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.
var hasError bool
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.
session.LoginUser(w, r, user)
templates.Redirect(w, "/me")

View 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
}

View File

@ -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.
ctx := context.WithValue(r.Context(), session.CurrentUserKey, user)
handler.ServeHTTP(w, r.WithContext(ctx))
@ -115,6 +120,11 @@ func CertRequired(handler http.Handler) http.Handler {
return
}
// Ask the user for their birthdate?
if AgeGate(currentUser, w, r) {
return
}
handler.ServeHTTP(w, r)
})
}

View File

@ -41,6 +41,7 @@ func New() http.Handler {
// Login Required. Pages that non-certified users can access.
mux.Handle("/me", middleware.LoginRequired(account.Dashboard()))
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("/u/", account.Profile()) // public access OK
mux.Handle("/photo/upload", middleware.LoginRequired(photo.Upload()))

View File

@ -4,6 +4,10 @@ abbr {
cursor: help;
}
.cursor-not-allowed {
cursor: not-allowed;
}
/* Container for large profile pic on user pages */
.profile-photo {
width: 150px;

View 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}}

View File

@ -56,12 +56,17 @@
<div class="column field is-half">
<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"
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">
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>
</div>
</div>

View File

@ -120,6 +120,18 @@
id="password2"
required>
</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}}
<div class="field">