this repo has no description
1<script lang="ts"> 2 import { navigate } from '../lib/router.svelte' 3 import { _ } from '../lib/i18n' 4 5 let code = $state('') 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 function getChannel(): string { 15 const params = new URLSearchParams(window.location.hash.split('?')[1] || '') 16 return params.get('channel') || 'email' 17 } 18 19 async function handleSubmit(e: Event) { 20 e.preventDefault() 21 const requestUri = getRequestUri() 22 if (!requestUri) { 23 error = $_('oauth.twoFactorCode.errors.missingRequestUri') 24 return 25 } 26 27 submitting = true 28 error = null 29 30 try { 31 const response = await fetch('/oauth/authorize/2fa', { 32 method: 'POST', 33 headers: { 34 'Content-Type': 'application/json', 35 'Accept': 'application/json' 36 }, 37 body: JSON.stringify({ 38 request_uri: requestUri, 39 code: code.trim() 40 }) 41 }) 42 43 const data = await response.json() 44 45 if (!response.ok) { 46 error = data.error_description || data.error || $_('oauth.twoFactorCode.errors.verificationFailed') 47 submitting = false 48 return 49 } 50 51 if (data.redirect_uri) { 52 window.location.href = data.redirect_uri 53 return 54 } 55 56 error = $_('oauth.twoFactorCode.errors.unexpectedResponse') 57 submitting = false 58 } catch { 59 error = $_('oauth.twoFactorCode.errors.connectionFailed') 60 submitting = false 61 } 62 } 63 64 function handleCancel() { 65 const requestUri = getRequestUri() 66 if (requestUri) { 67 navigate(`/oauth/login?request_uri=${encodeURIComponent(requestUri)}`) 68 } else { 69 window.history.back() 70 } 71 } 72 73 let channel = $derived(getChannel()) 74</script> 75 76<div class="oauth-2fa-container"> 77 <h1>{$_('oauth.twoFactorCode.title')}</h1> 78 <p class="subtitle"> 79 {$_('oauth.twoFactorCode.subtitle', { values: { channel } })} 80 </p> 81 82 {#if error} 83 <div class="error">{error}</div> 84 {/if} 85 86 <form onsubmit={handleSubmit}> 87 <div class="field"> 88 <label for="code">{$_('oauth.twoFactorCode.codeLabel')}</label> 89 <input 90 id="code" 91 type="text" 92 bind:value={code} 93 placeholder={$_('oauth.twoFactorCode.codePlaceholder')} 94 disabled={submitting} 95 required 96 maxlength="6" 97 pattern="[0-9]{6}" 98 autocomplete="one-time-code" 99 inputmode="numeric" 100 /> 101 </div> 102 103 <div class="actions"> 104 <button type="button" class="cancel-btn" onclick={handleCancel} disabled={submitting}> 105 {$_('common.cancel')} 106 </button> 107 <button type="submit" class="submit-btn" disabled={submitting || code.trim().length !== 6}> 108 {submitting ? $_('oauth.twoFactorCode.verifying') : $_('oauth.twoFactorCode.verify')} 109 </button> 110 </div> 111 </form> 112</div> 113 114<style> 115 .oauth-2fa-container { 116 max-width: var(--width-sm); 117 margin: var(--space-9) auto; 118 padding: var(--space-7); 119 } 120 121 h1 { 122 margin: 0 0 var(--space-2) 0; 123 } 124 125 .subtitle { 126 color: var(--text-secondary); 127 margin: 0 0 var(--space-7) 0; 128 } 129 130 form { 131 display: flex; 132 flex-direction: column; 133 gap: var(--space-4); 134 } 135 136 .field { 137 display: flex; 138 flex-direction: column; 139 gap: var(--space-1); 140 } 141 142 label { 143 font-size: var(--text-sm); 144 font-weight: var(--font-medium); 145 } 146 147 input { 148 padding: var(--space-3); 149 border: 1px solid var(--border-color); 150 border-radius: var(--radius-md); 151 font-size: var(--text-xl); 152 letter-spacing: 0.5em; 153 text-align: center; 154 background: var(--bg-input); 155 color: var(--text-primary); 156 } 157 158 input:focus { 159 outline: none; 160 border-color: var(--accent); 161 } 162 163 .error { 164 padding: var(--space-3); 165 background: var(--error-bg); 166 border: 1px solid var(--error-border); 167 border-radius: var(--radius-md); 168 color: var(--error-text); 169 margin-bottom: var(--space-4); 170 } 171 172 .actions { 173 display: flex; 174 gap: var(--space-4); 175 margin-top: var(--space-2); 176 } 177 178 .actions button { 179 flex: 1; 180 padding: var(--space-3); 181 border: none; 182 border-radius: var(--radius-md); 183 font-size: var(--text-base); 184 cursor: pointer; 185 transition: background-color var(--transition-fast); 186 } 187 188 .actions button:disabled { 189 opacity: 0.6; 190 cursor: not-allowed; 191 } 192 193 .cancel-btn { 194 background: var(--bg-secondary); 195 color: var(--text-primary); 196 border: 1px solid var(--border-color); 197 } 198 199 .cancel-btn:hover:not(:disabled) { 200 background: var(--error-bg); 201 border-color: var(--error-border); 202 color: var(--error-text); 203 } 204 205 .submit-btn { 206 background: var(--accent); 207 color: var(--text-inverse); 208 } 209 210 .submit-btn:hover:not(:disabled) { 211 background: var(--accent-hover); 212 } 213</style>