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