this repo has no description
1<script lang="ts"> 2 import { navigate } from '../lib/router.svelte' 3 4 let code = $state('') 5 let trustDevice = $state(false) 6 let submitting = $state(false) 7 let error = $state<string | null>(null) 8 9 function getRequestUri(): string | null { 10 const params = new URLSearchParams(window.location.hash.split('?')[1] || '') 11 return params.get('request_uri') 12 } 13 14 async function handleSubmit(e: Event) { 15 e.preventDefault() 16 const requestUri = getRequestUri() 17 if (!requestUri) { 18 error = 'Missing request_uri parameter' 19 return 20 } 21 22 submitting = true 23 error = null 24 25 try { 26 const response = await fetch('/oauth/authorize/2fa', { 27 method: 'POST', 28 headers: { 29 'Content-Type': 'application/json', 30 'Accept': 'application/json' 31 }, 32 body: JSON.stringify({ 33 request_uri: requestUri, 34 code: code.trim().toUpperCase(), 35 trust_device: trustDevice 36 }) 37 }) 38 39 const data = await response.json() 40 41 if (!response.ok) { 42 error = data.error_description || data.error || 'Verification failed' 43 submitting = false 44 return 45 } 46 47 if (data.redirect_uri) { 48 window.location.href = data.redirect_uri 49 return 50 } 51 52 error = 'Unexpected response from server' 53 submitting = false 54 } catch { 55 error = 'Failed to connect to server' 56 submitting = false 57 } 58 } 59 60 function handleCancel() { 61 const requestUri = getRequestUri() 62 if (requestUri) { 63 navigate(`/oauth/login?request_uri=${encodeURIComponent(requestUri)}`) 64 } else { 65 window.history.back() 66 } 67 } 68 69 let isBackupCode = $derived(code.trim().length === 8 && /^[A-Z0-9]+$/i.test(code.trim())) 70 let isTotpCode = $derived(code.trim().length === 6 && /^[0-9]+$/.test(code.trim())) 71 let canSubmit = $derived(isBackupCode || isTotpCode) 72</script> 73 74<div class="oauth-totp-container"> 75 <h1>Two-Factor Authentication</h1> 76 <p class="subtitle"> 77 Enter the 6-digit code from your authenticator app, or use a backup code. 78 </p> 79 80 {#if error} 81 <div class="error">{error}</div> 82 {/if} 83 84 <form onsubmit={handleSubmit}> 85 <div class="field"> 86 <label for="code">Verification Code</label> 87 <input 88 id="code" 89 type="text" 90 bind:value={code} 91 placeholder="Enter code" 92 disabled={submitting} 93 required 94 maxlength="8" 95 autocomplete="one-time-code" 96 autocapitalize="characters" 97 /> 98 <p class="hint"> 99 {#if isBackupCode} 100 Using backup code 101 {:else if isTotpCode} 102 Using authenticator code 103 {:else} 104 6 digits for authenticator, 8 characters for backup code 105 {/if} 106 </p> 107 </div> 108 109 <label class="trust-device-label"> 110 <input 111 type="checkbox" 112 bind:checked={trustDevice} 113 disabled={submitting} 114 /> 115 <span>Trust this device for 30 days</span> 116 </label> 117 118 <div class="actions"> 119 <button type="button" class="cancel-btn" onclick={handleCancel} disabled={submitting}> 120 Cancel 121 </button> 122 <button type="submit" class="submit-btn" disabled={submitting || !canSubmit}> 123 {submitting ? 'Verifying...' : 'Verify'} 124 </button> 125 </div> 126 </form> 127</div> 128 129<style> 130 .oauth-totp-container { 131 max-width: 400px; 132 margin: 4rem auto; 133 padding: 2rem; 134 } 135 136 h1 { 137 margin: 0 0 0.5rem 0; 138 } 139 140 .subtitle { 141 color: var(--text-secondary); 142 margin: 0 0 2rem 0; 143 } 144 145 form { 146 display: flex; 147 flex-direction: column; 148 gap: 1rem; 149 } 150 151 .field { 152 display: flex; 153 flex-direction: column; 154 gap: 0.25rem; 155 } 156 157 label { 158 font-size: 0.875rem; 159 font-weight: 500; 160 } 161 162 input { 163 padding: 0.75rem; 164 border: 1px solid var(--border-color-light); 165 border-radius: 4px; 166 font-size: 1.5rem; 167 letter-spacing: 0.25em; 168 text-align: center; 169 background: var(--bg-input); 170 color: var(--text-primary); 171 text-transform: uppercase; 172 } 173 174 input:focus { 175 outline: none; 176 border-color: var(--accent); 177 } 178 179 .hint { 180 font-size: 0.75rem; 181 color: var(--text-muted); 182 margin: 0.25rem 0 0 0; 183 text-align: center; 184 } 185 186 .error { 187 padding: 0.75rem; 188 background: var(--error-bg); 189 border: 1px solid var(--error-border); 190 border-radius: 4px; 191 color: var(--error-text); 192 margin-bottom: 1rem; 193 } 194 195 .actions { 196 display: flex; 197 gap: 1rem; 198 margin-top: 0.5rem; 199 } 200 201 .actions button { 202 flex: 1; 203 padding: 0.75rem; 204 border: none; 205 border-radius: 4px; 206 font-size: 1rem; 207 cursor: pointer; 208 transition: background-color 0.15s; 209 } 210 211 .actions button:disabled { 212 opacity: 0.6; 213 cursor: not-allowed; 214 } 215 216 .cancel-btn { 217 background: var(--bg-secondary); 218 color: var(--text-primary); 219 border: 1px solid var(--border-color); 220 } 221 222 .cancel-btn:hover:not(:disabled) { 223 background: var(--error-bg); 224 border-color: var(--error-border); 225 color: var(--error-text); 226 } 227 228 .submit-btn { 229 background: var(--accent); 230 color: white; 231 } 232 233 .submit-btn:hover:not(:disabled) { 234 background: var(--accent-hover); 235 } 236 237 .trust-device-label { 238 display: flex; 239 align-items: center; 240 gap: 0.5rem; 241 cursor: pointer; 242 font-size: 0.875rem; 243 color: var(--text-secondary); 244 margin-top: 0.5rem; 245 } 246 247 .trust-device-label input[type="checkbox"] { 248 width: auto; 249 margin: 0; 250 } 251</style>