tangled
alpha
login
or
join now
futur.blue
/
pegasus
56
fork
atom
objective categorical abstract machine language personal data server
56
fork
atom
overview
issues
2
pulls
pipelines
Password reset flow
futur.blue
2 months ago
79452164
79e9bbd2
verified
This commit was signed with the committer's
known signature
.
futur.blue
SSH Key Fingerprint:
SHA256:QHGqHWNpqYyw9bt8KmPuJIyeZX9SZewBZ0PR1COtKQ0=
+247
-1
5 changed files
expand all
collapse all
unified
split
bin
main.ml
frontend
client
Router.mlx
src
templates
LoginPage.mlx
PasswordResetPage.mlx
pegasus
lib
api
account_
password_reset.ml
+2
bin/main.ml
···
78
78
; (post, "/account/migrate", Api.Account_.Migrate.post_handler)
79
79
; (post, "/account/switch", Api.Account_.Login.switch_account_handler)
80
80
; (get, "/account/logout", Api.Account_.Logout.handler)
81
81
+
; (get, "/account/reset", Api.Account_.Password_reset.get_handler)
82
82
+
; (post, "/account/reset", Api.Account_.Password_reset.post_handler)
81
83
; (* passkey management (authed) *)
82
84
(get, "/account/passkeys", Api.Account_.Passkeys.list_handler)
83
85
; ( get
+1
frontend/client/Router.mlx
···
16
16
[ {path= "/"; template= (module RootPage)}
17
17
; {path= "/oauth/authorize"; template= (module OauthAuthorizePage)}
18
18
; {path= "/account/login"; template= (module LoginPage)}
19
19
+
; {path= "/account/reset"; template= (module PasswordResetPage)}
19
20
; {path= "/account/signup"; template= (module SignupPage)}
20
21
; {path= "/account/migrate"; template= (module MigratePage)}
21
22
; {path= "/account"; template= (module AccountPage)}
+8
-1
frontend/src/templates/LoginPage.mlx
···
215
215
(string "create an account")
216
216
</a>
217
217
</li>
218
218
-
<li>
218
218
+
<li className="mb-1">
219
219
<a
220
220
className="text-mana-100 underline hover:text-mana-200"
221
221
href="/account/migrate">
222
222
(string "migrate from another PDS")
223
223
+
</a>
224
224
+
</li>
225
225
+
<li>
226
226
+
<a
227
227
+
className="text-mana-100 underline hover:text-mana-200"
228
228
+
href="/account/password-reset">
229
229
+
(string "reset your password")
223
230
</a>
224
231
</li>
225
232
</ul>
+132
frontend/src/templates/PasswordResetPage.mlx
···
1
1
+
[@@@ocaml.warning "-26-27"]
2
2
+
3
3
+
open Melange_json.Primitives
4
4
+
open React
5
5
+
6
6
+
type props =
7
7
+
{ csrf_token: string
8
8
+
; step: string [@default "request"] (* "request" | "reset" | "success" *)
9
9
+
; email_sent_to: string option [@default None]
10
10
+
; error: string option [@default None] }
11
11
+
[@@deriving json]
12
12
+
13
13
+
let[@react.component] make
14
14
+
~props:({csrf_token; step; email_sent_to; error} : props) () =
15
15
+
<main className="w-full h-auto max-w-xs px-4 sm:px-0 my-auto">
16
16
+
<h1 className="text-2xl font-serif text-mana-200 mb-2">
17
17
+
(string "reset password")
18
18
+
</h1>
19
19
+
( match step with
20
20
+
| "success" ->
21
21
+
<div>
22
22
+
<span className="w-full text-balance text-mist-100">
23
23
+
(string
24
24
+
"Your password has been reset successfully. You can now sign in \
25
25
+
with your new password." )
26
26
+
</span>
27
27
+
<div className="mt-4">
28
28
+
<a
29
29
+
className="text-mana-100 underline hover:text-mana-200"
30
30
+
href="/account/login">
31
31
+
(string "sign in")
32
32
+
</a>
33
33
+
</div>
34
34
+
</div>
35
35
+
| "reset" ->
36
36
+
<div>
37
37
+
<span className="w-full text-balance text-mist-100">
38
38
+
( match email_sent_to with
39
39
+
| Some email ->
40
40
+
string
41
41
+
( "A reset code has been sent to " ^ email
42
42
+
^ ". Enter the code and your new password below." )
43
43
+
| None ->
44
44
+
string
45
45
+
"Enter the reset code from your email and your new password."
46
46
+
)
47
47
+
</span>
48
48
+
<form className="w-full flex flex-col mt-4 mb-2 gap-y-2">
49
49
+
<input type_="hidden" name="dream.csrf" value=csrf_token />
50
50
+
<input type_="hidden" name="step" value="reset" />
51
51
+
<Input
52
52
+
sr_only=true
53
53
+
name="token"
54
54
+
type_="text"
55
55
+
label="reset code"
56
56
+
autoComplete="one-time-code"
57
57
+
/>
58
58
+
<Input
59
59
+
sr_only=true
60
60
+
name="password"
61
61
+
type_="password"
62
62
+
label="new password"
63
63
+
autoComplete="new-password"
64
64
+
/>
65
65
+
( match error with
66
66
+
| Some err ->
67
67
+
<span
68
68
+
className="inline-flex items-center text-phoenix-100 text-sm">
69
69
+
<CircleAlertIcon className="w-4 h-4 mr-2" /> (string err)
70
70
+
</span>
71
71
+
| None ->
72
72
+
null )
73
73
+
<Button type_="submit" formMethod="post" className="mt-2">
74
74
+
(string "reset password")
75
75
+
</Button>
76
76
+
</form>
77
77
+
<div className="mt-4">
78
78
+
<a
79
79
+
className="text-sm text-mana-100 hover:text-mana-200"
80
80
+
href="/account/password-reset">
81
81
+
(string "request a new code")
82
82
+
</a>
83
83
+
</div>
84
84
+
</div>
85
85
+
| _ ->
86
86
+
<div>
87
87
+
<span className="w-full text-balance text-mist-100">
88
88
+
(string
89
89
+
"Enter your email address to receive a code to reset your \
90
90
+
password." )
91
91
+
</span>
92
92
+
<form className="w-full flex flex-col mt-4 mb-2 gap-y-2">
93
93
+
<input type_="hidden" name="dream.csrf" value=csrf_token />
94
94
+
<input type_="hidden" name="step" value="request" />
95
95
+
<Input
96
96
+
sr_only=true
97
97
+
name="email"
98
98
+
type_="email"
99
99
+
label="email"
100
100
+
autoComplete="email"
101
101
+
/>
102
102
+
( match error with
103
103
+
| Some err ->
104
104
+
<span
105
105
+
className="inline-flex items-center text-phoenix-100 text-sm">
106
106
+
<CircleAlertIcon className="w-4 h-4 mr-2" /> (string err)
107
107
+
</span>
108
108
+
| None ->
109
109
+
null )
110
110
+
<Button type_="submit" formMethod="post" className="mt-2">
111
111
+
(string "send reset code")
112
112
+
</Button>
113
113
+
</form>
114
114
+
<div className="mt-4">
115
115
+
<span className="text-sm text-mist-100">
116
116
+
(string "already have a code? ")
117
117
+
<a
118
118
+
className="text-mana-100 underline hover:text-mana-200"
119
119
+
href="/account/password-reset?step=reset">
120
120
+
(string "enter it here")
121
121
+
</a>
122
122
+
</span>
123
123
+
</div>
124
124
+
</div> )
125
125
+
<div className="mt-2">
126
126
+
<a
127
127
+
className="text-sm underline text-mana-100 hover:text-mana-200"
128
128
+
href="/account/login">
129
129
+
(string "back to sign in")
130
130
+
</a>
131
131
+
</div>
132
132
+
</main>
+104
pegasus/lib/api/account_/password_reset.ml
···
1
1
+
let get_handler =
2
2
+
Xrpc.handler (fun ctx ->
3
3
+
let csrf_token = Dream.csrf_token ctx.req in
4
4
+
let step =
5
5
+
Dream.query ctx.req "step" |> Option.value ~default:"request"
6
6
+
in
7
7
+
Util.render_html ~title:"Reset Password"
8
8
+
(module Frontend.PasswordResetPage)
9
9
+
~props:{csrf_token; step; email_sent_to= None; error= None} )
10
10
+
11
11
+
let post_handler =
12
12
+
Xrpc.handler (fun ctx ->
13
13
+
let csrf_token = Dream.csrf_token ctx.req in
14
14
+
match%lwt Dream.form ctx.req with
15
15
+
| `Ok fields -> (
16
16
+
let step =
17
17
+
List.assoc_opt "step" fields |> Option.value ~default:"request"
18
18
+
in
19
19
+
match step with
20
20
+
| "reset" -> (
21
21
+
let token =
22
22
+
List.assoc_opt "token" fields |> Option.value ~default:""
23
23
+
in
24
24
+
let password =
25
25
+
List.assoc_opt "password" fields |> Option.value ~default:""
26
26
+
in
27
27
+
if String.length token = 0 then
28
28
+
Util.render_html ~status:`Bad_Request ~title:"Reset Password"
29
29
+
(module Frontend.PasswordResetPage)
30
30
+
~props:
31
31
+
{ csrf_token
32
32
+
; step= "reset"
33
33
+
; email_sent_to= None
34
34
+
; error= Some "Please enter the reset code." }
35
35
+
else if String.length password < 8 then
36
36
+
Util.render_html ~status:`Bad_Request ~title:"Reset Password"
37
37
+
(module Frontend.PasswordResetPage)
38
38
+
~props:
39
39
+
{ csrf_token
40
40
+
; step= "reset"
41
41
+
; email_sent_to= None
42
42
+
; error= Some "Password must be at least 8 characters." }
43
43
+
else
44
44
+
match%lwt
45
45
+
Server.ResetPassword.reset_password ~token ~password ctx.db
46
46
+
with
47
47
+
| Ok _ ->
48
48
+
Util.render_html ~title:"Reset Password"
49
49
+
(module Frontend.PasswordResetPage)
50
50
+
~props:
51
51
+
{ csrf_token
52
52
+
; step= "success"
53
53
+
; email_sent_to= None
54
54
+
; error= None }
55
55
+
| Error Server.ResetPassword.InvalidToken
56
56
+
| Error Server.ResetPassword.ExpiredToken ->
57
57
+
Util.render_html ~status:`Bad_Request ~title:"Reset Password"
58
58
+
(module Frontend.PasswordResetPage)
59
59
+
~props:
60
60
+
{ csrf_token
61
61
+
; step= "reset"
62
62
+
; email_sent_to= None
63
63
+
; error=
64
64
+
Some
65
65
+
"Invalid or expired reset code. Please request a \
66
66
+
new one." } )
67
67
+
| _ ->
68
68
+
let email =
69
69
+
List.assoc_opt "email" fields
70
70
+
|> Option.value ~default:""
71
71
+
|> String.lowercase_ascii
72
72
+
in
73
73
+
if String.length email = 0 then
74
74
+
Util.render_html ~status:`Bad_Request ~title:"Reset Password"
75
75
+
(module Frontend.PasswordResetPage)
76
76
+
~props:
77
77
+
{ csrf_token
78
78
+
; step= "request"
79
79
+
; email_sent_to= None
80
80
+
; error= Some "Please enter your email address." }
81
81
+
else
82
82
+
let%lwt () =
83
83
+
match%lwt Data_store.get_actor_by_identifier email ctx.db with
84
84
+
| Some actor ->
85
85
+
Server.RequestPasswordReset.request_password_reset actor
86
86
+
ctx.db
87
87
+
| None ->
88
88
+
Lwt.return_unit
89
89
+
in
90
90
+
Util.render_html ~title:"Reset Password"
91
91
+
(module Frontend.PasswordResetPage)
92
92
+
~props:
93
93
+
{ csrf_token
94
94
+
; step= "reset"
95
95
+
; email_sent_to= Some email
96
96
+
; error= None } )
97
97
+
| _ ->
98
98
+
Util.render_html ~status:`Bad_Request ~title:"Reset Password"
99
99
+
(module Frontend.PasswordResetPage)
100
100
+
~props:
101
101
+
{ csrf_token
102
102
+
; step= "request"
103
103
+
; email_sent_to= None
104
104
+
; error= Some "Invalid form submission. Please try again." } )