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
Add manual passkey login trigger button
futur.blue
2 months ago
7d0bf431
8313fe8a
verified
This commit was signed with the committer's
known signature
.
futur.blue
SSH Key Fingerprint:
SHA256:QHGqHWNpqYyw9bt8KmPuJIyeZX9SZewBZ0PR1COtKQ0=
+68
-62
1 changed file
expand all
collapse all
unified
split
frontend
src
templates
LoginPage.mlx
+68
-62
frontend/src/templates/LoginPage.mlx
···
31
31
let passkeyError, setPasskeyError = useState (fun () -> None) in
32
32
let passkeyLoading, setPasskeyLoading = useState (fun () -> false) in
33
33
let currentOptions = useRef (None : Js.Json.t option) in
34
34
+
let%browser_only triggerPasskeyAutofill () =
35
35
+
WebAuthn.browserSupportsWebAuthnAutofill ()
36
36
+
|> Js.Promise.then_ (fun supported ->
37
37
+
if supported then begin
38
38
+
Fetch.fetch "/account/passkeys/login/options"
39
39
+
|> Js.Promise.then_ (fun response ->
40
40
+
if Fetch.Response.ok response then Fetch.Response.json response
41
41
+
else Js.Exn.raiseError "Failed to get options" )
42
42
+
|> Js.Promise.then_ (fun options ->
43
43
+
currentOptions.current <- Some options ;
44
44
+
WebAuthn.startAuthentication
45
45
+
{optionsJSON= options; useBrowserAutofill= true} )
46
46
+
|> Js.Promise.then_ (fun credential ->
47
47
+
setPasskeyLoading (fun _ -> true) ;
48
48
+
let challenge =
49
49
+
match currentOptions.current with
50
50
+
| Some opts ->
51
51
+
Js.Dict.unsafeGet (Obj.magic opts) "challenge"
52
52
+
| None ->
53
53
+
Js.Json.string ""
54
54
+
in
55
55
+
let body =
56
56
+
Js.Json.object_
57
57
+
(Js.Dict.fromArray
58
58
+
[| ( "response"
59
59
+
, Js.Json.string (Js.Json.stringify credential) )
60
60
+
; ("challenge", challenge) |] )
61
61
+
in
62
62
+
Fetch.fetchWithInit
63
63
+
( "/account/passkeys/login/verify?redirect_url="
64
64
+
^ Js.Global.encodeURIComponent redirect_url )
65
65
+
(Fetch.RequestInit.make ~method_:Post
66
66
+
~body:(Fetch.BodyInit.make (Js.Json.stringify body))
67
67
+
~headers:
68
68
+
(Fetch.HeadersInit.makeWithArray
69
69
+
[|("Content-Type", "application/json")|] )
70
70
+
() ) )
71
71
+
|> Js.Promise.then_ (fun response ->
72
72
+
setPasskeyLoading (fun _ -> false) ;
73
73
+
if Fetch.Response.ok response then
74
74
+
Fetch.Response.json response
75
75
+
|> Js.Promise.then_ (fun json ->
76
76
+
let redirect =
77
77
+
Js.Dict.unsafeGet (Obj.magic json) "redirect"
78
78
+
|> Js.Json.decodeString
79
79
+
|> Option.value ~default:"/account"
80
80
+
in
81
81
+
Webapi.Dom.(Window.setLocation window redirect) ;
82
82
+
Js.Promise.resolve () )
83
83
+
else begin
84
84
+
setPasskeyError (fun _ -> Some "Passkey authentication failed") ;
85
85
+
Js.Promise.resolve ()
86
86
+
end )
87
87
+
|> Js.Promise.catch (fun _ ->
88
88
+
(* user cancelled or error *)
89
89
+
Js.Promise.resolve () )
90
90
+
end
91
91
+
else Js.Promise.resolve () )
92
92
+
in
34
93
let _ =
35
94
React.useEffect0 (fun () ->
36
95
(* only start passkey autofill if not in 2FA step *)
37
96
if not two_fa_required then
38
38
-
let _ =
39
39
-
WebAuthn.browserSupportsWebAuthnAutofill ()
40
40
-
|> Js.Promise.then_ (fun supported ->
41
41
-
if supported then begin
42
42
-
Fetch.fetch "/account/passkeys/login/options"
43
43
-
|> Js.Promise.then_ (fun response ->
44
44
-
if Fetch.Response.ok response then
45
45
-
Fetch.Response.json response
46
46
-
else Js.Exn.raiseError "Failed to get options" )
47
47
-
|> Js.Promise.then_ (fun options ->
48
48
-
currentOptions.current <- Some options ;
49
49
-
WebAuthn.startAuthentication
50
50
-
{optionsJSON= options; useBrowserAutofill= true} )
51
51
-
|> Js.Promise.then_ (fun credential ->
52
52
-
setPasskeyLoading (fun _ -> true) ;
53
53
-
let challenge =
54
54
-
match currentOptions.current with
55
55
-
| Some opts ->
56
56
-
Js.Dict.unsafeGet (Obj.magic opts) "challenge"
57
57
-
| None ->
58
58
-
Js.Json.string ""
59
59
-
in
60
60
-
let body =
61
61
-
Js.Json.object_
62
62
-
(Js.Dict.fromArray
63
63
-
[| ( "response"
64
64
-
, Js.Json.string (Js.Json.stringify credential)
65
65
-
)
66
66
-
; ("challenge", challenge) |] )
67
67
-
in
68
68
-
Fetch.fetchWithInit
69
69
-
( "/account/passkeys/login/verify?redirect_url="
70
70
-
^ Js.Global.encodeURIComponent redirect_url )
71
71
-
(Fetch.RequestInit.make ~method_:Post
72
72
-
~body:(Fetch.BodyInit.make (Js.Json.stringify body))
73
73
-
~headers:
74
74
-
(Fetch.HeadersInit.makeWithArray
75
75
-
[|("Content-Type", "application/json")|] )
76
76
-
() ) )
77
77
-
|> Js.Promise.then_ (fun response ->
78
78
-
setPasskeyLoading (fun _ -> false) ;
79
79
-
if Fetch.Response.ok response then
80
80
-
Fetch.Response.json response
81
81
-
|> Js.Promise.then_ (fun json ->
82
82
-
let redirect =
83
83
-
Js.Dict.unsafeGet (Obj.magic json) "redirect"
84
84
-
|> Js.Json.decodeString
85
85
-
|> Option.value ~default:"/account"
86
86
-
in
87
87
-
Webapi.Dom.(Window.setLocation window redirect) ;
88
88
-
Js.Promise.resolve () )
89
89
-
else begin
90
90
-
setPasskeyError (fun _ ->
91
91
-
Some "Passkey authentication failed" ) ;
92
92
-
Js.Promise.resolve ()
93
93
-
end )
94
94
-
|> Js.Promise.catch (fun _ ->
95
95
-
(* user cancelled or error *)
96
96
-
Js.Promise.resolve () )
97
97
-
end
98
98
-
else Js.Promise.resolve () )
99
99
-
in
97
97
+
let _ = triggerPasskeyAutofill () in
100
98
None
101
99
else None )
102
100
in
···
203
201
null )
204
202
<Button type_="submit" formMethod="post" className="mt-2">
205
203
(string (if passkeyLoading then "signing in..." else "sign in"))
204
204
+
</Button>
205
205
+
<Button
206
206
+
kind=`Tertiary
207
207
+
className="text-sm"
208
208
+
onClick=(fun _ ->
209
209
+
let _ = triggerPasskeyAutofill () in
210
210
+
() )>
211
211
+
(string "sign in with passkey")
206
212
</Button>
207
213
</form>
208
214
<span className="text-sm text-mist-100">