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"
|
||||
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")
|
||||
|
|
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.
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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;
|
||||
|
|
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">
|
||||
<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>
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue
Block a user