this repo has no description
1<script lang="ts"> 2 import { navigate } from '../lib/router.svelte' 3 4 interface ScopeInfo { 5 scope: string 6 category: string 7 required: boolean 8 description: string 9 display_name: string 10 granted: boolean | null 11 } 12 13 interface ConsentData { 14 request_uri: string 15 client_id: string 16 client_name: string | null 17 client_uri: string | null 18 logo_uri: string | null 19 scopes: ScopeInfo[] 20 show_consent: boolean 21 did: string 22 } 23 24 let loading = $state(true) 25 let error = $state<string | null>(null) 26 let submitting = $state(false) 27 let consentData = $state<ConsentData | null>(null) 28 let scopeSelections = $state<Record<string, boolean>>({}) 29 let rememberChoice = $state(false) 30 31 function getRequestUri(): string | null { 32 const params = new URLSearchParams(window.location.hash.split('?')[1] || '') 33 return params.get('request_uri') 34 } 35 36 async function fetchConsentData() { 37 const requestUri = getRequestUri() 38 if (!requestUri) { 39 error = 'Missing request_uri parameter' 40 loading = false 41 return 42 } 43 44 try { 45 const response = await fetch(`/oauth/authorize/consent?request_uri=${encodeURIComponent(requestUri)}`) 46 if (!response.ok) { 47 const data = await response.json() 48 error = data.error_description || data.error || 'Failed to load consent data' 49 loading = false 50 return 51 } 52 const data: ConsentData = await response.json() 53 consentData = data 54 55 for (const scope of data.scopes) { 56 if (scope.required) { 57 scopeSelections[scope.scope] = true 58 } else if (scope.granted !== null) { 59 scopeSelections[scope.scope] = scope.granted 60 } else { 61 scopeSelections[scope.scope] = true 62 } 63 } 64 65 if (!data.show_consent) { 66 await submitConsent() 67 } 68 } catch { 69 error = 'Failed to connect to server' 70 } finally { 71 loading = false 72 } 73 } 74 75 async function submitConsent() { 76 if (!consentData) return 77 78 submitting = true 79 const approvedScopes = Object.entries(scopeSelections) 80 .filter(([_, approved]) => approved) 81 .map(([scope]) => scope) 82 83 try { 84 const response = await fetch('/oauth/authorize/consent', { 85 method: 'POST', 86 headers: { 'Content-Type': 'application/json' }, 87 body: JSON.stringify({ 88 request_uri: consentData.request_uri, 89 approved_scopes: approvedScopes, 90 remember: rememberChoice 91 }) 92 }) 93 94 if (!response.ok) { 95 const data = await response.json() 96 error = data.error_description || data.error || 'Authorization failed' 97 submitting = false 98 return 99 } 100 101 const data = await response.json() 102 if (data.redirect_uri) { 103 window.location.href = data.redirect_uri 104 } 105 } catch { 106 error = 'Failed to complete authorization' 107 submitting = false 108 } 109 } 110 111 async function handleDeny() { 112 if (!consentData) return 113 114 submitting = true 115 try { 116 const response = await fetch('/oauth/authorize/deny', { 117 method: 'POST', 118 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 119 body: `request_uri=${encodeURIComponent(consentData.request_uri)}` 120 }) 121 122 if (response.redirected) { 123 window.location.href = response.url 124 } 125 } catch { 126 error = 'Failed to deny authorization' 127 submitting = false 128 } 129 } 130 131 function handleScopeToggle(scope: string) { 132 const scopeInfo = consentData?.scopes.find(s => s.scope === scope) 133 if (scopeInfo?.required) return 134 scopeSelections[scope] = !scopeSelections[scope] 135 } 136 137 function groupScopesByCategory(scopes: ScopeInfo[]): Record<string, ScopeInfo[]> { 138 const groups: Record<string, ScopeInfo[]> = {} 139 for (const scope of scopes) { 140 if (!groups[scope.category]) { 141 groups[scope.category] = [] 142 } 143 groups[scope.category].push(scope) 144 } 145 return groups 146 } 147 148 $effect(() => { 149 fetchConsentData() 150 }) 151 152 let scopeGroups = $derived(consentData ? groupScopesByCategory(consentData.scopes) : {}) 153</script> 154 155<div class="consent-container"> 156 {#if loading} 157 <div class="loading"> 158 <p>Loading...</p> 159 </div> 160 {:else if error} 161 <div class="error-container"> 162 <h1>Authorization Error</h1> 163 <div class="error">{error}</div> 164 <button type="button" onclick={() => navigate('/login')}> 165 Return to Login 166 </button> 167 </div> 168 {:else if consentData} 169 <div class="client-info"> 170 {#if consentData.logo_uri} 171 <img src={consentData.logo_uri} alt="" class="client-logo" /> 172 {/if} 173 <h1>{consentData.client_name || 'Application'}</h1> 174 <p class="subtitle">wants to access your account</p> 175 {#if consentData.client_uri} 176 <a href={consentData.client_uri} target="_blank" rel="noopener noreferrer" class="client-link"> 177 {consentData.client_uri} 178 </a> 179 {/if} 180 </div> 181 182 <div class="account-info"> 183 <span class="label">Signing in as:</span> 184 <span class="did">{consentData.did}</span> 185 </div> 186 187 <div class="scopes-section"> 188 <h2>Permissions Requested</h2> 189 {#each Object.entries(scopeGroups) as [category, scopes]} 190 <div class="scope-group"> 191 <h3 class="category-title">{category}</h3> 192 {#each scopes as scope} 193 <label class="scope-item" class:required={scope.required}> 194 <input 195 type="checkbox" 196 checked={scopeSelections[scope.scope]} 197 disabled={scope.required || submitting} 198 onchange={() => handleScopeToggle(scope.scope)} 199 /> 200 <div class="scope-info"> 201 <span class="scope-name">{scope.display_name}</span> 202 <span class="scope-description">{scope.description}</span> 203 {#if scope.required} 204 <span class="required-badge">Required</span> 205 {/if} 206 </div> 207 </label> 208 {/each} 209 </div> 210 {/each} 211 </div> 212 213 <label class="remember-choice"> 214 <input type="checkbox" bind:checked={rememberChoice} disabled={submitting} /> 215 <span>Remember my choice for this application</span> 216 </label> 217 218 <div class="actions"> 219 <button type="button" class="deny-btn" onclick={handleDeny} disabled={submitting}> 220 Deny 221 </button> 222 <button type="button" class="approve-btn" onclick={submitConsent} disabled={submitting}> 223 {submitting ? 'Authorizing...' : 'Authorize'} 224 </button> 225 </div> 226 {/if} 227</div> 228 229<style> 230 .consent-container { 231 max-width: 480px; 232 margin: 2rem auto; 233 padding: 2rem; 234 } 235 236 .loading { 237 display: flex; 238 align-items: center; 239 justify-content: center; 240 min-height: 200px; 241 color: var(--text-secondary); 242 } 243 244 .error-container { 245 text-align: center; 246 } 247 248 .error { 249 padding: 0.75rem; 250 background: var(--error-bg); 251 border: 1px solid var(--error-border); 252 border-radius: 4px; 253 color: var(--error-text); 254 margin-bottom: 1rem; 255 } 256 257 .client-info { 258 text-align: center; 259 margin-bottom: 1.5rem; 260 } 261 262 .client-logo { 263 width: 64px; 264 height: 64px; 265 border-radius: 12px; 266 margin-bottom: 1rem; 267 } 268 269 .client-info h1 { 270 margin: 0 0 0.25rem 0; 271 font-size: 1.5rem; 272 } 273 274 .subtitle { 275 color: var(--text-secondary); 276 margin: 0; 277 } 278 279 .client-link { 280 display: inline-block; 281 margin-top: 0.5rem; 282 font-size: 0.875rem; 283 color: var(--accent); 284 text-decoration: none; 285 } 286 287 .client-link:hover { 288 text-decoration: underline; 289 } 290 291 .account-info { 292 display: flex; 293 flex-direction: column; 294 gap: 0.25rem; 295 padding: 1rem; 296 background: var(--bg-secondary); 297 border-radius: 8px; 298 margin-bottom: 1.5rem; 299 } 300 301 .account-info .label { 302 font-size: 0.75rem; 303 color: var(--text-muted); 304 text-transform: uppercase; 305 letter-spacing: 0.05em; 306 } 307 308 .account-info .did { 309 font-family: monospace; 310 font-size: 0.875rem; 311 color: var(--text-primary); 312 word-break: break-all; 313 } 314 315 .scopes-section { 316 margin-bottom: 1.5rem; 317 } 318 319 .scopes-section h2 { 320 font-size: 1rem; 321 margin: 0 0 1rem 0; 322 color: var(--text-secondary); 323 } 324 325 .scope-group { 326 margin-bottom: 1rem; 327 } 328 329 .category-title { 330 font-size: 0.875rem; 331 font-weight: 600; 332 color: var(--text-primary); 333 margin: 0 0 0.5rem 0; 334 padding-bottom: 0.25rem; 335 border-bottom: 1px solid var(--border-color); 336 } 337 338 .scope-item { 339 display: flex; 340 gap: 0.75rem; 341 padding: 0.75rem; 342 background: var(--bg-card); 343 border: 1px solid var(--border-color); 344 border-radius: 6px; 345 margin-bottom: 0.5rem; 346 cursor: pointer; 347 transition: border-color 0.15s; 348 } 349 350 .scope-item:hover:not(.required) { 351 border-color: var(--accent); 352 } 353 354 .scope-item.required { 355 background: var(--bg-secondary); 356 } 357 358 .scope-item input[type="checkbox"] { 359 flex-shrink: 0; 360 width: 18px; 361 height: 18px; 362 margin-top: 2px; 363 } 364 365 .scope-info { 366 flex: 1; 367 display: flex; 368 flex-direction: column; 369 gap: 0.125rem; 370 } 371 372 .scope-name { 373 font-weight: 500; 374 color: var(--text-primary); 375 } 376 377 .scope-description { 378 font-size: 0.875rem; 379 color: var(--text-secondary); 380 } 381 382 .required-badge { 383 display: inline-block; 384 font-size: 0.625rem; 385 padding: 0.125rem 0.375rem; 386 background: var(--warning-bg); 387 color: var(--warning-text); 388 border-radius: 3px; 389 text-transform: uppercase; 390 letter-spacing: 0.05em; 391 margin-top: 0.25rem; 392 width: fit-content; 393 } 394 395 .remember-choice { 396 display: flex; 397 align-items: center; 398 gap: 0.5rem; 399 margin-bottom: 1.5rem; 400 cursor: pointer; 401 color: var(--text-secondary); 402 font-size: 0.875rem; 403 } 404 405 .remember-choice input { 406 width: 16px; 407 height: 16px; 408 } 409 410 .actions { 411 display: flex; 412 gap: 1rem; 413 } 414 415 .actions button { 416 flex: 1; 417 padding: 0.875rem; 418 border: none; 419 border-radius: 6px; 420 font-size: 1rem; 421 font-weight: 500; 422 cursor: pointer; 423 transition: background-color 0.15s; 424 } 425 426 .actions button:disabled { 427 opacity: 0.6; 428 cursor: not-allowed; 429 } 430 431 .deny-btn { 432 background: var(--bg-secondary); 433 color: var(--text-primary); 434 border: 1px solid var(--border-color); 435 } 436 437 .deny-btn:hover:not(:disabled) { 438 background: var(--error-bg); 439 border-color: var(--error-border); 440 color: var(--error-text); 441 } 442 443 .approve-btn { 444 background: var(--accent); 445 color: white; 446 } 447 448 .approve-btn:hover:not(:disabled) { 449 background: var(--accent-hover); 450 } 451</style>