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>