this repo has no description
1<script lang="ts">
2 import { navigate } from '../lib/router.svelte'
3
4 let username = $state('')
5 let password = $state('')
6 let rememberDevice = $state(false)
7 let submitting = $state(false)
8 let error = $state<string | null>(null)
9
10 function getRequestUri(): string | null {
11 const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
12 return params.get('request_uri')
13 }
14
15 function getErrorFromUrl(): string | null {
16 const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
17 return params.get('error')
18 }
19
20 $effect(() => {
21 const urlError = getErrorFromUrl()
22 if (urlError) {
23 error = urlError
24 }
25 })
26
27 async function handleSubmit(e: Event) {
28 e.preventDefault()
29 const requestUri = getRequestUri()
30 if (!requestUri) {
31 error = 'Missing request_uri parameter'
32 return
33 }
34
35 submitting = true
36 error = null
37
38 try {
39 const response = await fetch('/oauth/authorize', {
40 method: 'POST',
41 headers: {
42 'Content-Type': 'application/json',
43 'Accept': 'application/json'
44 },
45 body: JSON.stringify({
46 request_uri: requestUri,
47 username,
48 password,
49 remember_device: rememberDevice
50 })
51 })
52
53 const data = await response.json()
54
55 if (!response.ok) {
56 error = data.error_description || data.error || 'Login failed'
57 submitting = false
58 return
59 }
60
61 if (data.needs_2fa) {
62 navigate(`/oauth/2fa?request_uri=${encodeURIComponent(requestUri)}&channel=${encodeURIComponent(data.channel || '')}`)
63 return
64 }
65
66 if (data.redirect_uri) {
67 window.location.href = data.redirect_uri
68 return
69 }
70
71 error = 'Unexpected response from server'
72 submitting = false
73 } catch {
74 error = 'Failed to connect to server'
75 submitting = false
76 }
77 }
78
79 async function handleCancel() {
80 const requestUri = getRequestUri()
81 if (!requestUri) {
82 window.history.back()
83 return
84 }
85
86 submitting = true
87 try {
88 const response = await fetch('/oauth/authorize/deny', {
89 method: 'POST',
90 headers: {
91 'Content-Type': 'application/json',
92 'Accept': 'application/json'
93 },
94 body: JSON.stringify({ request_uri: requestUri })
95 })
96
97 const data = await response.json()
98 if (data.redirect_uri) {
99 window.location.href = data.redirect_uri
100 }
101 } catch {
102 window.history.back()
103 }
104 }
105</script>
106
107<div class="oauth-login-container">
108 <h1>Sign In</h1>
109 <p class="subtitle">Sign in to continue to the application</p>
110
111 {#if error}
112 <div class="error">{error}</div>
113 {/if}
114
115 <form onsubmit={handleSubmit}>
116 <div class="field">
117 <label for="username">Handle or Email</label>
118 <input
119 id="username"
120 type="text"
121 bind:value={username}
122 placeholder="you@example.com or handle"
123 disabled={submitting}
124 required
125 autocomplete="username"
126 />
127 </div>
128
129 <div class="field">
130 <label for="password">Password</label>
131 <input
132 id="password"
133 type="password"
134 bind:value={password}
135 disabled={submitting}
136 required
137 autocomplete="current-password"
138 />
139 </div>
140
141 <label class="remember-device">
142 <input type="checkbox" bind:checked={rememberDevice} disabled={submitting} />
143 <span>Remember this device</span>
144 </label>
145
146 <div class="actions">
147 <button type="button" class="cancel-btn" onclick={handleCancel} disabled={submitting}>
148 Cancel
149 </button>
150 <button type="submit" class="submit-btn" disabled={submitting || !username || !password}>
151 {submitting ? 'Signing in...' : 'Sign In'}
152 </button>
153 </div>
154 </form>
155</div>
156
157<style>
158 .oauth-login-container {
159 max-width: 400px;
160 margin: 4rem auto;
161 padding: 2rem;
162 }
163
164 h1 {
165 margin: 0 0 0.5rem 0;
166 }
167
168 .subtitle {
169 color: var(--text-secondary);
170 margin: 0 0 2rem 0;
171 }
172
173 form {
174 display: flex;
175 flex-direction: column;
176 gap: 1rem;
177 }
178
179 .field {
180 display: flex;
181 flex-direction: column;
182 gap: 0.25rem;
183 }
184
185 label {
186 font-size: 0.875rem;
187 font-weight: 500;
188 }
189
190 input[type="text"],
191 input[type="password"] {
192 padding: 0.75rem;
193 border: 1px solid var(--border-color-light);
194 border-radius: 4px;
195 font-size: 1rem;
196 background: var(--bg-input);
197 color: var(--text-primary);
198 }
199
200 input:focus {
201 outline: none;
202 border-color: var(--accent);
203 }
204
205 .remember-device {
206 display: flex;
207 align-items: center;
208 gap: 0.5rem;
209 cursor: pointer;
210 color: var(--text-secondary);
211 font-size: 0.875rem;
212 }
213
214 .remember-device input {
215 width: 16px;
216 height: 16px;
217 }
218
219 .error {
220 padding: 0.75rem;
221 background: var(--error-bg);
222 border: 1px solid var(--error-border);
223 border-radius: 4px;
224 color: var(--error-text);
225 margin-bottom: 1rem;
226 }
227
228 .actions {
229 display: flex;
230 gap: 1rem;
231 margin-top: 0.5rem;
232 }
233
234 .actions button {
235 flex: 1;
236 padding: 0.75rem;
237 border: none;
238 border-radius: 4px;
239 font-size: 1rem;
240 cursor: pointer;
241 transition: background-color 0.15s;
242 }
243
244 .actions button:disabled {
245 opacity: 0.6;
246 cursor: not-allowed;
247 }
248
249 .cancel-btn {
250 background: var(--bg-secondary);
251 color: var(--text-primary);
252 border: 1px solid var(--border-color);
253 }
254
255 .cancel-btn:hover:not(:disabled) {
256 background: var(--error-bg);
257 border-color: var(--error-border);
258 color: var(--error-text);
259 }
260
261 .submit-btn {
262 background: var(--accent);
263 color: white;
264 }
265
266 .submit-btn:hover:not(:disabled) {
267 background: var(--accent-hover);
268 }
269</style>