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>