Our Personal Data Server from scratch! tranquil.farm
pds rust database fun oauth atproto
221
fork

Configure Feed

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

at main 236 lines 7.2 kB view raw
1<script lang="ts"> 2 import { portal } from '../lib/portal' 3 import { getAuthState, getValidToken } from '../lib/auth.svelte' 4 import { api, ApiError } from '../lib/api' 5 import { _ } from '../lib/i18n' 6 import type { Session } from '../lib/types/api' 7 import { 8 prepareRequestOptions, 9 serializeAssertionResponse, 10 type WebAuthnRequestOptionsResponse, 11 } from '../lib/webauthn' 12 13 interface Props { 14 show: boolean 15 availableMethods?: string[] 16 onSuccess: () => void 17 onCancel: () => void 18 } 19 20 let { show = $bindable(), availableMethods = ['password'], onSuccess, onCancel }: Props = $props() 21 22 const auth = $derived(getAuthState()) 23 24 function getSession(): Session | null { 25 return auth.kind === 'authenticated' ? auth.session : null 26 } 27 28 const session = $derived(getSession()) 29 let activeMethod = $state<'password' | 'totp' | 'passkey'>('password') 30 let password = $state('') 31 let totpCode = $state('') 32 let loading = $state(false) 33 let error = $state('') 34 35 $effect(() => { 36 if (show) { 37 password = '' 38 totpCode = '' 39 error = '' 40 if (availableMethods.includes('password')) { 41 activeMethod = 'password' 42 } else if (availableMethods.includes('totp')) { 43 activeMethod = 'totp' 44 } else if (availableMethods.includes('passkey')) { 45 activeMethod = 'passkey' 46 if (availableMethods.length === 1) { 47 handlePasskeyAuth() 48 } 49 } 50 } 51 }) 52 53 async function handlePasswordSubmit(e: Event) { 54 e.preventDefault() 55 if (!session || !password) return 56 loading = true 57 error = '' 58 try { 59 const token = await getValidToken() 60 if (!token) { 61 error = 'Session expired. Please log in again.' 62 return 63 } 64 await api.reauthPassword(token, password) 65 show = false 66 onSuccess() 67 } catch (e) { 68 error = e instanceof ApiError ? e.message : 'Authentication failed' 69 } finally { 70 loading = false 71 } 72 } 73 74 async function handleTotpSubmit(e: Event) { 75 e.preventDefault() 76 if (!session || !totpCode) return 77 loading = true 78 error = '' 79 try { 80 const token = await getValidToken() 81 if (!token) { 82 error = 'Session expired. Please log in again.' 83 return 84 } 85 await api.reauthTotp(token, totpCode) 86 show = false 87 onSuccess() 88 } catch (e) { 89 error = e instanceof ApiError ? e.message : 'Invalid code' 90 } finally { 91 loading = false 92 } 93 } 94 95 async function handlePasskeyAuth() { 96 if (!session) return 97 if (!window.PublicKeyCredential) { 98 error = 'Passkeys are not supported in this browser' 99 return 100 } 101 loading = true 102 error = '' 103 try { 104 const token = await getValidToken() 105 if (!token) { 106 error = 'Session expired. Please log in again.' 107 return 108 } 109 const { options } = await api.reauthPasskeyStart(token) 110 const publicKeyOptions = prepareRequestOptions(options as unknown as WebAuthnRequestOptionsResponse) 111 const credential = await navigator.credentials.get({ 112 publicKey: publicKeyOptions 113 }) 114 if (!credential) { 115 error = 'Passkey authentication was cancelled' 116 return 117 } 118 const credentialResponse = serializeAssertionResponse(credential as PublicKeyCredential) 119 await api.reauthPasskeyFinish(token, credentialResponse) 120 show = false 121 onSuccess() 122 } catch (e) { 123 if (e instanceof DOMException && e.name === 'NotAllowedError') { 124 error = 'Passkey authentication was cancelled' 125 } else { 126 error = e instanceof ApiError ? e.message : 'Passkey authentication failed' 127 } 128 } finally { 129 loading = false 130 } 131 } 132 133 function handleClose() { 134 show = false 135 onCancel() 136 } 137</script> 138 139{#if show} 140 <div class="modal-backdrop" use:portal onclick={handleClose} onkeydown={(e) => e.key === 'Escape' && handleClose()} role="presentation"> 141 <div class="modal" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} role="dialog" aria-modal="true" tabindex="-1"> 142 <div class="modal-header"> 143 <h2>{$_('reauth.title')}</h2> 144 <button class="close-btn" onclick={handleClose} aria-label="Close">&times;</button> 145 </div> 146 147 {#if error} 148 <div class="error-message">{error}</div> 149 {/if} 150 151 {#if availableMethods.length > 1} 152 <div class="tabs"> 153 {#if availableMethods.includes('password')} 154 <button 155 class="tab" 156 class:active={activeMethod === 'password'} 157 onclick={() => activeMethod = 'password'} 158 > 159 {$_('reauth.password')} 160 </button> 161 {/if} 162 {#if availableMethods.includes('totp')} 163 <button 164 class="tab" 165 class:active={activeMethod === 'totp'} 166 onclick={() => activeMethod = 'totp'} 167 > 168 {$_('reauth.totp')} 169 </button> 170 {/if} 171 {#if availableMethods.includes('passkey')} 172 <button 173 class="tab" 174 class:active={activeMethod === 'passkey'} 175 onclick={() => activeMethod = 'passkey'} 176 > 177 {$_('reauth.passkey')} 178 </button> 179 {/if} 180 </div> 181 {/if} 182 183 <div class="modal-content"> 184 {#if activeMethod === 'password'} 185 <form id="reauth-form" onsubmit={handlePasswordSubmit}> 186 <div> 187 <label for="reauth-password">{$_('reauth.password')}</label> 188 <input 189 id="reauth-password" 190 type="password" 191 bind:value={password} 192 required 193 autocomplete="current-password" 194 /> 195 </div> 196 </form> 197 {:else if activeMethod === 'totp'} 198 <form id="reauth-form" onsubmit={handleTotpSubmit}> 199 <div> 200 <label for="reauth-totp">{$_('reauth.authenticatorCode')}</label> 201 <input 202 id="reauth-totp" 203 type="text" 204 bind:value={totpCode} 205 required 206 autocomplete="one-time-code" 207 inputmode="numeric" 208 pattern="[0-9]*" 209 maxlength="6" 210 /> 211 </div> 212 </form> 213 {:else if activeMethod === 'passkey'} 214 <div class="passkey-auth"> 215 <p>{$_('reauth.usePasskey')}</p> 216 </div> 217 {/if} 218 </div> 219 220 <div class="modal-footer"> 221 <button class="secondary" onclick={handleClose} disabled={loading}> 222 {$_('reauth.cancel')} 223 </button> 224 {#if activeMethod === 'passkey'} 225 <button onclick={handlePasskeyAuth} disabled={loading}> 226 {loading ? $_('reauth.authenticating') : $_('common.verify')} 227 </button> 228 {:else} 229 <button type="submit" form="reauth-form" disabled={loading || (activeMethod === 'password' ? !password : !totpCode)}> 230 {loading ? $_('common.verifying') : $_('common.verify')} 231 </button> 232 {/if} 233 </div> 234 </div> 235 </div> 236{/if}