Update auth flows

This commit is contained in:
Ben Visness 2022-08-13 14:15:00 -05:00
parent 4f1989f663
commit a0cc2f5c66
25 changed files with 1295 additions and 1140 deletions

File diff suppressed because it is too large Load Diff

View File

@ -216,6 +216,8 @@ pre, code, .codeblock {
--theme-color-dim: #444; --theme-color-dim: #444;
--theme-color-dimmer: #383838; --theme-color-dimmer: #383838;
--theme-color-dimmest: #333; --theme-color-dimmest: #333;
--theme-color-dark: #666;
--theme-color-light: #666;
--link-color: #aaa; --link-color: #aaa;
--link-border-color: #aaa; --link-border-color: #aaa;
--hr-color: #aaa; --hr-color: #aaa;

View File

@ -234,6 +234,8 @@ pre, code, .codeblock {
--theme-color-dim: #aaa; --theme-color-dim: #aaa;
--theme-color-dimmer: #bbb; --theme-color-dimmer: #bbb;
--theme-color-dimmest: #ccc; --theme-color-dimmest: #ccc;
--theme-color-dark: #666;
--theme-color-light: #666;
--link-color: #666; --link-color: #666;
--link-border-color: #666; --link-border-color: #666;
--hr-color: #444; --hr-color: #444;

View File

@ -286,7 +286,7 @@ func init() {
var err error var err error
switch emailType { switch emailType {
case "registration": case "registration":
err = email.SendRegistrationEmail(toAddress, toName, "test_user", "test_token", p) err = email.SendRegistrationEmail(toAddress, toName, "test_user", "test_token", "", p)
case "passwordreset": case "passwordreset":
err = email.SendPasswordReset(toAddress, toName, "test_user", "test_token", time.Now().Add(time.Hour*24), p) err = email.SendPasswordReset(toAddress, toName, "test_user", "test_token", time.Now().Add(time.Hour*24), p)
default: default:

View File

@ -26,14 +26,21 @@ type RegistrationEmailData struct {
CompleteRegistrationUrl string CompleteRegistrationUrl string
} }
func SendRegistrationEmail(toAddress string, toName string, username string, completionToken string, perf *perf.RequestPerf) error { func SendRegistrationEmail(
toAddress string,
toName string,
username string,
completionToken string,
destination string,
perf *perf.RequestPerf,
) error {
perf.StartBlock("EMAIL", "Registration email") perf.StartBlock("EMAIL", "Registration email")
perf.StartBlock("EMAIL", "Rendering template") perf.StartBlock("EMAIL", "Rendering template")
contents, err := renderTemplate("email_registration.html", RegistrationEmailData{ contents, err := renderTemplate("email_registration.html", RegistrationEmailData{
Name: toName, Name: toName,
HomepageUrl: hmnurl.BuildHomepage(), HomepageUrl: hmnurl.BuildHomepage(),
CompleteRegistrationUrl: hmnurl.BuildEmailConfirmation(username, completionToken), CompleteRegistrationUrl: hmnurl.BuildEmailConfirmation(username, completionToken, destination),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -28,7 +28,7 @@ var WRJ2021 = Jam{
var WRJ2022 = Jam{ var WRJ2022 = Jam{
Name: "Wheel Reinvention Jam 2022", Name: "Wheel Reinvention Jam 2022",
Slug: "WRJ2022", Slug: "WRJ2022",
StartTime: time.Date(2022, 8, 15, 8, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))), StartTime: time.Date(2022, 8, 3, 8, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
EndTime: time.Date(2022, 8, 22, 8, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))), EndTime: time.Date(2022, 8, 22, 8, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
} }

View File

@ -119,9 +119,13 @@ func BuildRegistrationSuccess() string {
var RegexEmailConfirmation = regexp.MustCompile("^/email_confirmation/(?P<username>[^/]+)/(?P<token>[^/]+)$") var RegexEmailConfirmation = regexp.MustCompile("^/email_confirmation/(?P<username>[^/]+)/(?P<token>[^/]+)$")
func BuildEmailConfirmation(username, token string) string { func BuildEmailConfirmation(username, token string, destination string) string {
defer CatchPanic() defer CatchPanic()
return Url(fmt.Sprintf("/email_confirmation/%s/%s", url.PathEscape(username), token), nil) var query []Q
if destination != "" {
query = append(query, Q{"destination", destination})
}
return Url(fmt.Sprintf("/email_confirmation/%s/%s", url.PathEscape(username), token), query)
} }
var RegexRequestPasswordReset = regexp.MustCompile("^/password_reset$") var RegexRequestPasswordReset = regexp.MustCompile("^/password_reset$")

View File

@ -163,8 +163,8 @@ input, select, textarea {
&:hover { &:hover {
@include usevar('color', 'form-button-color-active'); @include usevar('color', 'form-button-color-active');
@include usevar('background-color', 'theme-color-dim'); @include usevar('background-color', 'theme-color-light');
@include usevar('border-color', 'theme-color-dim'); @include usevar('border-color', 'theme-color-light');
} }
&.lite { &.lite {

View File

@ -43,6 +43,8 @@ $width-3: 4rem !default;
$width-4: 8rem !default; $width-4: 8rem !default;
$width-5: 16rem !default; $width-5: 16rem !default;
$width-6: 32rem !default; $width-6: 32rem !default;
$width-7: 48rem !default;
$width-8: 64rem !default;
$max-width-1: 1rem !default; $max-width-1: 1rem !default;
$max-width-2: 2rem !default; $max-width-2: 2rem !default;
$max-width-3: 4rem !default; $max-width-3: 4rem !default;

View File

@ -55,6 +55,8 @@
.w4 { width: $width-4; } .w4 { width: $width-4; }
.w5 { width: $width-5; } .w5 { width: $width-5; }
.w6 { width: $width-6; } .w6 { width: $width-6; }
.w7 { width: $width-7; }
.w8 { width: $width-8; }
.w-10 { width: 10%; } .w-10 { width: 10%; }
.w-20 { width: 20%; } .w-20 { width: 20%; }
@ -82,6 +84,8 @@
.w4-ns { width: $width-4; } .w4-ns { width: $width-4; }
.w5-ns { width: $width-5; } .w5-ns { width: $width-5; }
.w6-ns { width: $width-6; } .w6-ns { width: $width-6; }
.w7-ns { width: $width-7; }
.w8-ns { width: $width-8; }
.w-10-ns { width: 10%; } .w-10-ns { width: 10%; }
.w-20-ns { width: 20%; } .w-20-ns { width: 20%; }
.w-25-ns { width: 25%; } .w-25-ns { width: 25%; }
@ -108,6 +112,8 @@
.w4-m { width: $width-4; } .w4-m { width: $width-4; }
.w5-m { width: $width-5; } .w5-m { width: $width-5; }
.w6-m { width: $width-6; } .w6-m { width: $width-6; }
.w7-m { width: $width-7; }
.w8-m { width: $width-8; }
.w-10-m { width: 10%; } .w-10-m { width: 10%; }
.w-20-m { width: 20%; } .w-20-m { width: 20%; }
.w-25-m { width: 25%; } .w-25-m { width: 25%; }
@ -134,6 +140,8 @@
.w4-l { width: $width-4; } .w4-l { width: $width-4; }
.w5-l { width: $width-5; } .w5-l { width: $width-5; }
.w6-l { width: $width-6; } .w6-l { width: $width-6; }
.w7-l { width: $width-7; }
.w8-l { width: $width-8; }
.w-10-l { width: 10%; } .w-10-l { width: 10%; }
.w-20-l { width: 20%; } .w-20-l { width: 20%; }
.w-25-l { width: 25%; } .w-25-l { width: 25%; }

View File

@ -9,6 +9,9 @@ $vars: (
theme-color-dimmer: #383838, theme-color-dimmer: #383838,
theme-color-dimmest: #333, theme-color-dimmest: #333,
theme-color-dark: #666,
theme-color-light: #666,
link-color: #aaa, link-color: #aaa,
link-border-color: #aaa, link-border-color: #aaa,

View File

@ -9,6 +9,9 @@ $vars: (
theme-color-dimmer: #bbb, theme-color-dimmer: #bbb,
theme-color-dimmest: #ccc, theme-color-dimmest: #ccc,
theme-color-dark: #666,
theme-color-light: #666,
link-color: #666, link-color: #666,
link-border-color: #666, link-border-color: #666,

View File

@ -1,41 +1,38 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "content" }} {{ define "content" }}
<div class="content-block"> <div class="flex ph3 ph0-ns justify-center">
<form id="password_reset_form" method="post"> <div class="w-100 w-auto-ns pv3">
<h1>Hi, {{ .Username }}!</h1>
<form method="post" class="flex flex-column">
<input type="hidden" name="token" value="{{ .Token }}" /> <input type="hidden" name="token" value="{{ .Token }}" />
{{/*NOTE: The username field isn't `type="hidden"` because this way browser will offer to save the username along with the password */}} {{/*NOTE: The username field isn't `type="hidden"` because this way browser will offer to save the username along with the password */}}
<input style="position:absolute; visibility:hidden;" type="text" name="username" value="{{ .Username }}" readonly /> <input
style="position:absolute; visibility:hidden;"
type="text"
name="username"
value="{{ .Username }}"
readonly
/>
<h1>Hi, {{ .Username }}!</h1> <div>Please enter a new password.</div>
<p class="mb3 b">Please enter a new password</p>
<p class="mb2">
<label class="db b" for="password">New Password</label>
<input class="db" type="password" name="password" minlength="8" required />
</p>
<p class="mb2"> <div class="mt2">
<label class="db b" for="password2">New password confirmation</label> <label class="db b" for="password">New password</label>
<input class="db" type="password" name="password2" minlength="8" required /> <input class="db w-100 w5-ns"
<span class="note db">Enter the same password as before, for verification.</span> name="password"
</p> type="password"
minlength="8"
required
/>
</div>
<input class="db mt3" type="submit" value="Reset your password" /> <input class="db mt3 w-100"
type="submit"
value="Reset your password"
/>
</form> </form>
</div> </div>
<script> </div>
let form = document.querySelector("#password_reset_form")
let password1 = form.querySelector("input[name=password]")
let password2 = form.querySelector("input[name=password2]")
password1.addEventListener("input", validatePasswordConfirmation);
password2.addEventListener("input", validatePasswordConfirmation);
function validatePasswordConfirmation(ev) {
if (password1.value != password2.value) {
password2.setCustomValidity("Password doesn't match");
} else {
password2.setCustomValidity("");
}
}
</script>
{{ end }} {{ end }}

View File

@ -1,19 +1,39 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "content" }} {{ define "content" }}
<div class="content-block"> <div class="mw7 ph3 ph0-ns pb3">
<form method="POST"> <form method="POST">
<input type="hidden" name="token" value="{{ .Token }}" /> <input type="hidden" name="token" value="{{ .Token }}" />
{{/*NOTE: The username field isn't `type="hidden"` because this way browser will offer to save the username along with the password */}} <input type="hidden" name="destination" value="{{ .DestinationURL }}" />
<input style="position:absolute; visibility:hidden;" type="text" name="username" value="{{ .Username }}" readonly />
<h1>Hi, {{ .Username }}!</h1> {{/*NOTE: The username field isn't `type="hidden"` because this way browser will offer to save the username along with the password */}}
<p class="mb3 b">You're almost done signing up.</p> <input
<p>To complete your registration and log in, please enter the password you used during the registration process.</p> style="position:absolute; visibility:hidden;"
<label class="db b" for="password">Password</label> name="username"
<input class="db" type="password" name="password" minlength="8" required /> type="text"
value="{{ .Username }}"
readonly
/>
<input class="db mt3" type="submit" value="Complete registration" /> <h1>Hi, {{ .Username }}!</h1>
</form> <p class="mb3 b">
</div> You're almost done signing up.
</p>
<p>
To complete your registration and log in, please enter the password you used during the registration process.
</p>
<label class="db b" for="password">Password</label>
<input class="db w-100 w-auto-ns"
name="password"
type="password"
minlength="8"
required
/>
<input class="db mt3 w-100 w-auto-ns"
type="submit"
value="Complete registration"
/>
</form>
</div>
{{ end }} {{ end }}

View File

@ -1,25 +1,43 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "content" }} {{ define "content" }}
<div class="content-block"> <div class="flex ph3 ph0-ns justify-center">
<h1>Please enter your username and password</h1> <div class="w-100 w-auto-ns pv3">
<form method="POST"> <h1 class="tc">Log in</h1>
<form method="POST" class="flex flex-column">
<input type="hidden" name="redirect" value="{{ .RedirectUrl }}" /> <input type="hidden" name="redirect" value="{{ .RedirectUrl }}" />
<p class="mb2">
<label class="db b" for="username">Username</label>
<input class="db w5" name="username" minlength="3" maxlength="30" type="text" required />
</p>
<p class="mb2">
<label class="db b" for="password">Password</label>
<input class="db w5" name="password" minlength="8" type="password" required />
</p>
<a href="{{ .ForgotPasswordUrl }}">Forgot your password?</a>
<p class="mt3"> <div>
<input type="submit" value="Log in" /> <label class="db b" for="username">Username</label>
</p> <input class="db w-100 w5-ns"
name="username"
type="text"
minlength="3" maxlength="30"
required
/>
</div>
<div class="mt2">
<div class="flex justify-between">
<label class="db b" for="password">Password</label>
<a href="{{ .ForgotPasswordUrl }}" tabindex="-1">Forgot your password?</a>
</div>
<input class="db w-100 w5-ns"
name="password"
type="password"
minlength="8"
required
/>
</div>
<div class="mt3">
<input class="w-100" type="submit" value="Log in" />
</div>
<div class="tc pa3">
Need an account? <a href="{{ .RegisterUrl }}">Sign up.</a>
</div>
</form> </form>
</div> </div>
</div>
{{ end }} {{ end }}

View File

@ -1,26 +1,40 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "content" }} {{ define "content" }}
<div class="content-block"> <div class="flex ph3 ph0-ns justify-center">
<h1>Request a password reset link</h1> <div class="w-100 w-auto-ns pv3">
<form method="POST"> <h1 class="tc">Reset your password</h1>
<p class="mb2"> <form method="POST" class="flex flex-column">
<div>
<label class="db b" for="username">Username</label> <label class="db b" for="username">Username</label>
<input class="db w5" name="username" minlength="3" maxlength="30" type="text" required pattern="^[0-9a-zA-Z][\w-]{2,29}$" /> <input class="db w-100 w5-ns"
</p> name="username"
type="text"
minlength="3" maxlength="30"
required
/>
</div>
<p class="mb2"> <div class="mt2">
<label class="db b" for="email">Email</label> <label class="db b" for="email">Email</label>
<input class="db w5" name="email" type="text" required /> <input class="db w-100 w5-ns"
</p> name="email"
type="text"
required
/>
</div>
<p class="mt3"> <div class="mt3">
<input type="submit" value="Request password reset link" /> <input class="w-100"
</p> type="submit"
value="Request password reset"
/>
</div>
<p class="note mt3"> <div class="note mt3 mw-none mw5-ns">
Note: To avoid your being spammed with password reset links, we limit the number of requests per account every 24 hours. Note: To avoid spamming you with password reset links, we limit the number of requests per account every 24 hours.
</p> </div>
</form> </form>
</div> </div>
</div>
{{ end }} {{ end }}

View File

@ -1,9 +1,13 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "content" }} {{ define "content" }}
<div class="content-block"> <div class="mw6 ph3 ph0-ns pb3">
<h4>A password reset link was sent. It'll expire in 24 hours.</h4> <h1>A password reset link was sent.</h1>
<p>If for some reason the email shouldn't arrive in a timely fashion, and it also doesn't show up in your spam trap, please <a href="{{ .ContactUsUrl }}">contact the staff</a>.</p> <p>
<p><small>* Security best practices prevent us from disclosing whether or not the username and email combination was actually valid. An email may or may not have been sent depending on whether it was or wasn't.</small></p> It will expire in 24 hours.
</div> </p>
<p>
If for some reason the email doesn't arrive in a timely fashion, and it also doesn't show up in your spam, please <a href="{{ .ContactUsUrl }}">contact the staff</a>.
</p>
</div>
{{ end }} {{ end }}

View File

@ -1,58 +1,58 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "content" }} {{ define "content" }}
<div class="content-block"> <div class="flex ph3 ph0-ns justify-center">
<form id="register_form" method="post"> <div class="w-100 w-auto-ns pv3">
{{/* NOTE(asaf): No CSRF on register. We don't have a user session yet and I don't think we would gain anything from a pre-login session here */}} <h1 class="tc">Sign up</h1>
<form method="POST" class="flex flex-column">
{{/* NOTE(asaf): No CSRF on register. We don't have a user session yet and I don't think we would gain anything from a pre-login session here */}}
<input type="hidden" name="destination" value="{{ .DestinationURL }}" />
<p class="mb2"> <div>
<label class="db b" for="username">Username</label> <label class="db b" for="username">Username</label>
<input class="db w5" name="username" minlength="3" maxlength="30" type="text" required pattern="^[0-9a-zA-Z][\w-]{2,29}$" /> <input class="db w-100 w5-ns"
<span class="note db">Required. You may use up to 30 characters. Must start with a letter or number. Dashes and underscores are allowed.</span> name="username"
</p> type="text"
minlength="3" maxlength="30"
pattern="^[0-9a-zA-Z][\w-]{2,29}$"
required
/>
</div>
<p class="mb2"> <div class="mt2">
<label class="db b" for="displayname">Display name</label> <label class="db" for="displayname"><b>Display name</b> <span class="c--dim i">(optional)</span></label>
<input class="db w5" name="displayname" type="text" /> <input class="db w-100 w5-ns"
<span class="note db">Optional.</span> name="displayname"
</p> type="text"
/>
</div>
<p class="mb2"> <div class="mt2">
<label class="db b" for="email">Email</label> <label class="db b" for="email">Email</label>
<input class="db w5" name="email" type="email" required /> <input class="db w-100 w5-ns"
</p> name="email"
type="email"
required
/>
</div>
<p class="mb2"> <div class="mt2">
<label class="db b" for="password">Password</label> <label class="db b" for="password">Password</label>
<input class="db w5" name="password" minlength="8" type="password" required /> <input class="db w-100 w5-ns"
</p> name="password"
type="password"
minlength="8"
required
/>
</div>
{{/* TODO(asaf): Consider adding some bot-mitigation thing here */}}
<p class="mb2"> <div class="mt3">
<label class="db b" for="password2">Password confirmation</label> <input class="w-100" type="submit" value="Register" />
<input class="db w5" name="password2" minlength="8" type="password" required /> </div>
<span class="note db">Enter the same password as before, for verification.</span> </form>
</p> </div>
{{/* TODO(asaf): Consider adding some bot-mitigation thing here */}}
<p>
<input type="submit" value="Register" />
</p>
</form>
<script>
let form = document.querySelector("#register_form")
let password1 = form.querySelector("input[name=password]")
let password2 = form.querySelector("input[name=password2]")
password1.addEventListener("input", validatePasswordConfirmation);
password2.addEventListener("input", validatePasswordConfirmation);
function validatePasswordConfirmation(ev) {
if (password1.value != password2.value) {
password2.setCustomValidity("Password doesn't match");
} else {
password2.setCustomValidity("");
}
}
</script>
</div> </div>
{{ end }} {{ end }}

View File

@ -1,11 +1,16 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "content" }} {{ define "content" }}
<div class="content-block"> <div class="mw7 ph3 ph0-ns">
<h1>Hi! You're almost done signing up.</h1> <h1>Verify your email</h1>
<p class="mb3 b">We've sent you an email with a confirmation link. Please follow it to complete the registration process.</p> <p class="mb3 b">
<p>If for some reason the email doesn't arrive in a timely fashion and you also can't find it in your spam trap,<br /> We've sent you an email with a confirmation link. Please follow it to complete the registration process.
you should feel free to <a href="{{ .ContactUsUrl }}">contact the staff</a> and ask us to activate you manually.<br /> </p>
You'll want to tell us the username you chose and preferably email us from the same address you used to sign up.</p> <p>
If for some reason the email doesn't arrive in a timely fashion and you also can't find it in your spam trap, you should feel free to <a href="{{ .ContactUsUrl }}">contact the staff</a> and ask us to activate you manually.
</p>
<p>
Please tell us your username, and ideally email us from the same address you used to sign up.
</p>
</div> </div>
{{ end }} {{ end }}

View File

@ -1,12 +1,24 @@
<p>Hello, {{ .Name }}</p> <p>
<p>Someone has requested a password reset for your account.<br /> Hello {{ .Name }},
We hope it was you. If you didn't request it, we apologise. Kindly ignore this message.</p> </p>
<p>To finish the password reset, visit: <a href="{{ .DoPasswordResetUrl }}"><b>{{ .DoPasswordResetUrl }}</b></a><br /> <p>
This link will be valid for 24 hours (until {{ absolutedate .Expiration }} UTC)</p> Someone has requested a password reset for your Handmade Network account. To finish the password reset, visit the following link:
<p>Thanks,<br /> </p>
The Handmade Network staff.</p> <p>
<a href="{{ .DoPasswordResetUrl }}">{{ .DoPasswordResetUrl }}</a>
</p>
<p>
This link will be valid for 24 hours (until {{ absolutedate .Expiration }} UTC).
</p>
<p>
If you did not request this password reset, we apologize - please ignore this message.
</p>
<p>
Thanks,<br />
The Handmade Network staff.
</p>
<hr /> <hr />
<p style="font-size:small; -webkit-text-size-adjust:none ;color: #666"> <p style="font-size:small; -webkit-text-size-adjust:none; color: #666">
You are receiving this email because someone requested a password reset for your account and supplied both your username and email address correctly. You are receiving this email because someone requested a password reset for your account and supplied both your username and email address correctly.
</p> </p>

View File

@ -1,11 +1,16 @@
<p>Hello, {{ .Name }}</p> <p>
<p>Welcome to <a href="{{ .HomepageUrl }}"><b>Handmade Network</b></a>.<br /> Hello {{ .Name }} - welcome to <a href="{{ .HomepageUrl }}"><b>Handmade Network</b></a>!
To complete the registration process, please use the following link: <a href="{{ .CompleteRegistrationUrl }}"><b>{{ .CompleteRegistrationUrl }}</b></a>.</p> </p>
<p>
To complete the registration process, please use the following link:
</p>
<p>
<a href="{{ .CompleteRegistrationUrl }}">{{ .CompleteRegistrationUrl }}</a>.
</p>
<p>Thanks,<br /> <p>Thanks,<br />
The Handmade Network staff.</p> The Handmade Network staff.</p>
<hr /> <hr />
<p style="font-size:small; -webkit-text-size-adjust:none ;color: #666"> <p style="font-size:small; -webkit-text-size-adjust:none; color: #666">
You are receiving this email because someone registered with your email address at <a href="{{ .HomepageUrl }}">Handmade.Network</a>.<br /> You are receiving this email because someone registered with your email address at <a href="{{ .HomepageUrl }}">handmade.network</a>. If that wasn't you, kindly ignore this email. If you do not complete the registration, your information will be deleted from our servers after 7 days.
If that wasn't you, kindly ignore this email. If you do not complete the registration, your information will be deleted from our servers after 7 days.
</p> </p>

View File

@ -5,8 +5,9 @@
{{ $themeDimmer := eq .Theme "dark" | ternary (lightness 0.3 $c) (lightness 0.8 $c) | color2css }} {{ $themeDimmer := eq .Theme "dark" | ternary (lightness 0.3 $c) (lightness 0.8 $c) | color2css }}
{{ $themeDimmest := eq .Theme "dark" | ternary (lightness 0.2 $c) (lightness 0.85 $c) | color2css }} {{ $themeDimmest := eq .Theme "dark" | ternary (lightness 0.2 $c) (lightness 0.85 $c) | color2css }}
{{/* Theme "dark" colors are always darker in value, regardless of light or dark theme. */}} {{/* Theme "dark" and "light" colors are always darker or lighter in value, regardless of theme. */}}
{{ $themeDark := eq .Theme "dark" | ternary (lightness 0.30 $c) (lightness 0.35 $c) | color2css }} {{ $themeDark := eq .Theme "dark" | ternary (lightness 0.30 $c) (lightness 0.35 $c) | color2css }}
{{ $themeLight := eq .Theme "dark" | ternary (lightness 0.55 $c) (lightness 0.55 $c) | color2css }}
{{ $linkColor := eq .Theme "dark" | ternary (lightness 0.55 $c) (lightness 0.35 $c) | color2css }} {{ $linkColor := eq .Theme "dark" | ternary (lightness 0.55 $c) (lightness 0.35 $c) | color2css }}
{{ $linkHoverColor := eq .Theme "dark" | ternary (lightness 0.65 $c) (lightness 0.45 $c) | color2css }} {{ $linkHoverColor := eq .Theme "dark" | ternary (lightness 0.65 $c) (lightness 0.45 $c) | color2css }}
@ -18,6 +19,7 @@
--theme-color-dimmest: {{ $themeDimmest }}; --theme-color-dimmest: {{ $themeDimmest }};
--theme-color-dark: {{ $themeDark }}; --theme-color-dark: {{ $themeDark }};
--theme-color-light: {{ $themeLight }};
--link-color: {{ $linkColor }}; --link-color: {{ $linkColor }};
--link-hover-color: {{ $linkHoverColor }}; --link-hover-color: {{ $linkHoverColor }};
@ -89,6 +91,11 @@ a:hover, button:hover, .button:hover, input[type=button]:hover {
background-color: var(--theme-color-dark); background-color: var(--theme-color-dark);
} }
.bg-theme-light {
background-color: {{ $themeLight }};
background-color: var(--theme-color-light);
}
.b--theme { .b--theme {
border-color: {{ $c | color2css }}; border-color: {{ $c | color2css }};
border-color: var(--theme-color); border-color: var(--theme-color);
@ -114,6 +121,11 @@ a:hover, button:hover, .button:hover, input[type=button]:hover {
border-color: var(--theme-color-dark); border-color: var(--theme-color-dark);
} }
.b--theme-light {
border-color: {{ $themeLight }};
border-color: var(--theme-color-light);
}
:root { :root {
--background-even-background: {{ .PostBgColor }}; --background-even-background: {{ .PostBgColor }};
} }

View File

@ -90,19 +90,13 @@
<div class="edit-form-row"> <div class="edit-form-row">
<div class="pt-input-ns">New password:</div> <div class="pt-input-ns">New password:</div>
<div> <div>
<input id="id_new_password1" name="new_password1" type="password" /> <input name="new_password" type="password" />
<div class="c--dim f7 mw6"> <div class="c--dim f7 mw6">
Your password must be 8 or more characters, and must differ from your username and current password. Your password must be 8 or more characters, and must differ from your username and current password.
Other than that, <a href="http://krebsonsecurity.com/password-dos-and-donts/" class="external" target="_blank">please follow best practices</a>. Other than that, <a href="http://krebsonsecurity.com/password-dos-and-donts/" class="external" target="_blank">please follow best practices</a>.
</div> </div>
</div> </div>
</div> </div>
<div class="edit-form-row">
<div class="pt-input-ns">New password confirmation:</div>
<div>
<input id="id_new_password2" name="new_password2" type="password" />
</div>
</div>
<div class="edit-form-row"> <div class="edit-form-row">
<div></div> <div></div>
<div> <div>

View File

@ -9,6 +9,7 @@ import (
"time" "time"
"git.handmade.network/hmn/hmn/src/auth" "git.handmade.network/hmn/hmn/src/auth"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db" "git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/email" "git.handmade.network/hmn/hmn/src/email"
"git.handmade.network/hmn/hmn/src/hmnurl" "git.handmade.network/hmn/hmn/src/hmnurl"
@ -23,6 +24,7 @@ var UsernameRegex = regexp.MustCompile(`^[0-9a-zA-Z][\w-]{2,29}$`)
type LoginPageData struct { type LoginPageData struct {
templates.BaseData templates.BaseData
RedirectUrl string RedirectUrl string
RegisterUrl string
ForgotPasswordUrl string ForgotPasswordUrl string
} }
@ -33,8 +35,9 @@ func LoginPage(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_login.html", LoginPageData{ res.MustWriteTemplate("auth_login.html", LoginPageData{
BaseData: getBaseDataAutocrumb(c, "Log in"), BaseData: getBaseData(c, "Log in", nil),
RedirectUrl: c.Req.URL.Query().Get("redirect"), RedirectUrl: c.Req.URL.Query().Get("redirect"),
RegisterUrl: hmnurl.BuildRegister(),
ForgotPasswordUrl: hmnurl.BuildRequestPasswordReset(), ForgotPasswordUrl: hmnurl.BuildRequestPasswordReset(),
}, c.Perf) }, c.Perf)
return res return res
@ -47,12 +50,14 @@ func Login(c *RequestContext) ResponseData {
} }
redirect := form.Get("redirect") redirect := form.Get("redirect")
if redirect == "" {
redirect = "/" destination := hmnurl.BuildHomepage()
if redirect != "" && urlIsLocal(redirect) {
destination = redirect
} }
if c.CurrentUser != nil { if c.CurrentUser != nil {
res := c.Redirect(redirect, http.StatusSeeOther) res := c.Redirect(destination, http.StatusSeeOther)
res.AddFutureNotice("warn", fmt.Sprintf("You are already logged in as %s.", c.CurrentUser.Username)) res.AddFutureNotice("warn", fmt.Sprintf("You are already logged in as %s.", c.CurrentUser.Username))
return res return res
} }
@ -65,7 +70,7 @@ func Login(c *RequestContext) ResponseData {
showLoginWithFailure := func(c *RequestContext, redirect string) ResponseData { showLoginWithFailure := func(c *RequestContext, redirect string) ResponseData {
var res ResponseData var res ResponseData
baseData := getBaseDataAutocrumb(c, "Log in") baseData := getBaseData(c, "Log in", nil)
baseData.AddImmediateNotice("failure", "Incorrect username or password") baseData.AddImmediateNotice("failure", "Incorrect username or password")
res.MustWriteTemplate("auth_login.html", LoginPageData{ res.MustWriteTemplate("auth_login.html", LoginPageData{
BaseData: baseData, BaseData: baseData,
@ -105,7 +110,7 @@ func Login(c *RequestContext) ResponseData {
return c.RejectRequest("You must validate your email address before logging in. You should've received an email shortly after registration. If you did not receive the email, please contact the staff.") return c.RejectRequest("You must validate your email address before logging in. You should've received an email shortly after registration. If you did not receive the email, please contact the staff.")
} }
res := c.Redirect(redirect, http.StatusSeeOther) res := c.Redirect(destination, http.StatusSeeOther)
err = loginUser(c, user, &res) err = loginUser(c, user, &res)
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err) return c.ErrorResponse(http.StatusInternalServerError, err)
@ -114,12 +119,14 @@ func Login(c *RequestContext) ResponseData {
} }
func Logout(c *RequestContext) ResponseData { func Logout(c *RequestContext) ResponseData {
redir := c.Req.URL.Query().Get("redirect") redirect := c.Req.URL.Query().Get("redirect")
if redir == "" {
redir = "/" destination := hmnurl.BuildHomepage()
if redirect != "" && urlIsLocal(redirect) {
destination = redirect
} }
res := c.Redirect(redir, http.StatusSeeOther) res := c.Redirect(destination, http.StatusSeeOther)
logoutUser(c, &res) logoutUser(c, &res)
return res return res
} }
@ -128,9 +135,21 @@ func RegisterNewUser(c *RequestContext) ResponseData {
if c.CurrentUser != nil { if c.CurrentUser != nil {
c.Redirect(hmnurl.BuildUserSettings(c.CurrentUser.Username), http.StatusSeeOther) c.Redirect(hmnurl.BuildUserSettings(c.CurrentUser.Username), http.StatusSeeOther)
} }
// TODO(asaf): Do something to prevent bot registration // TODO(asaf): Do something to prevent bot registration
type RegisterPageData struct {
templates.BaseData
DestinationURL string
}
tmpl := RegisterPageData{
BaseData: getBaseData(c, "Register", nil),
DestinationURL: c.Req.URL.Query().Get("destination"),
}
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_register.html", getBaseDataAutocrumb(c, "Register"), c.Perf) res.MustWriteTemplate("auth_register.html", tmpl, c.Perf)
return res return res
} }
@ -144,7 +163,7 @@ func RegisterNewUserSubmit(c *RequestContext) ResponseData {
displayName := strings.TrimSpace(c.Req.Form.Get("displayname")) displayName := strings.TrimSpace(c.Req.Form.Get("displayname"))
emailAddress := strings.TrimSpace(c.Req.Form.Get("email")) emailAddress := strings.TrimSpace(c.Req.Form.Get("email"))
password := c.Req.Form.Get("password") password := c.Req.Form.Get("password")
password2 := c.Req.Form.Get("password2") destination := strings.TrimSpace(c.Req.Form.Get("destination"))
if !UsernameRegex.Match([]byte(username)) { if !UsernameRegex.Match([]byte(username)) {
return c.RejectRequest("Invalid username") return c.RejectRequest("Invalid username")
} }
@ -154,9 +173,6 @@ func RegisterNewUserSubmit(c *RequestContext) ResponseData {
if len(password) < 8 { if len(password) < 8 {
return c.RejectRequest("Password too short") return c.RejectRequest("Password too short")
} }
if password != password2 {
return c.RejectRequest("Password confirmation doesn't match password")
}
c.Perf.StartBlock("SQL", "Check blacklist") c.Perf.StartBlock("SQL", "Check blacklist")
// TODO(asaf): Check email against blacklist // TODO(asaf): Check email against blacklist
@ -257,7 +273,14 @@ func RegisterNewUserSubmit(c *RequestContext) ResponseData {
if mailName == "" { if mailName == "" {
mailName = username mailName = username
} }
err = email.SendRegistrationEmail(emailAddress, mailName, username, ott, c.Perf) err = email.SendRegistrationEmail(
emailAddress,
mailName,
username,
ott,
destination,
c.Perf,
)
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to send registration email")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to send registration email"))
} }
@ -283,7 +306,7 @@ func RegisterNewUserSuccess(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_register_success.html", RegisterNewUserSuccessData{ res.MustWriteTemplate("auth_register_success.html", RegisterNewUserSuccessData{
BaseData: getBaseDataAutocrumb(c, "Register"), BaseData: getBaseData(c, "Register", nil),
ContactUsUrl: hmnurl.BuildContactPage(), ContactUsUrl: hmnurl.BuildContactPage(),
}, c.Perf) }, c.Perf)
return res return res
@ -291,8 +314,9 @@ func RegisterNewUserSuccess(c *RequestContext) ResponseData {
type EmailValidationData struct { type EmailValidationData struct {
templates.BaseData templates.BaseData
Token string Token string
Username string Username string
DestinationURL string
} }
func EmailConfirmation(c *RequestContext) ResponseData { func EmailConfirmation(c *RequestContext) ResponseData {
@ -329,9 +353,10 @@ func EmailConfirmation(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_email_validation.html", EmailValidationData{ res.MustWriteTemplate("auth_email_validation.html", EmailValidationData{
BaseData: getBaseDataAutocrumb(c, "Register"), BaseData: getBaseData(c, "Register", nil),
Token: token, Token: token,
Username: username, Username: username,
DestinationURL: c.Req.URL.Query().Get("destination"),
}, c.Perf) }, c.Perf)
return res return res
} }
@ -342,6 +367,7 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData {
token := c.Req.Form.Get("token") token := c.Req.Form.Get("token")
username := c.Req.Form.Get("username") username := c.Req.Form.Get("username")
password := c.Req.Form.Get("password") password := c.Req.Form.Get("password")
destination := c.Req.Form.Get("destination")
validationResult := validateUsernameAndToken(c, username, token, models.TokenTypeRegistration) validationResult := validateUsernameAndToken(c, username, token, models.TokenTypeRegistration)
if !validationResult.Match { if !validationResult.Match {
@ -354,7 +380,7 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData {
return c.ErrorResponse(http.StatusInternalServerError, err) return c.ErrorResponse(http.StatusInternalServerError, err)
} else if !success { } else if !success {
var res ResponseData var res ResponseData
baseData := getBaseDataAutocrumb(c, "Register") baseData := getBaseData(c, "Register", nil)
// NOTE(asaf): We can report that the password is incorrect, because an attacker wouldn't have a valid token to begin with. // NOTE(asaf): We can report that the password is incorrect, because an attacker wouldn't have a valid token to begin with.
baseData.AddImmediateNotice("failure", "Incorrect password. Please try again.") baseData.AddImmediateNotice("failure", "Incorrect password. Please try again.")
res.MustWriteTemplate("auth_email_validation.html", EmailValidationData{ res.MustWriteTemplate("auth_email_validation.html", EmailValidationData{
@ -401,7 +427,12 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData {
} }
c.Perf.EndBlock() c.Perf.EndBlock()
res := c.Redirect(hmnurl.BuildHomepage(), http.StatusSeeOther) redirect := hmnurl.BuildHomepage()
if destination != "" && urlIsLocal(destination) {
redirect = destination
}
res := c.Redirect(redirect, http.StatusSeeOther)
res.AddFutureNotice("success", "You've completed your registration successfully!") res.AddFutureNotice("success", "You've completed your registration successfully!")
err = loginUser(c, validationResult.User, &res) err = loginUser(c, validationResult.User, &res)
if err != nil { if err != nil {
@ -432,7 +463,7 @@ func RequestPasswordReset(c *RequestContext) ResponseData {
return c.Redirect(hmnurl.BuildHomepage(), http.StatusSeeOther) return c.Redirect(hmnurl.BuildHomepage(), http.StatusSeeOther)
} }
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_password_reset.html", getBaseDataAutocrumb(c, "Password Reset"), c.Perf) res.MustWriteTemplate("auth_password_reset.html", getBaseData(c, "Password Reset", nil), c.Perf)
return res return res
} }
@ -550,7 +581,7 @@ func PasswordResetSent(c *RequestContext) ResponseData {
} }
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_password_reset_sent.html", PasswordResetSentData{ res.MustWriteTemplate("auth_password_reset_sent.html", PasswordResetSentData{
BaseData: getBaseDataAutocrumb(c, "Password Reset"), BaseData: getBaseData(c, "Password Reset", nil),
ContactUsUrl: hmnurl.BuildContactPage(), ContactUsUrl: hmnurl.BuildContactPage(),
}, c.Perf) }, c.Perf)
return res return res
@ -584,7 +615,7 @@ func DoPasswordReset(c *RequestContext) ResponseData {
} }
res.MustWriteTemplate("auth_do_password_reset.html", DoPasswordResetData{ res.MustWriteTemplate("auth_do_password_reset.html", DoPasswordResetData{
BaseData: getBaseDataAutocrumb(c, "Password Reset"), BaseData: getBaseData(c, "Password Reset", nil),
Username: username, Username: username,
Token: token, Token: token,
}, c.Perf) }, c.Perf)
@ -597,7 +628,6 @@ func DoPasswordResetSubmit(c *RequestContext) ResponseData {
token := c.Req.Form.Get("token") token := c.Req.Form.Get("token")
username := c.Req.Form.Get("username") username := c.Req.Form.Get("username")
password := c.Req.Form.Get("password") password := c.Req.Form.Get("password")
password2 := c.Req.Form.Get("password2")
validationResult := validateUsernameAndToken(c, username, token, models.TokenTypePasswordReset) validationResult := validateUsernameAndToken(c, username, token, models.TokenTypePasswordReset)
if !validationResult.Match { if !validationResult.Match {
@ -611,9 +641,6 @@ func DoPasswordResetSubmit(c *RequestContext) ResponseData {
if len(password) < 8 { if len(password) < 8 {
return c.RejectRequest("Password too short") return c.RejectRequest("Password too short")
} }
if password != password2 {
return c.RejectRequest("Password confirmation doesn't match password")
}
hashed := auth.HashPassword(password) hashed := auth.HashPassword(password)
@ -806,3 +833,7 @@ func validateUsernameAndToken(c *RequestContext, username string, token string,
return result return result
} }
func urlIsLocal(url string) bool {
return strings.HasPrefix(url, config.Config.BaseUrl)
}

View File

@ -405,10 +405,9 @@ func UserSettingsSave(c *RequestContext) ResponseData {
// Update password // Update password
oldPassword := form.Get("old_password") oldPassword := form.Get("old_password")
newPassword := form.Get("new_password1") newPassword := form.Get("new_password")
newPasswordConfirmation := form.Get("new_password2")
if oldPassword != "" && newPassword != "" { if oldPassword != "" && newPassword != "" {
errorRes := updatePassword(c, tx, oldPassword, newPassword, newPasswordConfirmation) errorRes := updatePassword(c, tx, oldPassword, newPassword)
if errorRes != nil { if errorRes != nil {
return *errorRes return *errorRes
} }
@ -526,12 +525,7 @@ func UserProfileAdminNuke(c *RequestContext) ResponseData {
return res return res
} }
func updatePassword(c *RequestContext, tx pgx.Tx, old, new, confirm string) *ResponseData { func updatePassword(c *RequestContext, tx pgx.Tx, old, new string) *ResponseData {
if new != confirm {
res := c.RejectRequest("Your password and password confirmation did not match.")
return &res
}
oldHashedPassword, err := auth.ParsePasswordString(c.CurrentUser.Password) oldHashedPassword, err := auth.ParsePasswordString(c.CurrentUser.Password)
if err != nil { if err != nil {
c.Logger.Warn().Err(err).Msg("failed to parse user's password string") c.Logger.Warn().Err(err).Msg("failed to parse user's password string")