objective categorical abstract machine language personal data server
0
fork

Configure Feed

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

Account identity page

futur.blue b5e62970 7aab9464

verified
+317 -1
+2
bin/main.ml
··· 33 33 ; (post, "/account", Api.Account_.Index.post_handler) 34 34 ; (get, "/account/permissions", Api.Account_.Permissions.get_handler) 35 35 ; (post, "/account/permissions", Api.Account_.Permissions.post_handler) 36 + ; (get, "/account/identity", Api.Account_.Identity.get_handler) 37 + ; (post, "/account/identity", Api.Account_.Identity.post_handler) 36 38 ; (get, "/account/login", Api.Account_.Login.get_handler) 37 39 ; (post, "/account/login", Api.Account_.Login.post_handler) 38 40 ; (get, "/account/signup", Api.Account_.Signup.get_handler)
+1
frontend/client/Router.mlx
··· 20 20 ; {path= "/account/migrate"; template= (module MigratePage)} 21 21 ; {path= "/account"; template= (module AccountPage)} 22 22 ; {path= "/account/permissions"; template= (module AccountPermissionsPage)} 23 + ; {path= "/account/identity"; template= (module AccountIdentityPage)} 23 24 ; {path= "/admin/login"; template= (module AdminLoginPage)} 24 25 ; {path= "/admin/users"; template= (module AdminUsersPage)} 25 26 ; {path= "/admin/invites"; template= (module AdminInvitesPage)}
+2 -1
frontend/src/components/AccountSidebar.mlx
··· 4 4 5 5 let pages = 6 6 [ ("Account", "/account") 7 - ; ("Permissions", "/account/permissions") ] 7 + ; ("Permissions", "/account/permissions") 8 + ; ("Identity", "/account/identity") ] 8 9 9 10 let[@react.component] make ~current_user ~logged_in_users ~active_page () = 10 11 <Sidebar
+163
frontend/src/templates/AccountIdentityPage.mlx
··· 1 + [@@@ocaml.warning "-26-27"] 2 + 3 + open Melange_json.Primitives 4 + open React 5 + 6 + type actor = AccountSwitcher.actor = 7 + {did: string; handle: string; avatar_data_uri: string option [@default None]} 8 + [@@deriving json] 9 + 10 + type props = 11 + { current_user: actor 12 + ; logged_in_users: actor list 13 + ; csrf_token: string 14 + ; is_plc: bool 15 + ; credentials_json: string option [@default None] 16 + ; error: string option [@default None] 17 + ; success: string option [@default None] } 18 + [@@deriving json] 19 + 20 + let[@react.component] make 21 + ~props: 22 + ({ current_user 23 + ; logged_in_users 24 + ; csrf_token 25 + ; is_plc 26 + ; credentials_json 27 + ; error 28 + ; success } : 29 + props ) () = 30 + let credentialsPlaceholder = Option.value credentials_json ~default:"{}" in 31 + let credentialsInput, setCredentialsInput = 32 + useState (fun () -> credentialsPlaceholder) 33 + in 34 + let loading, setLoading = useState (fun () -> false) in 35 + let errorState, setErrorState = useState (fun () -> error) in 36 + let successState, setSuccessState = useState (fun () -> success) in 37 + <div 38 + className="w-auto h-full max-w-full px-4 sm:px-0 pt-16 mx-auto flex flex-col \ 39 + md:flex-row gap-12"> 40 + <AccountSidebar 41 + current_user logged_in_users active_page="/account/identity" 42 + /> 43 + <main className="flex-1 w-full max-w-lg"> 44 + <h1 className="text-2xl font-serif text-mana-200 mb-1"> 45 + (string "identity") 46 + </h1> 47 + ( if not is_plc then 48 + <p className="text-mist-100"> 49 + (string "Identity management is only available for did:plc accounts.") 50 + </p> 51 + else 52 + <div> 53 + <p className="text-mist-100 mb-4"> 54 + (string 55 + "Managing your PLC identity could render your account \ 56 + permanently unusable. Make sure you know what you're doing!" ) 57 + </p> 58 + <ClientOnly 59 + fallback=( 60 + <div className="flex flex-col gap-y-4"> 61 + <textarea 62 + className="w-full h-96 p-3 font-mono text-sm bg-feather-100 \ 63 + border border-mist-60 rounded-xl text-mana-200 \ 64 + resize-none focus:outline-none focus:border-mana-100" 65 + value=credentialsPlaceholder 66 + disabled=true 67 + /> 68 + <Button type_="submit" disabled=true> 69 + (string "submit operation") 70 + </Button> 71 + </div> 72 + )> 73 + [%browser_only 74 + (fun () -> 75 + let submitOperation () = 76 + setLoading (fun _ -> true) ; 77 + setErrorState (fun _ -> None) ; 78 + setSuccessState (fun _ -> None) ; 79 + let body = 80 + Fetch.BodyInit.make 81 + (Webapi.Url.URLSearchParams.makeWithArray 82 + [| ("dream.csrf", csrf_token) 83 + ; ("action", "submit") 84 + ; ("credentials", credentialsInput) |] 85 + |> Webapi.Url.URLSearchParams.toString ) 86 + in 87 + let _ = 88 + Fetch.fetchWithInit "/account/identity" 89 + (Fetch.RequestInit.make ~method_:Post ~body 90 + ~headers: 91 + (Fetch.HeadersInit.makeWithArray 92 + [| ( "Content-Type" 93 + , "application/x-www-form-urlencoded" ) |] ) 94 + () ) 95 + |> Js.Promise.then_ (fun response -> 96 + setLoading (fun _ -> false) ; 97 + Fetch.Response.json response ) 98 + |> Js.Promise.then_ (fun json -> 99 + let open Js.Json in 100 + let dict = 101 + decodeObject json |> Option.value ~default:(Js.Dict.empty ()) 102 + in 103 + ( match Js.Dict.get dict "error" with 104 + | Some err_json -> 105 + let err = 106 + decodeString err_json 107 + |> Option.value ~default:"An error occurred" 108 + in 109 + setErrorState (fun _ -> Some err) 110 + | None -> 111 + setSuccessState (fun _ -> 112 + Some "Operation submitted successfully." ) ) ; 113 + Js.Promise.resolve () ) 114 + |> Js.Promise.catch (fun _ -> 115 + setLoading (fun _ -> false) ; 116 + setErrorState (fun _ -> 117 + Some "An error occurred. Please try again." ) ; 118 + Js.Promise.resolve () ) 119 + in 120 + () 121 + in 122 + <div className="flex flex-col gap-y-4"> 123 + <textarea 124 + className="w-full h-96 p-3 font-mono text-sm bg-feather-100 \ 125 + border border-mist-60 rounded-xl text-mana-200 \ 126 + resize-none focus:outline-none focus:border-mana-100" 127 + value=credentialsInput 128 + onChange=(fun e -> 129 + setCredentialsInput (fun _ -> 130 + (Event.Form.target e)##value ) ) 131 + disabled=loading 132 + /> 133 + ( match errorState with 134 + | Some err -> 135 + <span 136 + className="inline-flex items-center text-phoenix-100 \ 137 + text-sm"> 138 + <CircleAlertIcon className="w-4 h-4 mr-2" /> 139 + (string err) 140 + </span> 141 + | None -> 142 + null ) 143 + ( match successState with 144 + | Some msg -> 145 + <span 146 + className="inline-flex items-center text-mana-100 \ 147 + text-sm"> 148 + (string msg) 149 + </span> 150 + | None -> 151 + null ) 152 + <Button 153 + type_="button" 154 + disabled=loading 155 + onClick=(fun _ -> submitOperation ())> 156 + (string 157 + (if loading then "submitting..." else "submit operation") ) 158 + </Button> 159 + </div> )] 160 + </ClientOnly> 161 + </div> ) 162 + </main> 163 + </div>
+149
pegasus/lib/api/account_/identity.ml
··· 1 + let get_handler = 2 + Xrpc.handler (fun ctx -> 3 + match%lwt Session.Raw.get_current_did ctx.req with 4 + | None -> 5 + Dream.redirect ctx.req "/account/login" 6 + | Some did -> ( 7 + let%lwt current_user, logged_in_users = 8 + Session.list_logged_in_actors ctx.req ctx.db 9 + in 10 + match%lwt Data_store.get_actor_by_identifier did ctx.db with 11 + | None -> 12 + Dream.redirect ctx.req "/account/login" 13 + | Some actor -> 14 + let current_user = 15 + Option.value 16 + ~default: 17 + {did= actor.did; handle= actor.handle; avatar_data_uri= None} 18 + current_user 19 + in 20 + let csrf_token = Dream.csrf_token ctx.req in 21 + let is_plc = String.starts_with ~prefix:"did:plc:" did in 22 + let%lwt credentials_json = 23 + if is_plc then 24 + match%lwt 25 + Identity.GetRecommendedDidCredentials.get_credentials did ctx.db 26 + with 27 + | Ok credentials -> 28 + Lwt.return_some 29 + ( Plc.credentials_to_yojson credentials 30 + |> Yojson.Safe.pretty_to_string ) 31 + | Error _ -> 32 + Lwt.return_none 33 + else Lwt.return_none 34 + in 35 + Util.render_html ~title:"Identity" 36 + (module Frontend.AccountIdentityPage) 37 + ~props: 38 + { current_user 39 + ; logged_in_users 40 + ; csrf_token 41 + ; is_plc 42 + ; credentials_json 43 + ; error= None 44 + ; success= None } ) ) 45 + 46 + type post_response = {error: string option [@default None]} 47 + [@@deriving yojson {strict= false}] 48 + 49 + let post_handler = 50 + Xrpc.handler (fun ctx -> 51 + match%lwt Session.Raw.get_current_did ctx.req with 52 + | None -> 53 + Dream.json ~status:`Unauthorized 54 + (Yojson.Safe.to_string 55 + (post_response_to_yojson {error= Some "Not authenticated"}) ) 56 + | Some did -> ( 57 + if not (String.starts_with ~prefix:"did:plc:" did) then 58 + Dream.json ~status:`Bad_Request 59 + (Yojson.Safe.to_string 60 + (post_response_to_yojson 61 + {error= Some "Identity management is only for did:plc"} ) ) 62 + else 63 + match%lwt Dream.form ctx.req with 64 + | `Ok fields -> ( 65 + let action = List.assoc_opt "action" fields in 66 + match action with 67 + | Some "submit" -> ( 68 + let credentials_str = 69 + List.assoc_opt "credentials" fields 70 + |> Option.value ~default:"{}" 71 + in 72 + match 73 + Yojson.Safe.from_string credentials_str 74 + |> Plc.credentials_of_yojson 75 + with 76 + | Error e -> 77 + Dream.json ~status:`Bad_Request 78 + (Yojson.Safe.to_string 79 + (post_response_to_yojson 80 + {error= Some ("Invalid JSON: " ^ e)} ) ) 81 + | Ok credentials -> ( 82 + match%lwt Plc.get_audit_log did with 83 + | Error e -> 84 + Dream.json ~status:`Internal_Server_Error 85 + (Yojson.Safe.to_string 86 + (post_response_to_yojson 87 + {error= Some ("Failed to get audit log: " ^ e)} ) ) 88 + | Ok log -> ( 89 + let latest = Mist.Util.last log |> Option.get in 90 + let unsigned_op : Plc.unsigned_operation = 91 + Operation 92 + { type'= "plc_operation" 93 + ; rotation_keys= credentials.rotation_keys 94 + ; verification_methods= 95 + credentials.verification_methods 96 + ; also_known_as= credentials.also_known_as 97 + ; services= credentials.services 98 + ; prev= Some latest.cid } 99 + in 100 + let signed_op = 101 + Plc.sign_operation Env.rotation_key unsigned_op 102 + in 103 + match%lwt 104 + Data_store.get_actor_by_identifier did ctx.db 105 + with 106 + | None -> 107 + Dream.json ~status:`Internal_Server_Error 108 + (Yojson.Safe.to_string 109 + (post_response_to_yojson 110 + {error= Some "Actor not found"} ) ) 111 + | Some actor -> ( 112 + match 113 + Plc.validate_operation ~handle:actor.handle 114 + ~signing_key:actor.signing_key signed_op 115 + with 116 + | Error e -> 117 + Dream.json ~status:`Bad_Request 118 + (Yojson.Safe.to_string 119 + (post_response_to_yojson {error= Some e}) ) 120 + | Ok () -> ( 121 + match%lwt 122 + Plc.submit_operation did signed_op 123 + with 124 + | Ok () -> 125 + let%lwt _ = 126 + Sequencer.sequence_identity ctx.db ~did () 127 + in 128 + let%lwt _ = Id_resolver.Did.resolve did in 129 + Dream.json ~status:`OK 130 + (Yojson.Safe.to_string 131 + (post_response_to_yojson {error= None}) ) 132 + | Error (_status, msg) -> 133 + Dream.json ~status:`Bad_Request 134 + (Yojson.Safe.to_string 135 + (post_response_to_yojson 136 + { error= 137 + Some 138 + ( "The directory returned an \ 139 + error: " ^ msg ) } ) ) ) ) 140 + ) ) ) 141 + | _ -> 142 + Dream.json ~status:`Bad_Request 143 + (Yojson.Safe.to_string 144 + (post_response_to_yojson {error= Some "Invalid action"}) ) ) 145 + | _ -> 146 + Dream.json ~status:`Bad_Request 147 + (Yojson.Safe.to_string 148 + (post_response_to_yojson 149 + {error= Some "Invalid form submission"} ) ) ) )