Our Personal Data Server from scratch!
0

Configure Feed

Select the types of activity you want to include in your feed.

tranquil-pds / frontend / src / routes / RegisterSso.svelte
6.0 kB 219 lines
1<script lang="ts"> 2 import { _ } from '../lib/i18n' 3 import { getFullUrl } from '../lib/router.svelte' 4 import { routes } from '../lib/types/routes' 5 import { toast } from '../lib/toast.svelte' 6 import AccountTypeSwitcher from '../components/AccountTypeSwitcher.svelte' 7 import SsoIcon from '../components/SsoIcon.svelte' 8 import { ensureRequestUri, getRequestUriFromUrl, getOAuthRequestUri } from '../lib/oauth' 9 10 interface SsoProvider { 11 provider: string 12 name: string 13 icon: string 14 } 15 16 let providers = $state<SsoProvider[]>([]) 17 let loading = $state(true) 18 let initiating = $state<string | null>(null) 19 let initialized = false 20 21 $effect(() => { 22 if (!initialized) { 23 initialized = true 24 ensureRequestUri().then((requestUri) => { 25 if (!requestUri) return 26 fetchProviders() 27 }).catch((err) => { 28 console.error('Failed to ensure OAuth request URI:', err) 29 toast.error($_('common.error')) 30 }) 31 } 32 }) 33 34 async function fetchProviders() { 35 try { 36 const response = await fetch('/oauth/sso/providers') 37 if (response.ok) { 38 const data = await response.json() 39 providers = (data.providers || []).toSorted((a: SsoProvider, b: SsoProvider) => a.name.localeCompare(b.name)) 40 } 41 } catch (err) { 42 console.error('Failed to fetch SSO providers:', err) 43 toast.error(err instanceof Error ? err.message : $_('common.error')) 44 } finally { 45 loading = false 46 } 47 } 48 49 async function initiateRegistration(provider: string) { 50 initiating = provider 51 let requestUri = getRequestUriFromUrl() 52 53 try { 54 let response = await fetch('/oauth/sso/initiate', { 55 method: 'POST', 56 headers: { 57 'Content-Type': 'application/json', 58 'Accept': 'application/json', 59 }, 60 body: JSON.stringify({ 61 provider, 62 action: 'register', 63 request_uri: requestUri, 64 }), 65 }) 66 67 let data = await response.json() 68 69 if (!response.ok) { 70 console.log('SSO initiate failed, restarting OAuth flow', data) 71 try { 72 const newRequestUri = await getOAuthRequestUri('create') 73 const url = new URL(window.location.href) 74 url.searchParams.set('request_uri', newRequestUri) 75 window.location.href = url.toString() 76 } catch (e) { 77 console.error('Failed to restart OAuth flow:', e) 78 toast.error(data.message || data.error || $_('common.error')) 79 initiating = null 80 } 81 return 82 } 83 84 if (data.redirect_url) { 85 window.location.href = data.redirect_url 86 return 87 } 88 89 toast.error($_('common.error')) 90 initiating = null 91 } catch (err) { 92 console.error('SSO registration initiation failed:', err) 93 toast.error(err instanceof Error ? err.message : $_('common.error')) 94 initiating = null 95 } 96 } 97</script> 98 99<div class="page"> 100 <header class="page-header"> 101 <h1>{$_('register.title')}</h1> 102 <p class="subtitle">{$_('register.ssoSubtitle')}</p> 103 </header> 104 105 <div class="migrate-callout"> 106 <div class="migrate-icon"></div> 107 <div class="migrate-content"> 108 <strong>{$_('register.migrateTitle')}</strong> 109 <p>{$_('register.migrateDescription')}</p> 110 <a href={getFullUrl(routes.migrate)} class="migrate-link"> 111 {$_('register.migrateLink')}112 </a> 113 </div> 114 </div> 115 116 <AccountTypeSwitcher active="sso" ssoAvailable={providers.length > 0} oauthRequestUri={getRequestUriFromUrl()} /> 117 118 {#if loading} 119 <div class="loading"> 120 <div class="spinner md"></div> 121 </div> 122 {:else if providers.length === 0} 123 <div class="no-providers"> 124 <p>{$_('register.noSsoProviders')}</p> 125 </div> 126 {:else} 127 <div class="provider-list"> 128 <p class="provider-hint">{$_('register.ssoHint')}</p> 129 <div class="provider-grid"> 130 {#each providers as provider} 131 <button 132 class="provider-button" 133 onclick={() => initiateRegistration(provider.provider)} 134 disabled={initiating !== null} 135 > 136 <SsoIcon provider={provider.provider} size={24} /> 137 <span class="provider-name"> 138 {#if initiating === provider.provider} 139 {$_('common.loading')} 140 {:else} 141 {$_('register.continueWith', { values: { provider: provider.name } })} 142 {/if} 143 </span> 144 </button> 145 {/each} 146 </div> 147 </div> 148 {/if} 149 150 <div class="form-links"> 151 <p class="link-text"> 152 {$_('register.alreadyHaveAccount')} <a href={getFullUrl(routes.login)}>{$_('register.signIn')}</a> 153 </p> 154 </div> 155</div> 156 157<style> 158 .no-providers { 159 text-align: center; 160 padding: var(--space-8); 161 color: var(--text-secondary); 162 } 163 164 .provider-list { 165 display: flex; 166 flex-direction: column; 167 gap: var(--space-3); 168 max-width: var(--width-md); 169 } 170 171 .provider-hint { 172 color: var(--text-secondary); 173 font-size: var(--text-sm); 174 margin: 0 0 var(--space-4) 0; 175 } 176 177 .provider-grid { 178 display: grid; 179 grid-template-columns: 1fr; 180 gap: var(--space-3); 181 } 182 183 @media (min-width: 500px) { 184 .provider-grid { 185 grid-template-columns: repeat(2, 1fr); 186 } 187 } 188 189 .provider-button { 190 display: flex; 191 align-items: center; 192 gap: var(--space-3); 193 padding: var(--space-4); 194 background: var(--bg-card); 195 border: 1px solid var(--border-dark); 196 border-radius: var(--radius-lg); 197 cursor: pointer; 198 transition: all var(--transition-normal); 199 font-size: var(--text-base); 200 font-weight: var(--font-medium); 201 color: var(--text-primary); 202 text-align: left; 203 width: 100%; 204 } 205 206 .provider-button:hover:not(:disabled) { 207 background: var(--bg-secondary); 208 border-color: var(--accent); 209 } 210 211 .provider-button:disabled { 212 opacity: 0.6; 213 cursor: not-allowed; 214 } 215 216 .provider-button .provider-name { 217 flex: 1; 218 } 219</style>