objective categorical abstract machine language personal data server
65
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 241 lines 9.1 kB view raw
1[@@@ocaml.warning "-26-27"] 2 3open Melange_json.Primitives 4open React 5 6type two_fa_methods = 7 { totp: bool [@default false] 8 ; email: bool [@default false] 9 ; backup_code: bool [@default false] 10 ; security_key: bool [@default false] } 11[@@deriving json] 12 13type props = 14 { redirect_url: string 15 ; csrf_token: string 16 ; error: string option [@default None] 17 ; two_fa_required: bool [@default false] 18 ; pending_2fa_token: string option [@default None] 19 ; two_fa_methods: two_fa_methods option [@default None] } 20[@@deriving json] 21 22let[@react.component] make 23 ~props: 24 ({ redirect_url 25 ; csrf_token 26 ; error 27 ; two_fa_required 28 ; pending_2fa_token 29 ; two_fa_methods } : 30 props ) () = 31 let passkeyError, setPasskeyError = useState (fun () -> None) in 32 let passkeyLoading, setPasskeyLoading = useState (fun () -> false) in 33 let currentOptions = useRef (None : Js.Json.t option) in 34 let%browser_only triggerPasskeyAutofill () = 35 WebAuthn.browserSupportsWebAuthnAutofill () 36 |> Js.Promise.then_ (fun supported -> 37 if supported then begin 38 Fetch.fetch "/account/passkeys/login/options" 39 |> Js.Promise.then_ (fun response -> 40 if Fetch.Response.ok response then Fetch.Response.json response 41 else Js.Exn.raiseError "Failed to get options" ) 42 |> Js.Promise.then_ (fun options -> 43 currentOptions.current <- Some options ; 44 WebAuthn.startAuthentication 45 {optionsJSON= options; useBrowserAutofill= true} ) 46 |> Js.Promise.then_ (fun credential -> 47 setPasskeyLoading (fun _ -> true) ; 48 let challenge = 49 match currentOptions.current with 50 | Some opts -> 51 Js.Dict.unsafeGet (Obj.magic opts) "challenge" 52 | None -> 53 Js.Json.string "" 54 in 55 let body = 56 Js.Json.object_ 57 (Js.Dict.fromArray 58 [| ( "response" 59 , Js.Json.string (Js.Json.stringify credential) ) 60 ; ("challenge", challenge) |] ) 61 in 62 Fetch.fetchWithInit 63 ( "/account/passkeys/login/verify?redirect_url=" 64 ^ Js.Global.encodeURIComponent redirect_url ) 65 (Fetch.RequestInit.make ~method_:Post 66 ~body:(Fetch.BodyInit.make (Js.Json.stringify body)) 67 ~headers: 68 (Fetch.HeadersInit.makeWithArray 69 [|("Content-Type", "application/json")|] ) 70 () ) ) 71 |> Js.Promise.then_ (fun response -> 72 setPasskeyLoading (fun _ -> false) ; 73 if Fetch.Response.ok response then 74 Fetch.Response.json response 75 |> Js.Promise.then_ (fun json -> 76 let redirect = 77 Js.Dict.unsafeGet (Obj.magic json) "redirect" 78 |> Js.Json.decodeString 79 |> Option.value ~default:"/account" 80 in 81 Webapi.Dom.(Window.setLocation window redirect) ; 82 Js.Promise.resolve () ) 83 else begin 84 setPasskeyError (fun _ -> Some "Passkey authentication failed") ; 85 Js.Promise.resolve () 86 end ) 87 |> Js.Promise.catch (fun _ -> 88 (* user cancelled or error *) 89 Js.Promise.resolve () ) 90 end 91 else Js.Promise.resolve () ) 92 in 93 let _ = 94 React.useEffect0 (fun () -> 95 (* only start passkey autofill if not in 2FA step *) 96 if not two_fa_required then 97 let _ = triggerPasskeyAutofill () in 98 None 99 else None ) 100 in 101 <main className="w-full h-auto max-w-xs px-4 sm:px-0 my-auto"> 102 <h1 className="text-2xl font-serif text-mana-200 mb-2"> 103 (string (if two_fa_required then "verify your identity" else "sign in")) 104 </h1> 105 ( if two_fa_required then 106 (* 2FA verification step *) 107 let methods = 108 Option.value 109 ~default: 110 { totp= false 111 ; email= false 112 ; backup_code= false 113 ; security_key= false } 114 two_fa_methods 115 in 116 <div> 117 <span className="w-full text-balance text-mist-100"> 118 ( if methods.totp || methods.security_key then 119 string 120 "Enter the code from your authenticator app or security key." 121 else if methods.email then 122 string "Enter the verification code sent to your email." 123 else string "Enter your verification code." ) 124 </span> 125 <form className="w-full flex flex-col mt-4 mb-2 gap-y-2"> 126 <input type_="hidden" name="dream.csrf" value=csrf_token /> 127 <input 128 type_="hidden" 129 name="pending_2fa_token" 130 value=(Option.value ~default:"" pending_2fa_token) 131 /> 132 <input type_="hidden" name="redirect_url" value=redirect_url /> 133 <Input 134 sr_only=true 135 name="two_fa_code" 136 type_="text" 137 label="verification code" 138 autoComplete="one-time-code" 139 inputMode="numeric" 140 pattern="[0-9A-Za-z\\-]*" 141 /> 142 ( match error with 143 | Some err -> 144 <span 145 className="inline-flex items-center text-phoenix-100 text-sm"> 146 <CircleAlertIcon className="w-4 h-4 mr-2" /> (string err) 147 </span> 148 | None -> 149 null ) 150 <Button type_="submit" formMethod="post" className="mt-2"> 151 (string "verify") 152 </Button> 153 </form> 154 ( if methods.backup_code then 155 <span className="text-sm text-mist-80"> 156 (string "Lost access? You can use a backup code instead.") 157 </span> 158 else null ) 159 <div className="mt-4"> 160 <a 161 className="text-sm text-mana-100 hover:text-mana-200" 162 href="/account/login"> 163 (string "cancel and start over") 164 </a> 165 </div> 166 </div> 167 else 168 <div> 169 <span className="w-full text-balance text-mist-100"> 170 (string 171 "Enter your handle, email address, or DID, and your password." ) 172 </span> 173 <form className="w-full flex flex-col mt-4 mb-2 gap-y-2"> 174 <input type_="hidden" name="dream.csrf" value=csrf_token /> 175 <Input 176 sr_only=true 177 name="identifier" 178 type_="text" 179 label="identifier" 180 autoComplete="username webauthn" 181 /> 182 <Input 183 sr_only=true name="password" type_="password" label="password" 184 /> 185 <input type_="hidden" name="redirect_url" value=redirect_url /> 186 ( match error with 187 | Some err -> 188 <span 189 className="inline-flex items-center text-phoenix-100 text-sm"> 190 <CircleAlertIcon className="w-4 h-4 mr-2" /> (string err) 191 </span> 192 | None -> 193 null ) 194 ( match passkeyError with 195 | Some err -> 196 <span 197 className="inline-flex items-center text-phoenix-100 text-sm"> 198 <CircleAlertIcon className="w-4 h-4 mr-2" /> (string err) 199 </span> 200 | None -> 201 null ) 202 <Button type_="submit" formMethod="post" className="mt-2"> 203 (string (if passkeyLoading then "signing in..." else "sign in")) 204 </Button> 205 <Button 206 kind=`Tertiary 207 className="text-sm" 208 onClick=(fun _ -> 209 let _ = triggerPasskeyAutofill () in 210 () )> 211 (string "sign in with passkey") 212 </Button> 213 </form> 214 <span className="text-sm text-mist-100"> 215 (string "or: ") 216 <ul className="mt-1 pl-2"> 217 <li className="mb-1"> 218 <a 219 className="text-mana-100 underline hover:text-mana-200" 220 href="/account/signup"> 221 (string "create an account") 222 </a> 223 </li> 224 <li className="mb-1"> 225 <a 226 className="text-mana-100 underline hover:text-mana-200" 227 href="/account/migrate"> 228 (string "migrate from another PDS") 229 </a> 230 </li> 231 <li> 232 <a 233 className="text-mana-100 underline hover:text-mana-200" 234 href="/account/reset"> 235 (string "reset your password") 236 </a> 237 </li> 238 </ul> 239 </span> 240 </div> ) 241 </main>