diff --git a/pkg/controller/account/age_gate.go b/pkg/controller/account/age_gate.go new file mode 100644 index 0000000..3518ad9 --- /dev/null +++ b/pkg/controller/account/age_gate.go @@ -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 + } + }) +} diff --git a/pkg/controller/account/signup.go b/pkg/controller/account/signup.go index 9a415f0..0607b9a 100644 --- a/pkg/controller/account/signup.go +++ b/pkg/controller/account/signup.go @@ -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") diff --git a/pkg/middleware/age_gate.go b/pkg/middleware/age_gate.go new file mode 100644 index 0000000..5deb4d0 --- /dev/null +++ b/pkg/middleware/age_gate.go @@ -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 +} diff --git a/pkg/middleware/authentication.go b/pkg/middleware/authentication.go index 63a34e5..bc20902 100644 --- a/pkg/middleware/authentication.go +++ b/pkg/middleware/authentication.go @@ -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) }) } diff --git a/pkg/router/router.go b/pkg/router/router.go index 5e418ed..57be1e8 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -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())) diff --git a/web/static/css/theme.css b/web/static/css/theme.css index f61aa59..4af4365 100644 --- a/web/static/css/theme.css +++ b/web/static/css/theme.css @@ -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; diff --git a/web/templates/account/age_gate.html b/web/templates/account/age_gate.html new file mode 100644 index 0000000..e7352a5 --- /dev/null +++ b/web/templates/account/age_gate.html @@ -0,0 +1,80 @@ +{{define "title"}}Please complete your profile{{end}} +{{define "content"}} +
+
+
+
+

+ + Please complete your profile +

+
+
+
+ +
+
+
+ +
+ +
+ +
+ {{InputCSRF}} + +

+ 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. +

+ +

+ Your birthdate is not displayed to other members on this site, and + is used only to show your current age on your profile page. + Please enter your correct birthdate. +

+ +
+ + +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ + +{{end}} \ No newline at end of file diff --git a/web/templates/account/settings.html b/web/templates/account/settings.html index f87ca19..fac80cf 100644 --- a/web/templates/account/settings.html +++ b/web/templates/account/settings.html @@ -56,12 +56,17 @@
- + value="{{if not $User.Birthdate.IsZero}}{{$User.Birthdate.Format "2006-01-02"}}{{end}}" + required + {{if not $User.Birthdate.IsZero}}readonly{{end}}>

Used to show your age on your profile. + {{if not $User.Birthdate.IsZero}} + If you entered a wrong birthdate, contact support to change it. + {{end}}

diff --git a/web/templates/account/signup.html b/web/templates/account/signup.html index 3fec865..99a82b0 100644 --- a/web/templates/account/signup.html +++ b/web/templates/account/signup.html @@ -120,6 +120,18 @@ id="password2" required> +
+ + +

+ 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. +

+
{{end}}