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 function getChannel(): string {
14 const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
15 return params.get('channel') || 'email'
16 }
17
18 async function handleSubmit(e: Event) {
19 e.preventDefault()
20 const requestUri = getRequestUri()
21 if (!requestUri) {
22 error = 'Missing request_uri parameter'
23 return
24 }
25
26 submitting = true
27 error = null
28
29 try {
30 const response = await fetch('/oauth/authorize/2fa', {
31 method: 'POST',
32 headers: {
33 'Content-Type': 'application/json',
34 'Accept': 'application/json'
35 },
36 body: JSON.stringify({
37 request_uri: requestUri,
38 code: code.trim()
39 })
40 })
41
42 const data = await response.json()
43
44 if (!response.ok) {
45 error = data.error_description || data.error || 'Verification failed'
46 submitting = false
47 return
48 }
49
50 if (data.redirect_uri) {
51 window.location.href = data.redirect_uri
52 return
53 }
54
55 error = 'Unexpected response from server'
56 submitting = false
57 } catch {
58 error = 'Failed to connect to server'
59 submitting = false
60 }
61 }
62
63 function handleCancel() {
64 const requestUri = getRequestUri()
65 if (requestUri) {
66 navigate(`/oauth/login?request_uri=${encodeURIComponent(requestUri)}`)
67 } else {
68 window.history.back()
69 }
70 }
71
72 let channel = $derived(getChannel())
73</script>
74
75<div class="oauth-2fa-container">
76 <h1>Two-Factor Authentication</h1>
77 <p class="subtitle">
78 A verification code has been sent to your {channel}.
79 Enter the code below to continue.
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">Verification Code</label>
89 <input
90 id="code"
91 type="text"
92 bind:value={code}
93 placeholder="Enter 6-digit code"
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 Cancel
106 </button>
107 <button type="submit" class="submit-btn" disabled={submitting || code.trim().length !== 6}>
108 {submitting ? 'Verifying...' : 'Verify'}
109 </button>
110 </div>
111 </form>
112</div>
113
114<style>
115 .oauth-2fa-container {
116 max-width: 400px;
117 margin: 4rem auto;
118 padding: 2rem;
119 }
120
121 h1 {
122 margin: 0 0 0.5rem 0;
123 }
124
125 .subtitle {
126 color: var(--text-secondary);
127 margin: 0 0 2rem 0;
128 }
129
130 form {
131 display: flex;
132 flex-direction: column;
133 gap: 1rem;
134 }
135
136 .field {
137 display: flex;
138 flex-direction: column;
139 gap: 0.25rem;
140 }
141
142 label {
143 font-size: 0.875rem;
144 font-weight: 500;
145 }
146
147 input {
148 padding: 0.75rem;
149 border: 1px solid var(--border-color-light);
150 border-radius: 4px;
151 font-size: 1.5rem;
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: 0.75rem;
165 background: var(--error-bg);
166 border: 1px solid var(--error-border);
167 border-radius: 4px;
168 color: var(--error-text);
169 margin-bottom: 1rem;
170 }
171
172 .actions {
173 display: flex;
174 gap: 1rem;
175 margin-top: 0.5rem;
176 }
177
178 .actions button {
179 flex: 1;
180 padding: 0.75rem;
181 border: none;
182 border-radius: 4px;
183 font-size: 1rem;
184 cursor: pointer;
185 transition: background-color 0.15s;
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: white;
208 }
209
210 .submit-btn:hover:not(:disabled) {
211 background: var(--accent-hover);
212 }
213</style>