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>