objective categorical abstract machine language personal data server
65
fork

Configure Feed

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

Implement email confirmation via frontend

futurGH fd846cc6 862a8ae1

+332 -93
+141 -1
frontend/src/templates/AccountPage.mlx
··· 14 14 ; handle: string 15 15 ; email: string 16 16 ; deactivated: bool [@default false] 17 + ; email_confirmed: bool [@default true] 18 + ; email_confirmation_pending: bool [@default false] 19 + ; email_confirmation_error: string option [@default None] 17 20 ; email_change_pending: bool [@default false] 18 21 ; pending_email: string option [@default None] 19 22 ; email_error: string option [@default None] ··· 31 34 ; handle 32 35 ; email 33 36 ; deactivated 37 + ; email_confirmed 38 + ; email_confirmation_pending 39 + ; email_confirmation_error 34 40 ; email_change_pending 35 41 ; pending_email 36 42 ; email_error ··· 63 69 in 64 70 let deleteLoading, setDeleteLoading = useState (fun () -> false) in 65 71 let deleteTokenInput, setDeleteTokenInput = useState (fun () -> "") in 72 + let confirmEmailPending, setConfirmEmailPending = 73 + useState (fun () -> email_confirmation_pending) 74 + in 75 + let confirmEmailError, setConfirmEmailError = 76 + useState (fun () -> email_confirmation_error) 77 + in 78 + let confirmEmailLoading, setConfirmEmailLoading = useState (fun () -> false) in 79 + let confirmEmailTokenInput, setConfirmEmailTokenInput = useState (fun () -> "") in 80 + let successMessage, setSuccessMessage = useState (fun () -> success) in 66 81 <div className="w-auto h-auto px-4 sm:px-0 flex flex-col md:flex-row gap-12"> 67 82 <AccountSidebar 68 83 current_user logged_in_users active_page="/account" ··· 317 332 </Aria.ModalOverlay> 318 333 </Aria.DialogTrigger> )] 319 334 </ClientOnly> 335 + ( if not email_confirmed then 336 + <ClientOnly fallback=( 337 + <span className="-mt-1.5 inline-flex items-center text-mist-80 text-sm"> 338 + <CircleAlertIcon className="w-4 h-4 mr-2" /> 339 + (string ("Your email address isn't confirmed " ^ {js|·|js})) 340 + <button 341 + type_="button" 342 + disabled=true 343 + className="ml-1 underline text-mana-100 hover:text-mana-200 cursor-pointer \ 344 + disabled:cursor-not-allowed"> 345 + (string "send confirmation code") 346 + </button> 347 + </span> 348 + )> 349 + [%browser_only 350 + (fun () -> 351 + let submitConfirmEmailForm action fields = 352 + setConfirmEmailLoading (fun _ -> true) ; 353 + setConfirmEmailError (fun _ -> None) ; 354 + let body = 355 + Fetch.BodyInit.make 356 + (Webapi.Url.URLSearchParams.makeWithArray 357 + (Array.append 358 + [|("dream.csrf", csrf_token); ("action", action)|] 359 + fields ) 360 + |> Webapi.Url.URLSearchParams.toString ) 361 + in 362 + let _ = 363 + Fetch.fetchWithInit "/account" 364 + (Fetch.RequestInit.make ~method_:Post ~body 365 + ~headers: 366 + (Fetch.HeadersInit.makeWithArray 367 + [|("Content-Type", "application/x-www-form-urlencoded")|] ) 368 + () ) 369 + |> Js.Promise.then_ (fun response -> 370 + setConfirmEmailLoading (fun _ -> false) ; 371 + if Fetch.Response.ok response then 372 + match action with 373 + | "request_email_confirmation" -> 374 + setConfirmEmailPending (fun _ -> true) ; 375 + Js.Promise.resolve () 376 + | "confirm_email_confirmation" -> 377 + Webapi.Dom.( 378 + location |> Location.reload) ; 379 + setSuccessMessage (fun _ -> Some "Email confirmed!") ; 380 + Js.Promise.resolve () 381 + | "cancel_email_confirmation" -> 382 + setConfirmEmailPending (fun _ -> false) ; 383 + Js.Promise.resolve () 384 + | _ -> 385 + Js.Promise.resolve () 386 + else ( 387 + setConfirmEmailError (fun _ -> 388 + Some "An error occurred. Please try again." ) ; 389 + Js.Promise.resolve () ) ) 390 + |> Js.Promise.catch (fun _ -> 391 + setConfirmEmailLoading (fun _ -> false) ; 392 + setConfirmEmailError (fun _ -> 393 + Some "An error occurred. Please try again." ) ; 394 + Js.Promise.resolve () ) 395 + in 396 + () 397 + in 398 + <div className="flex flex-col gap-y-3"> 399 + <span className="-mt-1.5 inline-flex items-center text-mist-80 text-sm"> 400 + <CircleAlertIcon className="w-4 h-4 mr-2" /> 401 + (string ("Your email address isn't confirmed " ^ {js|·|js})) 402 + <button 403 + type_="button" 404 + disabled=confirmEmailLoading 405 + className="ml-1 underline text-mana-100 hover:text-mana-200 cursor-pointer \ 406 + disabled:cursor-not-allowed" 407 + onClick=(fun _ -> 408 + submitConfirmEmailForm "request_email_confirmation" [||])> 409 + (string (if confirmEmailLoading then "sending..." 410 + else if confirmEmailPending then "resend confirmation code" 411 + else "send confirmation code")) 412 + </button> 413 + </span> 414 + ( if confirmEmailPending then 415 + <div className="flex flex-col gap-y-3"> 416 + <Input 417 + name="email_token" 418 + label="Confirmation code" 419 + placeholder="eml-..." 420 + showIndicator=false 421 + value=confirmEmailTokenInput 422 + onChange=(fun e -> 423 + setConfirmEmailTokenInput (fun _ -> 424 + (Event.Form.target e)##value ) ) 425 + /> 426 + ( match confirmEmailError with 427 + | Some err -> 428 + <span 429 + className="inline-flex items-center \ 430 + text-phoenix-100 text-sm"> 431 + <CircleAlertIcon className="w-4 h-4 mr-2" /> 432 + (string err) 433 + </span> 434 + | None -> 435 + null ) 436 + <div className="flex flex-row gap-x-3"> 437 + <Button 438 + type_="button" 439 + disabled=confirmEmailLoading 440 + onClick=(fun _ -> 441 + submitConfirmEmailForm "confirm_email_confirmation" 442 + [|("token", confirmEmailTokenInput)|])> 443 + (string (if confirmEmailLoading then "confirming..." else "confirm")) 444 + </Button> 445 + <Button 446 + kind=`Secondary 447 + type_="button" 448 + disabled=confirmEmailLoading 449 + onClick=(fun _ -> 450 + submitConfirmEmailForm "cancel_email_confirmation" [||])> 451 + (string "cancel") 452 + </Button> 453 + </div> 454 + </div> 455 + else 456 + null ) 457 + </div> )] 458 + </ClientOnly> 459 + else null ) 320 460 <HandleInput 321 461 name="handle" 322 462 label="Handle" ··· 339 479 </span> 340 480 | None -> 341 481 null ) 342 - ( match success with 482 + ( match successMessage with 343 483 | Some success -> 344 484 <span 345 485 className="inline-flex items-center text-mana-100 text-sm">
+1 -1
frontend/src/templates/AccountPermissionsPage.mlx
··· 61 61 else "Unknown Browser" 62 62 else "Unknown Browser" 63 63 in 64 - os ^ " \u{B7} " ^ browser 64 + os ^ {js| · |js} ^ browser 65 65 66 66 let[@react.component] make 67 67 ~props:
+1 -1
frontend/src/templates/AdminLoginPage.mlx
··· 8 8 <main className="w-full h-auto max-w-xs px-4 sm:px-0"> 9 9 <h1 className="text-2xl font-serif text-mana-200 mb-2">(string "admin")</h1> 10 10 <span className="w-full text-balance text-mist-100"> 11 - (string "Enter your admin password to continue.") 11 + (string "Enter the admin password to continue.") 12 12 </span> 13 13 <form className="w-full flex flex-col mt-4 mb-2 gap-y-2"> 14 14 <input type_="hidden" name="dream.csrf" value=csrf_token />
+77 -11
pegasus/lib/api/account_/index.ml
··· 12 12 | _ -> 13 13 false 14 14 15 + let has_valid_email_confirmation_code (actor : Data_store.Types.actor) = 16 + match (actor.auth_code, actor.auth_code_expires_at, actor.pending_email) with 17 + | Some code, Some expires_at, None -> 18 + String.starts_with ~prefix:"eml-" code && expires_at > Util.now_ms () 19 + | _ -> 20 + false 21 + 15 22 let get_handler = 16 23 Xrpc.handler (fun ctx -> 17 24 match%lwt Session.Raw.get_current_did ctx.req with 18 25 | None -> 19 26 Dream.redirect ctx.req "/account/login" 20 27 | Some did -> ( 21 - let%lwt logged_in_users = 28 + let%lwt current_user, logged_in_users = 22 29 Session.list_logged_in_actors ctx.req ctx.db 23 30 in 24 31 match%lwt Data_store.get_actor_by_identifier did ctx.db with 25 32 | None -> 26 33 Dream.redirect ctx.req "/account/login" 27 34 | Some actor -> 28 - let current_user : Frontend.AccountSwitcher.actor = 29 - {did= actor.did; handle= actor.handle; avatar_data_uri= None} 35 + let current_user = 36 + Option.value 37 + ~default: 38 + {did= actor.did; handle= actor.handle; avatar_data_uri= None} 39 + current_user 30 40 in 31 41 let csrf_token = Dream.csrf_token ctx.req in 32 42 let deactivated = actor.deactivated_at <> None in 43 + let email_confirmed = actor.email_confirmed_at <> None in 44 + let email_confirmation_pending = 45 + has_valid_email_confirmation_code actor 46 + in 33 47 let email_change_pending = has_valid_email_change_code actor in 34 48 let pending_email = actor.pending_email in 35 49 let delete_pending = has_valid_delete_code actor in ··· 42 56 ; handle= actor.handle 43 57 ; email= actor.email 44 58 ; deactivated 59 + ; email_confirmed 60 + ; email_confirmation_pending 61 + ; email_confirmation_error= None 45 62 ; email_change_pending 46 63 ; pending_email 47 64 ; email_error= None ··· 56 73 | None -> 57 74 Dream.redirect ctx.req "/account/login" 58 75 | Some did -> ( 59 - let%lwt logged_in_users = 76 + let%lwt current_user, logged_in_users = 60 77 Session.list_logged_in_actors ctx.req ctx.db 61 78 in 62 79 match%lwt Data_store.get_actor_by_identifier did ctx.db with 63 80 | None -> 64 81 Dream.redirect ctx.req "/account/login" 65 82 | Some actor -> ( 66 - let current_user : Frontend.AccountSwitcher.actor = 67 - {did= actor.did; handle= actor.handle; avatar_data_uri= None} 83 + let current_user = 84 + Option.value 85 + ~default: 86 + {did= actor.did; handle= actor.handle; avatar_data_uri= None} 87 + current_user 68 88 in 69 89 let csrf_token = Dream.csrf_token ctx.req in 70 - let render_page ?error ?success ?email_error ?delete_error () = 90 + let render_page ?error ?success ?email_error 91 + ?email_confirmation_error ?delete_error () = 71 92 let%lwt actor_opt = 72 93 Data_store.get_actor_by_identifier did ctx.db 73 94 in 74 95 let actor = Option.get actor_opt in 75 96 let deactivated = actor.deactivated_at <> None in 97 + let email_confirmed = actor.email_confirmed_at <> None in 98 + let email_confirmation_pending = 99 + has_valid_email_confirmation_code actor 100 + in 76 101 let email_change_pending = has_valid_email_change_code actor in 77 102 let pending_email = actor.pending_email in 78 103 let delete_pending = has_valid_delete_code actor in ··· 85 110 ; handle= actor.handle 86 111 ; email= actor.email 87 112 ; deactivated 113 + ; email_confirmed 114 + ; email_confirmation_pending 115 + ; email_confirmation_error 88 116 ; email_change_pending 89 117 ; pending_email 90 118 ; email_error ··· 199 227 render_page () ) 200 228 | Some "confirm_email_change" -> ( 201 229 let token = 202 - List.assoc_opt "token" fields 203 - |> Option.value ~default:"" |> String.trim 230 + List.assoc_opt "token" fields |> Option.map String.trim 204 231 in 205 232 match%lwt 206 - Server.UpdateEmail.update_email ~token:(Some token) 207 - actor ctx.db 233 + match (actor.auth_code, actor.auth_code_expires_at) with 234 + | Some code, Some expiry 235 + when Some code = token && expiry > Util.now_ms () -> 236 + Server.UpdateEmail.update_email ~token actor ctx.db 237 + | _ -> 238 + Lwt.return_error Server.UpdateEmail.InvalidToken 208 239 with 209 240 | Ok _ -> 210 241 render_page ~success:"Email address updated." () ··· 218 249 render_page ~email_error:"Verification code required." 219 250 () ) 220 251 | Some "cancel_email_change" -> 252 + let%lwt () = Data_store.clear_auth_code ~did ctx.db in 253 + render_page () 254 + | Some "request_email_confirmation" -> ( 255 + match%lwt 256 + Server.RequestEmailConfirmation.request_email_confirmation 257 + actor ctx.db 258 + with 259 + | Ok () -> 260 + render_page () 261 + | Error Server.RequestEmailConfirmation.AlreadyConfirmed -> 262 + render_page 263 + ~email_confirmation_error: 264 + "Email is already confirmed." 265 + () ) 266 + | Some "confirm_email_confirmation" -> ( 267 + let token = 268 + List.assoc_opt "token" fields 269 + |> Option.value ~default:"" |> String.trim 270 + in 271 + match%lwt 272 + Server.ConfirmEmail.confirm_email ~email:actor.email 273 + ~token actor ctx.db 274 + with 275 + | Ok () -> 276 + render_page ~success:"Email confirmed." () 277 + | Error Server.ConfirmEmail.ExpiredToken 278 + | Error Server.ConfirmEmail.InvalidToken -> 279 + render_page 280 + ~email_confirmation_error: 281 + "Invalid or expired confirmation code." 282 + () 283 + | Error Server.ConfirmEmail.EmailMismatch -> 284 + render_page 285 + ~email_confirmation_error:"Email mismatch." () ) 286 + | Some "cancel_email_confirmation" -> 221 287 let%lwt () = Data_store.clear_auth_code ~did ctx.db in 222 288 render_page () 223 289 | _ ->
+6 -3
pegasus/lib/api/account_/permissions.ml
··· 13 13 | None -> 14 14 Dream.redirect ctx.req "/account/login" 15 15 | Some did -> ( 16 - let%lwt logged_in_users = 16 + let%lwt current_user, logged_in_users = 17 17 Session.list_logged_in_actors ctx.req ctx.db 18 18 in 19 19 match%lwt Data_store.get_actor_by_identifier did ctx.db with 20 20 | None -> 21 21 Dream.redirect ctx.req "/account/login" 22 22 | Some actor -> 23 - let current_user : Frontend.AccountSwitcher.actor = 24 - {did= actor.did; handle= actor.handle; avatar_data_uri= None} 23 + let current_user = 24 + Option.value 25 + ~default: 26 + {did= actor.did; handle= actor.handle; avatar_data_uri= None} 27 + current_user 25 28 in 26 29 let csrf_token = Dream.csrf_token ctx.req in 27 30 let%lwt clients =
+4 -7
pegasus/lib/api/oauth_/authorize.ml
··· 76 76 match did with 77 77 | None -> 78 78 login_redirect 79 - | Some did -> 79 + | Some _ -> 80 80 let scopes = String.split_on_char ' ' req.scope in 81 81 let csrf_token = Dream.csrf_token ctx.req in 82 82 let client_id_uri = ··· 89 89 in 90 90 let client_url = (host, path) in 91 91 let client_name = metadata.client_name in 92 - let%lwt logged_in_users = 92 + let%lwt current_user, logged_in_users = 93 93 Session.list_logged_in_actors ctx.req ctx.db 94 94 in 95 95 let current_user = 96 - List.find_opt 97 - (fun (user : Frontend.OauthAuthorizePage.actor) -> 98 - user.did = did ) 99 - logged_in_users 100 - |> Option.value ~default:(List.hd logged_in_users) 96 + Option.value current_user 97 + ~default:(List.hd logged_in_users) 101 98 in 102 99 Util.render_html ~title:("Authorizing " ^ host) 103 100 (module Frontend.OauthAuthorizePage)
+27 -15
pegasus/lib/api/server/confirmEmail.ml
··· 1 1 type request = {email: string; token: string} 2 2 [@@deriving yojson {strict= false}] 3 3 4 + type confirm_error = InvalidToken | ExpiredToken | EmailMismatch 5 + 6 + let confirm_email ~email ~token (actor : Data_store.Types.actor) db = 7 + let email = String.lowercase_ascii email in 8 + if String.lowercase_ascii actor.email <> email then 9 + Lwt.return_error EmailMismatch 10 + else 11 + match (actor.auth_code, actor.auth_code_expires_at) with 12 + | Some auth_code, Some expires_at 13 + when String.starts_with ~prefix:"eml-" auth_code 14 + && auth_code = token 15 + && Util.now_ms () < expires_at -> 16 + let%lwt () = Data_store.confirm_email ~did:actor.did db in 17 + Lwt.return_ok () 18 + | Some _, Some expires_at when Util.now_ms () >= expires_at -> 19 + Lwt.return_error ExpiredToken 20 + | _ -> 21 + Lwt.return_error InvalidToken 22 + 4 23 let handler = 5 24 Xrpc.handler ~auth:Authorization (fun {req; auth; db; _} -> 6 25 Auth.assert_account_scope auth ~attr:Oauth.Scopes.Email 7 26 ~action:Oauth.Scopes.Manage ; 8 27 let did = Auth.get_authed_did_exn auth in 9 28 let%lwt {email; token} = Xrpc.parse_body req request_of_yojson in 10 - let email = String.lowercase_ascii email in 11 29 match%lwt Data_store.get_actor_by_identifier did db with 12 30 | None -> 13 31 Errors.invalid_request ~name:"AccountNotFound" "account not found" 14 32 | Some actor -> ( 15 - if String.lowercase_ascii actor.email <> email then 33 + match%lwt confirm_email ~email ~token actor db with 34 + | Ok () -> 35 + Dream.empty `OK 36 + | Error EmailMismatch -> 16 37 Errors.invalid_request ~name:"InvalidEmail" "email does not match" 17 - else 18 - match (actor.auth_code, actor.auth_code_expires_at) with 19 - | Some auth_code, Some expires_at 20 - when String.starts_with ~prefix:"eml-" auth_code 21 - && auth_code = token 22 - && Util.now_ms () < expires_at -> 23 - let%lwt () = Data_store.confirm_email ~did db in 24 - Dream.log "email confirmed for %s" did ; 25 - Dream.empty `OK 26 - | Some _, Some expires_at when Util.now_ms () >= expires_at -> 27 - Errors.invalid_request ~name:"ExpiredToken" "token expired" 28 - | _ -> 29 - Errors.invalid_request ~name:"InvalidToken" "invalid token" ) ) 38 + | Error ExpiredToken -> 39 + Errors.invalid_request ~name:"ExpiredToken" "token expired" 40 + | Error InvalidToken -> 41 + Errors.invalid_request ~name:"InvalidToken" "invalid token" ) )
+33 -23
pegasus/lib/api/server/requestEmailConfirmation.ml
··· 1 + type request_error = AlreadyConfirmed 2 + 3 + let request_email_confirmation (actor : Data_store.Types.actor) db = 4 + match actor.email_confirmed_at with 5 + | Some _ -> 6 + Lwt.return_error AlreadyConfirmed 7 + | None -> 8 + let code = 9 + "eml-" 10 + ^ String.sub 11 + Digestif.SHA256.( 12 + digest_string (actor.did ^ Int.to_string @@ Util.now_ms ()) 13 + |> to_hex ) 14 + 0 8 15 + in 16 + let expires_at = Util.now_ms () + (10 * 60 * 1000) in 17 + let%lwt () = 18 + Data_store.set_auth_code ~did:actor.did ~code ~expires_at db 19 + in 20 + let%lwt () = 21 + Util.send_email_or_log ~recipients:[To actor.email] 22 + ~subject:(Printf.sprintf "Confirm email for %s" actor.handle) 23 + ~body: 24 + (Plain 25 + (Printf.sprintf 26 + "Confirm your email address using the following token: %s" 27 + code ) ) 28 + in 29 + Lwt.return_ok () 30 + 1 31 let calc_key_did ctx = Some (Auth.get_authed_did_exn ctx.Xrpc.auth) 2 32 3 33 let handler = ··· 21 51 | None -> 22 52 Errors.internal_error ~msg:"actor not found" () 23 53 | Some actor -> ( 24 - match actor.email_confirmed_at with 25 - | Some _ -> 54 + match%lwt request_email_confirmation actor db with 55 + | Error AlreadyConfirmed -> 26 56 Errors.invalid_request ~name:"InvalidRequest" 27 57 "email already confirmed" 28 - | None -> 29 - let code = 30 - "eml-" 31 - ^ String.sub 32 - Digestif.SHA256.( 33 - digest_string (did ^ Int.to_string @@ Util.now_ms ()) 34 - |> to_hex ) 35 - 0 8 36 - in 37 - let expires_at = Util.now_ms () + (10 * 60 * 1000) in 38 - let%lwt () = Data_store.set_auth_code ~did ~code ~expires_at db in 39 - let%lwt () = 40 - Util.send_email_or_log ~recipients:[To actor.email] 41 - ~subject:(Printf.sprintf "Confirm email for %s" actor.handle) 42 - ~body: 43 - (Plain 44 - (Printf.sprintf 45 - "Confirm your email address using the following token: \ 46 - %s" 47 - code ) ) 48 - in 58 + | _ -> 49 59 Dream.empty `OK ) )
+5 -3
pegasus/lib/api/server/requestEmailUpdate.ml
··· 34 34 actor.email 35 35 in 36 36 Util.send_email_or_log ~recipients:[To to_email] 37 - ~subject:(Printf.sprintf "Email confirmation for %s" actor.handle) 37 + ~subject:(Printf.sprintf "Confirm email change for %s" actor.handle) 38 38 ~body: 39 39 (Plain 40 40 (Printf.sprintf 41 - "Confirm your email address using the following token: %s" code ) 42 - ) 41 + "Confirm that you would like to change your email address%s \ 42 + using the following token: %s" 43 + (match pending_email with Some e -> " to " ^ e | None -> "") 44 + code ) ) 43 45 else Lwt.return_unit 44 46 in 45 47 Lwt.return token_required
+37 -28
pegasus/lib/session.ml
··· 5 5 ; admin_authenticated: bool [@default false] } 6 6 [@@deriving yojson {strict= false}] 7 7 8 + type actor = Frontend.AccountSwitcher.actor = 9 + {did: string; handle: string; avatar_data_uri: string option} 10 + [@@deriving yojson {strict= false}] 11 + 8 12 let default = 9 13 { current_did= None 10 14 ; logged_in_dids= [] ··· 150 154 let list_logged_in_actors req db = 151 155 match%lwt get_logged_in_dids req with 152 156 | [] -> 153 - Lwt.return [] 157 + Lwt.return (None, []) 154 158 | dids -> 155 - Lwt_list.filter_map_s 156 - (fun did -> 157 - match%lwt Data_store.get_actor_by_identifier did db with 158 - | Some {handle; _} -> ( 159 - let actor : Frontend.OauthAuthorizePage.actor = 160 - {did; handle; avatar_data_uri= None} 161 - in 162 - let%lwt us = User_store.connect did in 163 - match%lwt 164 - User_store.get_record us "app.bsky.actor.profile/self" 165 - with 166 - | Some {value= profile; _} -> ( 167 - match Mist.Lex.String_map.find_opt "avatar" profile with 168 - | Some (`BlobRef {ref; _}) -> ( 169 - match%lwt User_store.get_blob us ref with 170 - | Some {data; mimetype; _} 171 - when String.starts_with ~prefix:"image/" mimetype -> 172 - Lwt.return_some 173 - { actor with 174 - avatar_data_uri= 175 - Some (Util.make_data_uri ~mimetype ~data) } 159 + let%lwt current_did = Raw.get_current_did req in 160 + let%lwt actors = 161 + Lwt_list.filter_map_s 162 + (fun did -> 163 + match%lwt Data_store.get_actor_by_identifier did db with 164 + | Some {handle; _} -> ( 165 + let actor = {did; handle; avatar_data_uri= None} in 166 + let%lwt us = User_store.connect did in 167 + match%lwt 168 + User_store.get_record us "app.bsky.actor.profile/self" 169 + with 170 + | Some {value= profile; _} -> ( 171 + match Mist.Lex.String_map.find_opt "avatar" profile with 172 + | Some (`BlobRef {ref; _}) -> ( 173 + match%lwt User_store.get_blob us ref with 174 + | Some {data; mimetype; _} 175 + when String.starts_with ~prefix:"image/" mimetype -> 176 + Lwt.return_some 177 + { actor with 178 + avatar_data_uri= 179 + Some (Util.make_data_uri ~mimetype ~data) } 180 + | _ -> 181 + Lwt.return_some actor ) 176 182 | _ -> 177 183 Lwt.return_some actor ) 178 - | _ -> 184 + | None -> 179 185 Lwt.return_some actor ) 180 - | None -> 181 - Lwt.return_some actor ) 182 - | _ -> 183 - Lwt.return_none ) 184 - dids 186 + | _ -> 187 + Lwt.return_none ) 188 + dids 189 + in 190 + let current_actor = 191 + List.find_opt (fun (a : actor) -> Some a.did = current_did) actors 192 + in 193 + Lwt.return (current_actor, actors) 185 194 186 195 let set_admin_authenticated req authenticated = 187 196 match%lwt get_session req with