@mintplayer/ng-spark 22.34.0 → 22.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/mintplayer-ng-spark-auth-account.mjs +576 -2
- package/fesm2022/mintplayer-ng-spark-auth-account.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-auth-core.mjs +24 -0
- package/fesm2022/mintplayer-ng-spark-auth-core.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-auth-models.mjs +4 -1
- package/fesm2022/mintplayer-ng-spark-auth-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-auth-register.mjs +18 -5
- package/fesm2022/mintplayer-ng-spark-auth-register.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-auth-routes.mjs +22 -1
- package/fesm2022/mintplayer-ng-spark-auth-routes.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mintplayer-ng-spark-auth-account.d.ts +56 -1
- package/types/mintplayer-ng-spark-auth-core.d.ts +11 -1
- package/types/mintplayer-ng-spark-auth-models.d.ts +96 -1
- package/types/mintplayer-ng-spark-auth-register.d.ts +13 -0
- package/types/mintplayer-ng-spark-auth-routes.d.ts +15 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-auth-core.mjs","names":[],"sources":["../../auth/core/src/spark-auth.service.ts","../../auth/core/src/spark-auth-translation.service.ts"],"sourcesContent":["import { computed, inject, Injectable, Injector, NgZone, signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { Router } from '@angular/router';\nimport { firstValueFrom } from 'rxjs';\nimport {\n AuthUser,\n SPARK_AUTH_CONFIG,\n SparkAuthCapabilities,\n SPARK_EXTERNAL_LOGIN_ACK_TYPE,\n SPARK_EXTERNAL_LOGIN_CHANNEL,\n SPARK_EXTERNAL_LOGIN_ERRORS,\n SPARK_EXTERNAL_LOGIN_MESSAGE_TYPE,\n SPARK_EXTERNAL_LOGIN_QUERY_PARAM,\n SPARK_EXTERNAL_LOGIN_TIMEOUT_MS,\n SparkExternalLoginAck,\n SparkExternalLoginError,\n SparkExternalLoginMessage,\n SparkExternalLoginOptions,\n SparkExternalLoginResult,\n resolveSparkExternalLoginMode,\n sparkExternalLoginDoneKey,\n sparkExternalLoginNonce,\n sparkExternalLoginStorageKey,\n SparkExternalLogins,\n SparkPasskeyError,\n SparkPasskeyResult,\n passkeysSupported,\n sparkPasskeyError,\n SparkUnlinkResult,\n SparkAccountInfo,\n SparkAccountProfile,\n SparkAccountProfileUpdate,\n SparkAccountResult,\n SparkAuthenticatorUri,\n SparkExternalLoginTwoFactor,\n SparkTwoFactorRequest,\n SparkTwoFactorState,\n} from '@mintplayer/ng-spark/auth/models';\n\n/** How often the popup is checked for a manual close. */\nconst POPUP_CLOSE_POLL_MS = 400;\n\n@Injectable({ providedIn: 'root' })\nexport class SparkAuthService {\n private readonly http = inject(HttpClient);\n private readonly config = inject(SPARK_AUTH_CONFIG);\n private readonly zone = inject(NgZone);\n\n private readonly currentUser = signal<AuthUser | null>(null);\n\n readonly user = this.currentUser.asReadonly();\n readonly isAuthenticated = computed(() => this.currentUser()?.isAuthenticated === true);\n\n /** Resolved lazily: only {@link takeExternalLoginResult} needs the router. */\n private readonly injector = inject(Injector);\n\n private readonly pendingExternalLogin = signal(false);\n\n /**\n * `true` while a popup sign-in or link attempt is open and its popup has not been seen closed.\n *\n * ⚠️ Drive button and spinner state from this, **not** from the promise settling. When the\n * provider's login page sends COOP the popup reads closed long before its result arrives, so this\n * turns `false` early (the buttons re-enable) while the attempt keeps listening, and a late result\n * still settles it.\n */\n readonly externalLoginPending = this.pendingExternalLogin.asReadonly();\n\n /** Settles the open popup attempt; `null` when none is open. */\n private activeExternalAttempt: ((error?: SparkExternalLoginError) => void) | null = null;\n\n constructor() {\n this.checkAuth();\n }\n\n /**\n * The credentials of a sign-in that answered `RequiresTwoFactor`, held in memory only until the\n * second step. MapIdentityApi's `/login` is stateless per request: the 2FA step must repeat the\n * email and password next to the code (its `LoginRequest` requires both), or it answers 400.\n */\n private pendingTwoFactorLogin: { email: string; password: string } | null = null;\n\n /** Whether the pending sign-in (and its two-factor step) asked for a persistent cookie. */\n private pendingRememberMe = true;\n\n /**\n * The login URL. MapIdentityApi's `/login` issues a persistent cookie for `useCookies=true`\n * and a session cookie (no `Expires`) when `useSessionCookies=true` is added.\n */\n private loginUrl(rememberMe: boolean): string {\n return `${this.config.apiBasePath}/login?useCookies=true${rememberMe ? '' : '&useSessionCookies=true'}`;\n }\n\n /**\n * Signs in with a cookie. `rememberMe` (default `true`) makes the cookie persistent; `false`\n * makes it a session cookie that ends with the browser session.\n */\n async login(email: string, password: string, rememberMe = true): Promise<void> {\n this.pendingTwoFactorLogin = null;\n this.pendingRememberMe = rememberMe;\n try {\n await firstValueFrom(this.http.post<void>(this.loginUrl(rememberMe), { email, password }));\n } catch (err: any) {\n if (err?.status === 401 && err?.error?.detail === 'RequiresTwoFactor')\n this.pendingTwoFactorLogin = { email, password };\n throw err;\n }\n await this.csrfRefresh();\n await this.checkAuth();\n }\n\n async loginTwoFactor(twoFactorCode: string, twoFactorRecoveryCode?: string): Promise<void> {\n await firstValueFrom(this.http.post<void>(this.loginUrl(this.pendingRememberMe), {\n ...this.pendingTwoFactorLogin,\n twoFactorCode,\n twoFactorRecoveryCode,\n }));\n this.pendingRememberMe = true;\n this.pendingTwoFactorLogin = null;\n await this.csrfRefresh();\n await this.checkAuth();\n }\n\n /** `userName` is the public handle other users see; it is required and may not contain `@`. */\n async register(email: string, password: string, userName: string): Promise<void> {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/register`, { email, password, userName }));\n }\n\n async logout(): Promise<void> {\n this.pendingTwoFactorLogin = null;\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/logout`, {}));\n await this.csrfRefresh();\n this.currentUser.set(null);\n }\n\n /**\n * What sign-in methods this deployment actually offers.\n *\n * Read from the server rather than assumed from the client's own route configuration: the two are\n * configured independently, and a mismatch is otherwise invisible until a user hits it.\n *\n * Callers that ask while a request is already in flight share it: the login page and the\n * `<spark-external-login-buttons>` it hosts both ask on construction, and that is one request, not\n * two. Nothing is cached once it settles, so a later call still reads the server's current answer.\n */\n capabilities(): Promise<SparkAuthCapabilities> {\n return this.capabilitiesInFlight ??= firstValueFrom(\n this.http.get<SparkAuthCapabilities>(`${this.config.apiBasePath}/capabilities`))\n .finally(() => this.capabilitiesInFlight = null);\n }\n\n private capabilitiesInFlight: Promise<SparkAuthCapabilities> | null = null;\n\n async csrfRefresh(): Promise<void> {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/csrf-refresh`, {}));\n }\n\n async checkAuth(): Promise<AuthUser | null> {\n try {\n const user = await firstValueFrom(this.http.get<AuthUser>(`${this.config.apiBasePath}/me`));\n this.currentUser.set(user);\n return user;\n } catch {\n this.currentUser.set(null);\n return null;\n }\n }\n\n /**\n * Signs in through an external provider (GitHub, Google, …) and leaves this service's\n * `user()` signal up to date, exactly as `login()` does.\n *\n * The whole handshake lives here: a per-attempt nonce, four ways for the result to come back\n * (`postMessage` from the opener-held popup, a BroadcastChannel, a `localStorage` entry and its\n * `storage` event, and a re-read of that entry when this tab is shown again), the acknowledgement\n * that lets the callback page close itself, teardown on every exit path, and re-reading the\n * session afterwards. Callers get an outcome, not a protocol.\n *\n * A popup attempt settles only when its result arrives, when another attempt starts (the old one\n * settles `popup_closed`), or after 10 minutes (`popup_closed`). A popup that *looks* closed does\n * not settle it: the provider's COOP header severs the handle while the user is still signing in.\n * ⚠️ Take button and spinner state from {@link externalLoginPending}, not from this promise.\n *\n * The mode is the call's `mode`, else the configured `externalLoginMode` (`'auto'`: redirect in an\n * installed web app, popup otherwise). In `'redirect'` mode the returned promise never settles:\n * this document is being replaced, and the outcome arrives as the next page load\n * ({@link takeExternalLoginResult}) rather than as a value.\n */\n loginWithProvider(provider: string, options: SparkExternalLoginOptions = {}): Promise<SparkExternalLoginResult> {\n return this.externalFlow('/external-login', provider, options);\n }\n\n /**\n * What external logins are attached to the signed-in account, and what else could be.\n *\n * Only served when the deployment allows linking at all; under `Disabled` the endpoint is not\n * mapped, so this rejects with a 404. That is deliberate on the server side — an account page\n * offering an action the deployment forbids is worse than one that is absent.\n */\n async externalLogins(): Promise<SparkExternalLogins> {\n return await firstValueFrom(\n this.http.get<SparkExternalLogins>(`${this.config.apiBasePath}/external-logins`));\n }\n\n /**\n * Attaches another provider to the account that is already signed in.\n *\n * The same handshake as {@link loginWithProvider}, against the endpoint that links rather than\n * the one that signs in — the difference that matters is on the server, where the challenge is\n * keyed on the current user so the identity coming back cannot land in somebody else's session.\n */\n linkProvider(provider: string, options: SparkExternalLoginOptions = {}): Promise<SparkExternalLoginResult> {\n return this.externalFlow('/external-logins/link', provider, options);\n }\n\n /**\n * Detaches a provider from the signed-in account.\n *\n * ⚠️ Resolves with `error: 'last_credential'` rather than throwing when the server refuses to\n * remove the account's last way in. It is an expected answer to a reasonable request, not a\n * fault — and it is the server's refusal that protects the user, not the `canUnlink` flag the\n * list hands out.\n */\n async unlinkProvider(provider: string, providerKey: string): Promise<SparkUnlinkResult> {\n const url = `${this.config.apiBasePath}/external-logins/unlink`\n + `?provider=${encodeURIComponent(provider)}`\n + `&providerKey=${encodeURIComponent(providerKey)}`;\n try {\n await firstValueFrom(this.http.post<{ unlinked: boolean }>(url, {}));\n await this.checkAuth();\n return { success: true };\n } catch (response: unknown) {\n const error = (response as { error?: { error?: SparkUnlinkResult['error'] } })?.error?.error;\n return { success: false, error: error ?? 'unlink_failed' };\n }\n }\n\n /**\n * Reads the `?sparkExternalLogin=<code>` a redirect-mode round trip came back with, and strips it\n * from the address bar (a `replaceUrl` navigation, so Back does not bring it back and a reload\n * does not show it twice).\n *\n * Answers `null` when the URL carries none. A value that is not a known\n * {@link SparkExternalLoginError} is answered as `no_login_info` rather than echoed, so a crafted\n * link cannot put text of its choosing on the page.\n *\n * Only failures carry the parameter, and they land on the `errorUrl` the attempt was started with\n * (by default the page that started it); a successful redirect lands on the `returnUrl` signed in.\n * The shipped `<spark-external-login-buttons>` (on the sign-in and login pages) and the account\n * pages call this on load. Each failure is answered once: a second call before the strip lands\n * answers `null`. An app that starts a redirect-mode\n * attempt from some other page calls it there (or in its shell) to show the failure.\n */\n takeExternalLoginResult(): SparkExternalLoginError | null {\n const router = this.injector.get(Router, null);\n if (!router) return null;\n const tree = router.parseUrl(router.url);\n const raw = tree.queryParams[SPARK_EXTERNAL_LOGIN_QUERY_PARAM];\n if (raw === undefined) return null;\n\n // Taken once. Until the stripping navigation below lands, the URL still carries the parameter,\n // so a second component reading it in the same render would show the same failure twice.\n if (this.externalLoginResultTakenFrom === router.url) return null;\n const takenFrom = router.url;\n this.externalLoginResultTakenFrom = takenFrom;\n\n const { [SPARK_EXTERNAL_LOGIN_QUERY_PARAM]: _, ...rest } = tree.queryParams;\n tree.queryParams = rest;\n // Deferred: this is called from a component constructor, inside the navigation that is\n // activating it; starting another navigation synchronously there would cancel that one.\n queueMicrotask(() => void router.navigateByUrl(tree, { replaceUrl: true }).finally(() => {\n if (this.externalLoginResultTakenFrom === takenFrom) this.externalLoginResultTakenFrom = null;\n }));\n\n const code = Array.isArray(raw) ? raw[0] : raw;\n return SPARK_EXTERNAL_LOGIN_ERRORS.includes(code as SparkExternalLoginError)\n ? code as SparkExternalLoginError\n : 'no_login_info';\n }\n\n /** The URL whose `sparkExternalLogin` {@link takeExternalLoginResult} already took, until the strip lands. */\n private externalLoginResultTakenFrom: string | null = null;\n\n /**\n * The in-app URL of this page (path and query, as the router sees it) with any earlier\n * `sparkExternalLogin` removed, so a second failure does not stack onto the first one's code.\n * Falls back to `location` when no router is provided.\n */\n private currentUrlForExternalLoginError(): string {\n const router = this.injector.get(Router, null);\n if (router) {\n const tree = router.parseUrl(router.url);\n const { [SPARK_EXTERNAL_LOGIN_QUERY_PARAM]: _, ...rest } = tree.queryParams;\n tree.queryParams = rest;\n return router.serializeUrl(tree);\n }\n const params = new URLSearchParams(window.location.search);\n params.delete(SPARK_EXTERNAL_LOGIN_QUERY_PARAM);\n const query = params.toString();\n return window.location.pathname + (query ? `?${query}` : '');\n }\n\n /**\n * The full-page navigation of a redirect-mode attempt. Its own method only so a unit test can\n * observe the URL: jsdom locks `window.location.assign` against redefinition.\n */\n private leaveForProvider(url: string): void {\n window.location.assign(url);\n }\n\n private externalFlow(\n path: string,\n provider: string,\n options: SparkExternalLoginOptions,\n ): Promise<SparkExternalLoginResult> {\n const returnUrl = options.returnUrl ?? this.config.defaultRedirectUrl;\n const mode = options.mode ?? resolveSparkExternalLoginMode(this.config.externalLoginMode);\n const url = `${this.config.apiBasePath}${path}?provider=${encodeURIComponent(provider)}`\n + `&returnUrl=${encodeURIComponent(returnUrl)}`;\n\n // D2: a new attempt supersedes the open one, which settles 'popup_closed' and is torn down.\n this.activeExternalAttempt?.('popup_closed');\n\n if (mode === 'redirect') {\n // D9: a failure comes back to the page that started the attempt, which reads ?sparkExternalLogin.\n const errorUrl = options.errorUrl ?? this.currentUrlForExternalLoginError();\n this.leaveForProvider(`${url}&errorUrl=${encodeURIComponent(errorUrl)}`);\n return new Promise<SparkExternalLoginResult>(() => { /* the page is going away */ });\n }\n\n const nonce = sparkExternalLoginNonce();\n const popup = window.open(\n `${url}&popup=1&nonce=${nonce}&ngsw-bypass=true`, 'spark-external-login', 'width=600,height=700');\n // No automatic redirect fallback (F10): in Firefox's installed web app a null handle may still\n // have started the flow inside the app, and redirecting too would run a second one. The shipped\n // sign-in page offers \"Continue in this tab\" instead.\n if (!popup) return Promise.resolve({ success: false, error: 'popup_blocked' });\n\n return new Promise<SparkExternalLoginResult>((resolve) => {\n const storageKey = sparkExternalLoginStorageKey(nonce);\n const doneKey = sparkExternalLoginDoneKey(nonce);\n\n let channel: BroadcastChannel | null = null;\n try {\n channel = typeof BroadcastChannel === 'function' ? new BroadcastChannel(SPARK_EXTERNAL_LOGIN_CHANNEL) : null;\n } catch {\n channel = null;\n }\n\n // One settle path, so no listener, channel or timer outlives the attempt whichever way it ends.\n let settled = false;\n const settle = (error?: SparkExternalLoginError) => {\n if (settled) return;\n settled = true;\n window.removeEventListener('message', onMessage);\n window.removeEventListener('storage', onStorage);\n window.removeEventListener('focus', reread);\n document.removeEventListener('visibilitychange', onVisibility);\n if (channel) {\n channel.removeEventListener('message', onChannel);\n channel.close();\n }\n clearInterval(poll);\n clearTimeout(timeout);\n if (this.activeExternalAttempt === settle) {\n this.activeExternalAttempt = null;\n this.pendingExternalLogin.set(false);\n }\n\n this.zone.run(async () => {\n if (!error) {\n // Explicit, like login(): the identity just changed, so ask for a token bound to it\n // rather than relying on the server minting one on whatever response comes next.\n // Never fatal — the sign-in already happened, and a rejection here would leave this\n // promise unresolved.\n await this.csrfRefresh().catch(() => undefined);\n await this.checkAuth();\n }\n resolve(error ? { success: false, error } : { success: true });\n });\n };\n\n /** The first payload for this nonce wins, from whichever of the four sources brings it. */\n const deliver = (data: unknown) => {\n if (settled) return;\n const message = data as Partial<SparkExternalLoginMessage> | null;\n if (!message || typeof message !== 'object') return;\n if (message.type !== SPARK_EXTERNAL_LOGIN_MESSAGE_TYPE) return;\n // A payload for another attempt (another tab, an older popup) is not ours.\n if (message.nonce !== nonce) return;\n\n const ack: SparkExternalLoginAck = { type: SPARK_EXTERNAL_LOGIN_ACK_TYPE, nonce };\n try { channel?.postMessage(ack); } catch { /* the callback page falls back to its own close */ }\n try {\n localStorage.setItem(doneKey, '1');\n localStorage.removeItem(storageKey);\n } catch { /* storage may be unavailable (private mode); the result is already in hand */ }\n try { popup.close(); } catch { /* cut off by COOP: nothing to close from here */ }\n\n settle(message.success ? undefined : (message.error ?? 'no_login_info'));\n };\n\n const onMessage = (event: MessageEvent) => {\n if (event.origin !== window.location.origin) return;\n deliver(event.data);\n };\n const onChannel = (event: MessageEvent) => deliver(event.data);\n const readStored = () => {\n let stored: string | null = null;\n try { stored = localStorage.getItem(storageKey); } catch { return; }\n if (!stored) return;\n try { deliver(JSON.parse(stored)); } catch { /* not a payload */ }\n };\n const onStorage = (event: StorageEvent) => {\n if (event.key === storageKey && event.newValue) readStored();\n };\n // A frozen background tab may have missed both the channel and the storage event.\n const reread = () => readStored();\n const onVisibility = () => {\n if (document.visibilityState === 'visible') readStored();\n };\n\n // D2 final: the poll drives UI only. Under COOP (F6) the popup reads closed while the user is\n // still signing in, so a closed popup only re-enables the buttons; the listeners stay.\n const poll = setInterval(() => {\n let closed = false;\n try { closed = popup.closed; } catch { closed = false; }\n if (!closed) return;\n clearInterval(poll);\n if (this.activeExternalAttempt === settle) this.pendingExternalLogin.set(false);\n }, POPUP_CLOSE_POLL_MS);\n const timeout = setTimeout(() => settle('popup_closed'), SPARK_EXTERNAL_LOGIN_TIMEOUT_MS);\n\n window.addEventListener('message', onMessage);\n window.addEventListener('storage', onStorage);\n window.addEventListener('focus', reread);\n document.addEventListener('visibilitychange', onVisibility);\n channel?.addEventListener('message', onChannel);\n\n this.activeExternalAttempt = settle;\n this.pendingExternalLogin.set(true);\n });\n }\n\n async forgotPassword(email: string): Promise<void> {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/forgotPassword`, { email }));\n }\n\n async resetPassword(email: string, resetCode: string, newPassword: string): Promise<void> {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/resetPassword`, {\n email,\n resetCode,\n newPassword,\n }));\n }\n\n // ---------------------------------------------------------------------------------------------\n // Account management (#460, D16) — the endpoints behind withAccount()'s pages. Every call resolves\n // a SparkAccountResult instead of throwing: a refusal (a wrong password, an invalid culture, a\n // missing re-authentication) is an expected answer the page renders, not a fault.\n // ---------------------------------------------------------------------------------------------\n\n /** `POST /confirm-email` — the query of a mailed confirmation link (a changed email too). */\n confirmEmail(userId: string, code: string, changedEmail?: string | null): Promise<SparkAccountResult> {\n return this.accountCall(async () => {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/confirm-email`, {\n userId, code, ...(changedEmail ? { changedEmail } : {}),\n }));\n // A signed-in user's claims carry the email; re-read them after a change.\n if (this.isAuthenticated()) await this.checkAuth();\n });\n }\n\n /** `POST /manage/password` — change it (`currentPassword` required when the account has one) or set a first one. */\n setPassword(newPassword: string, currentPassword?: string | null): Promise<SparkAccountResult> {\n return this.accountCall(async () => {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/manage/password`, {\n newPassword, ...(currentPassword ? { currentPassword } : {}),\n }));\n });\n }\n\n /** `GET /manage/info` — the email and whether it is confirmed. */\n accountInfo(): Promise<SparkAccountResult<SparkAccountInfo>> {\n return this.accountCall(() => firstValueFrom(this.http.get<SparkAccountInfo>(`${this.config.apiBasePath}/manage/info`)));\n }\n\n /**\n * `POST /manage/info { newEmail }` — mails a confirmation link to the NEW address; nothing changes\n * until it is followed (the confirm-email page completes it).\n */\n changeEmail(newEmail: string): Promise<SparkAccountResult<SparkAccountInfo>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.post<SparkAccountInfo>(`${this.config.apiBasePath}/manage/info`, { newEmail })));\n }\n\n /** `GET /manage/profile`. */\n profile(): Promise<SparkAccountResult<SparkAccountProfile>> {\n return this.accountCall(() => firstValueFrom(this.http.get<SparkAccountProfile>(`${this.config.apiBasePath}/manage/profile`)));\n }\n\n /** `POST /manage/profile` — answers the saved profile, or field errors. */\n updateProfile(update: SparkAccountProfileUpdate): Promise<SparkAccountResult<SparkAccountProfile>> {\n return this.accountCall(async () => {\n const saved = await firstValueFrom(this.http.post<SparkAccountProfile>(`${this.config.apiBasePath}/manage/profile`, update));\n if (update.userName !== undefined) await this.checkAuth();\n return saved;\n });\n }\n\n /** `POST /manage/2fa` (Identity's) — an empty request reads the state and creates a key when none exists. */\n twoFactor(request: SparkTwoFactorRequest = {}): Promise<SparkAccountResult<SparkTwoFactorState>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.post<SparkTwoFactorState>(`${this.config.apiBasePath}/manage/2fa`, request)));\n }\n\n /** `GET /manage/2fa/authenticator-uri` — 409 `no_authenticator_key` until {@link twoFactor} created one. */\n authenticatorUri(): Promise<SparkAccountResult<SparkAuthenticatorUri>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.get<SparkAuthenticatorUri>(`${this.config.apiBasePath}/manage/2fa/authenticator-uri`)));\n }\n\n /** `GET /manage/external-login-two-factor` — whether an external sign-in skips the second factor. */\n externalLoginTwoFactor(): Promise<SparkAccountResult<SparkExternalLoginTwoFactor>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.get<SparkExternalLoginTwoFactor>(`${this.config.apiBasePath}/manage/external-login-two-factor`)));\n }\n\n /**\n * `POST /manage/external-login-two-factor` — turning the bypass on needs a current authenticator\n * `code` (400 when it is not valid); turning it off needs none.\n */\n setExternalLoginTwoFactor(bypass: boolean, code?: string): Promise<SparkAccountResult<SparkExternalLoginTwoFactor>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.post<SparkExternalLoginTwoFactor>(`${this.config.apiBasePath}/manage/external-login-two-factor`,\n bypass && code ? { bypass, code } : { bypass })));\n }\n\n /** `GET /manage/personal-data` — the account and every contributor's data, as JSON. */\n personalData(): Promise<SparkAccountResult<unknown>> {\n return this.accountCall(() => firstValueFrom(this.http.get<unknown>(`${this.config.apiBasePath}/manage/personal-data`)));\n }\n\n /**\n * `DELETE /manage/account` — re-authenticated by `password`, or by a sign-in younger than the server's\n * `ReauthenticationMaxAge` (5 minutes) when omitted. 403 `reauthentication_required` otherwise. On\n * success the session is gone.\n */\n deleteAccount(password?: string | null): Promise<SparkAccountResult> {\n return this.accountCall(async () => {\n await firstValueFrom(this.http.delete<void>(`${this.config.apiBasePath}/manage/account`, {\n body: password ? { password } : {},\n }));\n this.currentUser.set(null);\n // The antiforgery token was bound to the deleted identity.\n await this.csrfRefresh().catch(() => undefined);\n });\n }\n\n private async accountCall<T>(call: () => Promise<T>): Promise<SparkAccountResult<T>> {\n try {\n const value = await call();\n return { success: true, value };\n } catch (response: unknown) {\n const http = response as { status?: number; error?: unknown };\n const body = (http?.error ?? null) as { error?: unknown; errors?: Record<string, string[] | string> } | null;\n const errors: Record<string, string[]> = {};\n if (body && typeof body === 'object' && body.errors && typeof body.errors === 'object') {\n for (const [key, value] of Object.entries(body.errors)) errors[key] = Array.isArray(value) ? value : [String(value)];\n }\n return {\n success: false,\n status: http?.status,\n error: body && typeof body === 'object' && typeof body.error === 'string' ? body.error : undefined,\n errors: Object.keys(errors).length ? errors : undefined,\n };\n }\n }\n\n // ---------------------------------------------------------------------------------------------\n // Passkeys (WebAuthn)\n //\n // Sign-in is two round trips: ask the server for options, hand them to the authenticator, post what\n // it produced back. The server keeps the challenge itself — nothing below carries it, and nothing\n // below may start to, because a client that can choose its own challenge can replay an assertion.\n //\n // Managing the signed-in user's passkeys is not here: it is the generic passkeys page\n // (`/po/passkeys/me`), whose Add action runs the enrollment ceremony through the `webauthn.create`\n // client method (`sparkAuthClientMethods`).\n // ---------------------------------------------------------------------------------------------\n\n /**\n * Signs in with a passkey.\n *\n * ⚠️ Takes no username, and must not grow one. The server's request-options endpoint refuses to\n * accept an identity precisely so that its response cannot reveal whether an account exists; the\n * browser offers whatever discoverable credential it holds, and the account is resolved from the\n * credential afterwards.\n */\n async signInWithPasskey(): Promise<SparkPasskeyResult> {\n if (!passkeysSupported()) return { success: false, error: 'unsupported' };\n\n try {\n const optionsJson = await firstValueFrom(\n this.http.post(`${this.config.apiBasePath}/passkeys/request-options`, {}, { responseType: 'text' }));\n\n const options = PublicKeyCredential.parseRequestOptionsFromJSON(JSON.parse(optionsJson));\n const credential = await navigator.credentials.get({ publicKey: options });\n if (!credential) return { success: false, error: 'no_credential' };\n\n await firstValueFrom(\n this.http.post<void>(`${this.config.apiBasePath}/passkeys/sign-in`, {\n credentialJson: JSON.stringify(credential),\n }));\n\n await this.csrfRefresh();\n await this.checkAuth();\n return { success: true };\n } catch (error) {\n return { success: false, error: this.passkeyError(error) };\n }\n }\n\n /** {@link sparkPasskeyError}; shared with the `webauthn.create` client method. */\n private passkeyError(error: unknown): SparkPasskeyError {\n return sparkPasskeyError(error);\n }\n}\n","import { Injectable, inject, signal, type Signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\n\ntype TranslatedString = Record<string, string>;\n\n/** Read the global language signal set by @mintplayer/ng-spark */\nconst currentLanguage: Signal<string> =\n (globalThis as any).__sparkCurrentLanguage ?? signal('en');\n\n@Injectable({ providedIn: 'root' })\nexport class SparkAuthTranslationService {\n private readonly http = inject(HttpClient);\n private readonly translationsMap = signal<Record<string, TranslatedString>>({});\n\n constructor() {\n this.loadTranslations();\n }\n\n private async loadTranslations(): Promise<void> {\n try {\n const t = await firstValueFrom(this.http.get<Record<string, TranslatedString>>('/spark/translations'));\n this.translationsMap.set(t);\n } catch {\n // Translations failed to load; keys will be shown as-is\n }\n }\n\n t(key: string): string {\n const ts = this.translationsMap()[key];\n if (!ts) return key;\n const lang = currentLanguage();\n return ts[lang] ?? ts['en'] ?? Object.values(ts)[0] ?? key;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-auth-core.mjs","names":[],"sources":["../../auth/core/src/spark-auth.service.ts","../../auth/core/src/spark-auth-translation.service.ts"],"sourcesContent":["import { computed, inject, Injectable, Injector, NgZone, signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { Router } from '@angular/router';\nimport { firstValueFrom } from 'rxjs';\nimport {\n AuthUser,\n SPARK_AUTH_CONFIG,\n SparkAuthCapabilities,\n SPARK_EXTERNAL_LOGIN_ACK_TYPE,\n SPARK_EXTERNAL_LOGIN_CHANNEL,\n SPARK_EXTERNAL_LOGIN_ERRORS,\n SPARK_EXTERNAL_LOGIN_MESSAGE_TYPE,\n SPARK_EXTERNAL_LOGIN_QUERY_PARAM,\n SPARK_EXTERNAL_LOGIN_TIMEOUT_MS,\n SparkExternalLoginAck,\n SparkExternalLoginError,\n SparkExternalLoginMessage,\n SparkExternalLoginOptions,\n SparkExternalLoginResult,\n resolveSparkExternalLoginMode,\n sparkExternalLoginDoneKey,\n sparkExternalLoginNonce,\n sparkExternalLoginStorageKey,\n SparkExternalLogins,\n SparkPasskeyError,\n SparkPasskeyResult,\n passkeysSupported,\n sparkPasskeyError,\n SparkUnlinkResult,\n SparkAccountInfo,\n SparkAccountProfile,\n SparkAccountProfileUpdate,\n SparkAccountResult,\n SparkAuthenticatorUri,\n SparkExternalLoginTwoFactor,\n SparkTwoFactorRequest,\n SparkTwoFactorState,\n SparkAgentGrantsOverview,\n SparkAgentGrantUpdate,\n SparkAgentTokenCreated,\n SparkAgentTokenRequest,\n} from '@mintplayer/ng-spark/auth/models';\n\n/** How often the popup is checked for a manual close. */\nconst POPUP_CLOSE_POLL_MS = 400;\n\n@Injectable({ providedIn: 'root' })\nexport class SparkAuthService {\n private readonly http = inject(HttpClient);\n private readonly config = inject(SPARK_AUTH_CONFIG);\n private readonly zone = inject(NgZone);\n\n private readonly currentUser = signal<AuthUser | null>(null);\n\n readonly user = this.currentUser.asReadonly();\n readonly isAuthenticated = computed(() => this.currentUser()?.isAuthenticated === true);\n\n /** Resolved lazily: only {@link takeExternalLoginResult} needs the router. */\n private readonly injector = inject(Injector);\n\n private readonly pendingExternalLogin = signal(false);\n\n /**\n * `true` while a popup sign-in or link attempt is open and its popup has not been seen closed.\n *\n * ⚠️ Drive button and spinner state from this, **not** from the promise settling. When the\n * provider's login page sends COOP the popup reads closed long before its result arrives, so this\n * turns `false` early (the buttons re-enable) while the attempt keeps listening, and a late result\n * still settles it.\n */\n readonly externalLoginPending = this.pendingExternalLogin.asReadonly();\n\n /** Settles the open popup attempt; `null` when none is open. */\n private activeExternalAttempt: ((error?: SparkExternalLoginError) => void) | null = null;\n\n constructor() {\n this.checkAuth();\n }\n\n /**\n * The credentials of a sign-in that answered `RequiresTwoFactor`, held in memory only until the\n * second step. MapIdentityApi's `/login` is stateless per request: the 2FA step must repeat the\n * email and password next to the code (its `LoginRequest` requires both), or it answers 400.\n */\n private pendingTwoFactorLogin: { email: string; password: string } | null = null;\n\n /** Whether the pending sign-in (and its two-factor step) asked for a persistent cookie. */\n private pendingRememberMe = true;\n\n /**\n * The login URL. MapIdentityApi's `/login` issues a persistent cookie for `useCookies=true`\n * and a session cookie (no `Expires`) when `useSessionCookies=true` is added.\n */\n private loginUrl(rememberMe: boolean): string {\n return `${this.config.apiBasePath}/login?useCookies=true${rememberMe ? '' : '&useSessionCookies=true'}`;\n }\n\n /**\n * Signs in with a cookie. `rememberMe` (default `true`) makes the cookie persistent; `false`\n * makes it a session cookie that ends with the browser session.\n */\n async login(email: string, password: string, rememberMe = true): Promise<void> {\n this.pendingTwoFactorLogin = null;\n this.pendingRememberMe = rememberMe;\n try {\n await firstValueFrom(this.http.post<void>(this.loginUrl(rememberMe), { email, password }));\n } catch (err: any) {\n if (err?.status === 401 && err?.error?.detail === 'RequiresTwoFactor')\n this.pendingTwoFactorLogin = { email, password };\n throw err;\n }\n await this.csrfRefresh();\n await this.checkAuth();\n }\n\n async loginTwoFactor(twoFactorCode: string, twoFactorRecoveryCode?: string): Promise<void> {\n await firstValueFrom(this.http.post<void>(this.loginUrl(this.pendingRememberMe), {\n ...this.pendingTwoFactorLogin,\n twoFactorCode,\n twoFactorRecoveryCode,\n }));\n this.pendingRememberMe = true;\n this.pendingTwoFactorLogin = null;\n await this.csrfRefresh();\n await this.checkAuth();\n }\n\n /** `userName` is the public handle other users see; it is required and may not contain `@`. */\n async register(email: string, password: string, userName: string): Promise<void> {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/register`, { email, password, userName }));\n }\n\n async logout(): Promise<void> {\n this.pendingTwoFactorLogin = null;\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/logout`, {}));\n await this.csrfRefresh();\n this.currentUser.set(null);\n }\n\n /**\n * What sign-in methods this deployment actually offers.\n *\n * Read from the server rather than assumed from the client's own route configuration: the two are\n * configured independently, and a mismatch is otherwise invisible until a user hits it.\n *\n * Callers that ask while a request is already in flight share it: the login page and the\n * `<spark-external-login-buttons>` it hosts both ask on construction, and that is one request, not\n * two. Nothing is cached once it settles, so a later call still reads the server's current answer.\n */\n capabilities(): Promise<SparkAuthCapabilities> {\n return this.capabilitiesInFlight ??= firstValueFrom(\n this.http.get<SparkAuthCapabilities>(`${this.config.apiBasePath}/capabilities`))\n .finally(() => this.capabilitiesInFlight = null);\n }\n\n private capabilitiesInFlight: Promise<SparkAuthCapabilities> | null = null;\n\n async csrfRefresh(): Promise<void> {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/csrf-refresh`, {}));\n }\n\n async checkAuth(): Promise<AuthUser | null> {\n try {\n const user = await firstValueFrom(this.http.get<AuthUser>(`${this.config.apiBasePath}/me`));\n this.currentUser.set(user);\n return user;\n } catch {\n this.currentUser.set(null);\n return null;\n }\n }\n\n /**\n * Signs in through an external provider (GitHub, Google, …) and leaves this service's\n * `user()` signal up to date, exactly as `login()` does.\n *\n * The whole handshake lives here: a per-attempt nonce, four ways for the result to come back\n * (`postMessage` from the opener-held popup, a BroadcastChannel, a `localStorage` entry and its\n * `storage` event, and a re-read of that entry when this tab is shown again), the acknowledgement\n * that lets the callback page close itself, teardown on every exit path, and re-reading the\n * session afterwards. Callers get an outcome, not a protocol.\n *\n * A popup attempt settles only when its result arrives, when another attempt starts (the old one\n * settles `popup_closed`), or after 10 minutes (`popup_closed`). A popup that *looks* closed does\n * not settle it: the provider's COOP header severs the handle while the user is still signing in.\n * ⚠️ Take button and spinner state from {@link externalLoginPending}, not from this promise.\n *\n * The mode is the call's `mode`, else the configured `externalLoginMode` (`'auto'`: redirect in an\n * installed web app, popup otherwise). In `'redirect'` mode the returned promise never settles:\n * this document is being replaced, and the outcome arrives as the next page load\n * ({@link takeExternalLoginResult}) rather than as a value.\n */\n loginWithProvider(provider: string, options: SparkExternalLoginOptions = {}): Promise<SparkExternalLoginResult> {\n return this.externalFlow('/external-login', provider, options);\n }\n\n /**\n * What external logins are attached to the signed-in account, and what else could be.\n *\n * Only served when the deployment allows linking at all; under `Disabled` the endpoint is not\n * mapped, so this rejects with a 404. That is deliberate on the server side — an account page\n * offering an action the deployment forbids is worse than one that is absent.\n */\n async externalLogins(): Promise<SparkExternalLogins> {\n return await firstValueFrom(\n this.http.get<SparkExternalLogins>(`${this.config.apiBasePath}/external-logins`));\n }\n\n /**\n * Attaches another provider to the account that is already signed in.\n *\n * The same handshake as {@link loginWithProvider}, against the endpoint that links rather than\n * the one that signs in — the difference that matters is on the server, where the challenge is\n * keyed on the current user so the identity coming back cannot land in somebody else's session.\n */\n linkProvider(provider: string, options: SparkExternalLoginOptions = {}): Promise<SparkExternalLoginResult> {\n return this.externalFlow('/external-logins/link', provider, options);\n }\n\n /**\n * Detaches a provider from the signed-in account.\n *\n * ⚠️ Resolves with `error: 'last_credential'` rather than throwing when the server refuses to\n * remove the account's last way in. It is an expected answer to a reasonable request, not a\n * fault — and it is the server's refusal that protects the user, not the `canUnlink` flag the\n * list hands out.\n */\n async unlinkProvider(provider: string, providerKey: string): Promise<SparkUnlinkResult> {\n const url = `${this.config.apiBasePath}/external-logins/unlink`\n + `?provider=${encodeURIComponent(provider)}`\n + `&providerKey=${encodeURIComponent(providerKey)}`;\n try {\n await firstValueFrom(this.http.post<{ unlinked: boolean }>(url, {}));\n await this.checkAuth();\n return { success: true };\n } catch (response: unknown) {\n const error = (response as { error?: { error?: SparkUnlinkResult['error'] } })?.error?.error;\n return { success: false, error: error ?? 'unlink_failed' };\n }\n }\n\n /**\n * Reads the `?sparkExternalLogin=<code>` a redirect-mode round trip came back with, and strips it\n * from the address bar (a `replaceUrl` navigation, so Back does not bring it back and a reload\n * does not show it twice).\n *\n * Answers `null` when the URL carries none. A value that is not a known\n * {@link SparkExternalLoginError} is answered as `no_login_info` rather than echoed, so a crafted\n * link cannot put text of its choosing on the page.\n *\n * Only failures carry the parameter, and they land on the `errorUrl` the attempt was started with\n * (by default the page that started it); a successful redirect lands on the `returnUrl` signed in.\n * The shipped `<spark-external-login-buttons>` (on the sign-in and login pages) and the account\n * pages call this on load. Each failure is answered once: a second call before the strip lands\n * answers `null`. An app that starts a redirect-mode\n * attempt from some other page calls it there (or in its shell) to show the failure.\n */\n takeExternalLoginResult(): SparkExternalLoginError | null {\n const router = this.injector.get(Router, null);\n if (!router) return null;\n const tree = router.parseUrl(router.url);\n const raw = tree.queryParams[SPARK_EXTERNAL_LOGIN_QUERY_PARAM];\n if (raw === undefined) return null;\n\n // Taken once. Until the stripping navigation below lands, the URL still carries the parameter,\n // so a second component reading it in the same render would show the same failure twice.\n if (this.externalLoginResultTakenFrom === router.url) return null;\n const takenFrom = router.url;\n this.externalLoginResultTakenFrom = takenFrom;\n\n const { [SPARK_EXTERNAL_LOGIN_QUERY_PARAM]: _, ...rest } = tree.queryParams;\n tree.queryParams = rest;\n // Deferred: this is called from a component constructor, inside the navigation that is\n // activating it; starting another navigation synchronously there would cancel that one.\n queueMicrotask(() => void router.navigateByUrl(tree, { replaceUrl: true }).finally(() => {\n if (this.externalLoginResultTakenFrom === takenFrom) this.externalLoginResultTakenFrom = null;\n }));\n\n const code = Array.isArray(raw) ? raw[0] : raw;\n return SPARK_EXTERNAL_LOGIN_ERRORS.includes(code as SparkExternalLoginError)\n ? code as SparkExternalLoginError\n : 'no_login_info';\n }\n\n /** The URL whose `sparkExternalLogin` {@link takeExternalLoginResult} already took, until the strip lands. */\n private externalLoginResultTakenFrom: string | null = null;\n\n /**\n * The in-app URL of this page (path and query, as the router sees it) with any earlier\n * `sparkExternalLogin` removed, so a second failure does not stack onto the first one's code.\n * Falls back to `location` when no router is provided.\n */\n private currentUrlForExternalLoginError(): string {\n const router = this.injector.get(Router, null);\n if (router) {\n const tree = router.parseUrl(router.url);\n const { [SPARK_EXTERNAL_LOGIN_QUERY_PARAM]: _, ...rest } = tree.queryParams;\n tree.queryParams = rest;\n return router.serializeUrl(tree);\n }\n const params = new URLSearchParams(window.location.search);\n params.delete(SPARK_EXTERNAL_LOGIN_QUERY_PARAM);\n const query = params.toString();\n return window.location.pathname + (query ? `?${query}` : '');\n }\n\n /**\n * The full-page navigation of a redirect-mode attempt. Its own method only so a unit test can\n * observe the URL: jsdom locks `window.location.assign` against redefinition.\n */\n private leaveForProvider(url: string): void {\n window.location.assign(url);\n }\n\n private externalFlow(\n path: string,\n provider: string,\n options: SparkExternalLoginOptions,\n ): Promise<SparkExternalLoginResult> {\n const returnUrl = options.returnUrl ?? this.config.defaultRedirectUrl;\n const mode = options.mode ?? resolveSparkExternalLoginMode(this.config.externalLoginMode);\n const url = `${this.config.apiBasePath}${path}?provider=${encodeURIComponent(provider)}`\n + `&returnUrl=${encodeURIComponent(returnUrl)}`;\n\n // D2: a new attempt supersedes the open one, which settles 'popup_closed' and is torn down.\n this.activeExternalAttempt?.('popup_closed');\n\n if (mode === 'redirect') {\n // D9: a failure comes back to the page that started the attempt, which reads ?sparkExternalLogin.\n const errorUrl = options.errorUrl ?? this.currentUrlForExternalLoginError();\n this.leaveForProvider(`${url}&errorUrl=${encodeURIComponent(errorUrl)}`);\n return new Promise<SparkExternalLoginResult>(() => { /* the page is going away */ });\n }\n\n const nonce = sparkExternalLoginNonce();\n const popup = window.open(\n `${url}&popup=1&nonce=${nonce}&ngsw-bypass=true`, 'spark-external-login', 'width=600,height=700');\n // No automatic redirect fallback (F10): in Firefox's installed web app a null handle may still\n // have started the flow inside the app, and redirecting too would run a second one. The shipped\n // sign-in page offers \"Continue in this tab\" instead.\n if (!popup) return Promise.resolve({ success: false, error: 'popup_blocked' });\n\n return new Promise<SparkExternalLoginResult>((resolve) => {\n const storageKey = sparkExternalLoginStorageKey(nonce);\n const doneKey = sparkExternalLoginDoneKey(nonce);\n\n let channel: BroadcastChannel | null = null;\n try {\n channel = typeof BroadcastChannel === 'function' ? new BroadcastChannel(SPARK_EXTERNAL_LOGIN_CHANNEL) : null;\n } catch {\n channel = null;\n }\n\n // One settle path, so no listener, channel or timer outlives the attempt whichever way it ends.\n let settled = false;\n const settle = (error?: SparkExternalLoginError) => {\n if (settled) return;\n settled = true;\n window.removeEventListener('message', onMessage);\n window.removeEventListener('storage', onStorage);\n window.removeEventListener('focus', reread);\n document.removeEventListener('visibilitychange', onVisibility);\n if (channel) {\n channel.removeEventListener('message', onChannel);\n channel.close();\n }\n clearInterval(poll);\n clearTimeout(timeout);\n if (this.activeExternalAttempt === settle) {\n this.activeExternalAttempt = null;\n this.pendingExternalLogin.set(false);\n }\n\n this.zone.run(async () => {\n if (!error) {\n // Explicit, like login(): the identity just changed, so ask for a token bound to it\n // rather than relying on the server minting one on whatever response comes next.\n // Never fatal — the sign-in already happened, and a rejection here would leave this\n // promise unresolved.\n await this.csrfRefresh().catch(() => undefined);\n await this.checkAuth();\n }\n resolve(error ? { success: false, error } : { success: true });\n });\n };\n\n /** The first payload for this nonce wins, from whichever of the four sources brings it. */\n const deliver = (data: unknown) => {\n if (settled) return;\n const message = data as Partial<SparkExternalLoginMessage> | null;\n if (!message || typeof message !== 'object') return;\n if (message.type !== SPARK_EXTERNAL_LOGIN_MESSAGE_TYPE) return;\n // A payload for another attempt (another tab, an older popup) is not ours.\n if (message.nonce !== nonce) return;\n\n const ack: SparkExternalLoginAck = { type: SPARK_EXTERNAL_LOGIN_ACK_TYPE, nonce };\n try { channel?.postMessage(ack); } catch { /* the callback page falls back to its own close */ }\n try {\n localStorage.setItem(doneKey, '1');\n localStorage.removeItem(storageKey);\n } catch { /* storage may be unavailable (private mode); the result is already in hand */ }\n try { popup.close(); } catch { /* cut off by COOP: nothing to close from here */ }\n\n settle(message.success ? undefined : (message.error ?? 'no_login_info'));\n };\n\n const onMessage = (event: MessageEvent) => {\n if (event.origin !== window.location.origin) return;\n deliver(event.data);\n };\n const onChannel = (event: MessageEvent) => deliver(event.data);\n const readStored = () => {\n let stored: string | null = null;\n try { stored = localStorage.getItem(storageKey); } catch { return; }\n if (!stored) return;\n try { deliver(JSON.parse(stored)); } catch { /* not a payload */ }\n };\n const onStorage = (event: StorageEvent) => {\n if (event.key === storageKey && event.newValue) readStored();\n };\n // A frozen background tab may have missed both the channel and the storage event.\n const reread = () => readStored();\n const onVisibility = () => {\n if (document.visibilityState === 'visible') readStored();\n };\n\n // D2 final: the poll drives UI only. Under COOP (F6) the popup reads closed while the user is\n // still signing in, so a closed popup only re-enables the buttons; the listeners stay.\n const poll = setInterval(() => {\n let closed = false;\n try { closed = popup.closed; } catch { closed = false; }\n if (!closed) return;\n clearInterval(poll);\n if (this.activeExternalAttempt === settle) this.pendingExternalLogin.set(false);\n }, POPUP_CLOSE_POLL_MS);\n const timeout = setTimeout(() => settle('popup_closed'), SPARK_EXTERNAL_LOGIN_TIMEOUT_MS);\n\n window.addEventListener('message', onMessage);\n window.addEventListener('storage', onStorage);\n window.addEventListener('focus', reread);\n document.addEventListener('visibilitychange', onVisibility);\n channel?.addEventListener('message', onChannel);\n\n this.activeExternalAttempt = settle;\n this.pendingExternalLogin.set(true);\n });\n }\n\n async forgotPassword(email: string): Promise<void> {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/forgotPassword`, { email }));\n }\n\n async resetPassword(email: string, resetCode: string, newPassword: string): Promise<void> {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/resetPassword`, {\n email,\n resetCode,\n newPassword,\n }));\n }\n\n // ---------------------------------------------------------------------------------------------\n // Account management (#460, D16) — the endpoints behind withAccount()'s pages. Every call resolves\n // a SparkAccountResult instead of throwing: a refusal (a wrong password, an invalid culture, a\n // missing re-authentication) is an expected answer the page renders, not a fault.\n // ---------------------------------------------------------------------------------------------\n\n /** `POST /confirm-email` — the query of a mailed confirmation link (a changed email too). */\n confirmEmail(userId: string, code: string, changedEmail?: string | null): Promise<SparkAccountResult> {\n return this.accountCall(async () => {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/confirm-email`, {\n userId, code, ...(changedEmail ? { changedEmail } : {}),\n }));\n // A signed-in user's claims carry the email; re-read them after a change.\n if (this.isAuthenticated()) await this.checkAuth();\n });\n }\n\n /** `POST /manage/password` — change it (`currentPassword` required when the account has one) or set a first one. */\n setPassword(newPassword: string, currentPassword?: string | null): Promise<SparkAccountResult> {\n return this.accountCall(async () => {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/manage/password`, {\n newPassword, ...(currentPassword ? { currentPassword } : {}),\n }));\n });\n }\n\n /** `GET /manage/info` — the email and whether it is confirmed. */\n accountInfo(): Promise<SparkAccountResult<SparkAccountInfo>> {\n return this.accountCall(() => firstValueFrom(this.http.get<SparkAccountInfo>(`${this.config.apiBasePath}/manage/info`)));\n }\n\n /**\n * `POST /manage/info { newEmail }` — mails a confirmation link to the NEW address; nothing changes\n * until it is followed (the confirm-email page completes it).\n */\n changeEmail(newEmail: string): Promise<SparkAccountResult<SparkAccountInfo>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.post<SparkAccountInfo>(`${this.config.apiBasePath}/manage/info`, { newEmail })));\n }\n\n /** `GET /manage/profile`. */\n profile(): Promise<SparkAccountResult<SparkAccountProfile>> {\n return this.accountCall(() => firstValueFrom(this.http.get<SparkAccountProfile>(`${this.config.apiBasePath}/manage/profile`)));\n }\n\n /** `POST /manage/profile` — answers the saved profile, or field errors. */\n updateProfile(update: SparkAccountProfileUpdate): Promise<SparkAccountResult<SparkAccountProfile>> {\n return this.accountCall(async () => {\n const saved = await firstValueFrom(this.http.post<SparkAccountProfile>(`${this.config.apiBasePath}/manage/profile`, update));\n if (update.userName !== undefined) await this.checkAuth();\n return saved;\n });\n }\n\n /** `POST /manage/2fa` (Identity's) — an empty request reads the state and creates a key when none exists. */\n twoFactor(request: SparkTwoFactorRequest = {}): Promise<SparkAccountResult<SparkTwoFactorState>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.post<SparkTwoFactorState>(`${this.config.apiBasePath}/manage/2fa`, request)));\n }\n\n /** `GET /manage/2fa/authenticator-uri` — 409 `no_authenticator_key` until {@link twoFactor} created one. */\n authenticatorUri(): Promise<SparkAccountResult<SparkAuthenticatorUri>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.get<SparkAuthenticatorUri>(`${this.config.apiBasePath}/manage/2fa/authenticator-uri`)));\n }\n\n /** `GET /manage/external-login-two-factor` — whether an external sign-in skips the second factor. */\n externalLoginTwoFactor(): Promise<SparkAccountResult<SparkExternalLoginTwoFactor>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.get<SparkExternalLoginTwoFactor>(`${this.config.apiBasePath}/manage/external-login-two-factor`)));\n }\n\n /**\n * `POST /manage/external-login-two-factor` — turning the bypass on needs a current authenticator\n * `code` (400 when it is not valid); turning it off needs none.\n */\n setExternalLoginTwoFactor(bypass: boolean, code?: string): Promise<SparkAccountResult<SparkExternalLoginTwoFactor>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.post<SparkExternalLoginTwoFactor>(`${this.config.apiBasePath}/manage/external-login-two-factor`,\n bypass && code ? { bypass, code } : { bypass })));\n }\n\n /** `GET /manage/personal-data` — the account and every contributor's data, as JSON. */\n personalData(): Promise<SparkAccountResult<unknown>> {\n return this.accountCall(() => firstValueFrom(this.http.get<unknown>(`${this.config.apiBasePath}/manage/personal-data`)));\n }\n\n /**\n * `DELETE /manage/account` — re-authenticated by `password`, or by a sign-in younger than the server's\n * `ReauthenticationMaxAge` (5 minutes) when omitted. 403 `reauthentication_required` otherwise. On\n * success the session is gone.\n */\n deleteAccount(password?: string | null): Promise<SparkAccountResult> {\n return this.accountCall(async () => {\n await firstValueFrom(this.http.delete<void>(`${this.config.apiBasePath}/manage/account`, {\n body: password ? { password } : {},\n }));\n this.currentUser.set(null);\n // The antiforgery token was bound to the deleted identity.\n await this.csrfRefresh().catch(() => undefined);\n });\n }\n\n // ---------------------------------------------------------------------------------------------\n // Connected applications (MCP PRD D14) — the AI agents the user connected, their personal access\n // tokens, and what other packages contribute. Mapped only when the server has the Mcp package (or a\n // contributing package); elsewhere these answer 404.\n // ---------------------------------------------------------------------------------------------\n\n /** `GET /manage/agents` — the user's grants, what may be granted, and contributed entries. */\n agentGrants(): Promise<SparkAccountResult<SparkAgentGrantsOverview>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.get<SparkAgentGrantsOverview>(`${this.config.apiBasePath}/manage/agents`)));\n }\n\n /** `POST /manage/agents/tokens` — the answer carries the token, once; it cannot be read again. */\n createAgentToken(request: SparkAgentTokenRequest): Promise<SparkAccountResult<SparkAgentTokenCreated>> {\n return this.accountCall(() =>\n firstValueFrom(this.http.post<SparkAgentTokenCreated>(`${this.config.apiBasePath}/manage/agents/tokens`, request)));\n }\n\n /** `POST /manage/agents/update` — the agent's next call uses the new choice; no new sign-in. */\n updateAgentGrant(update: SparkAgentGrantUpdate): Promise<SparkAccountResult> {\n return this.accountCall(async () => {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/manage/agents/update`, update));\n });\n }\n\n /** `POST /manage/agents/revoke` — disconnects the agent (or revokes the token) at once. */\n revokeAgentGrant(key: string): Promise<SparkAccountResult> {\n return this.accountCall(async () => {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/manage/agents/revoke`, { key }));\n });\n }\n\n /** `POST /manage/agents/applications/revoke` — revokes an entry another package contributed. */\n revokeConnectedApplication(source: string, id: string): Promise<SparkAccountResult> {\n return this.accountCall(async () => {\n await firstValueFrom(this.http.post<void>(`${this.config.apiBasePath}/manage/agents/applications/revoke`, { source, id }));\n });\n }\n\n private async accountCall<T>(call: () => Promise<T>): Promise<SparkAccountResult<T>> {\n try {\n const value = await call();\n return { success: true, value };\n } catch (response: unknown) {\n const http = response as { status?: number; error?: unknown };\n const body = (http?.error ?? null) as { error?: unknown; errors?: Record<string, string[] | string> } | null;\n const errors: Record<string, string[]> = {};\n if (body && typeof body === 'object' && body.errors && typeof body.errors === 'object') {\n for (const [key, value] of Object.entries(body.errors)) errors[key] = Array.isArray(value) ? value : [String(value)];\n }\n return {\n success: false,\n status: http?.status,\n error: body && typeof body === 'object' && typeof body.error === 'string' ? body.error : undefined,\n errors: Object.keys(errors).length ? errors : undefined,\n };\n }\n }\n\n // ---------------------------------------------------------------------------------------------\n // Passkeys (WebAuthn)\n //\n // Sign-in is two round trips: ask the server for options, hand them to the authenticator, post what\n // it produced back. The server keeps the challenge itself — nothing below carries it, and nothing\n // below may start to, because a client that can choose its own challenge can replay an assertion.\n //\n // Managing the signed-in user's passkeys is not here: it is the generic passkeys page\n // (`/po/passkeys/me`), whose Add action runs the enrollment ceremony through the `webauthn.create`\n // client method (`sparkAuthClientMethods`).\n // ---------------------------------------------------------------------------------------------\n\n /**\n * Signs in with a passkey.\n *\n * ⚠️ Takes no username, and must not grow one. The server's request-options endpoint refuses to\n * accept an identity precisely so that its response cannot reveal whether an account exists; the\n * browser offers whatever discoverable credential it holds, and the account is resolved from the\n * credential afterwards.\n */\n async signInWithPasskey(): Promise<SparkPasskeyResult> {\n if (!passkeysSupported()) return { success: false, error: 'unsupported' };\n\n try {\n const optionsJson = await firstValueFrom(\n this.http.post(`${this.config.apiBasePath}/passkeys/request-options`, {}, { responseType: 'text' }));\n\n const options = PublicKeyCredential.parseRequestOptionsFromJSON(JSON.parse(optionsJson));\n const credential = await navigator.credentials.get({ publicKey: options });\n if (!credential) return { success: false, error: 'no_credential' };\n\n await firstValueFrom(\n this.http.post<void>(`${this.config.apiBasePath}/passkeys/sign-in`, {\n credentialJson: JSON.stringify(credential),\n }));\n\n await this.csrfRefresh();\n await this.checkAuth();\n return { success: true };\n } catch (error) {\n return { success: false, error: this.passkeyError(error) };\n }\n }\n\n /** {@link sparkPasskeyError}; shared with the `webauthn.create` client method. */\n private passkeyError(error: unknown): SparkPasskeyError {\n return sparkPasskeyError(error);\n }\n}\n","import { Injectable, inject, signal, type Signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\n\ntype TranslatedString = Record<string, string>;\n\n/** Read the global language signal set by @mintplayer/ng-spark */\nconst currentLanguage: Signal<string> =\n (globalThis as any).__sparkCurrentLanguage ?? signal('en');\n\n@Injectable({ providedIn: 'root' })\nexport class SparkAuthTranslationService {\n private readonly http = inject(HttpClient);\n private readonly translationsMap = signal<Record<string, TranslatedString>>({});\n\n constructor() {\n this.loadTranslations();\n }\n\n private async loadTranslations(): Promise<void> {\n try {\n const t = await firstValueFrom(this.http.get<Record<string, TranslatedString>>('/spark/translations'));\n this.translationsMap.set(t);\n } catch {\n // Translations failed to load; keys will be shown as-is\n }\n }\n\n t(key: string): string {\n const ts = this.translationsMap()[key];\n if (!ts) return key;\n const lang = currentLanguage();\n return ts[lang] ?? ts['en'] ?? Object.values(ts)[0] ?? key;\n }\n}\n"],"mappings":"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"}
|
|
@@ -109,6 +109,9 @@ function provideSparkAccountProfileFields(...fields) {
|
|
|
109
109
|
multi: true
|
|
110
110
|
}));
|
|
111
111
|
}
|
|
112
|
-
|
|
112
|
+
function sparkAgentCanWrite(permission) {
|
|
113
|
+
return !!permission && permission.split(",").some((p) => p.trim() === "Write");
|
|
114
|
+
}
|
|
115
|
+
export { SPARK_ACCOUNT_OVERVIEW_LINKS, SPARK_ACCOUNT_PROFILE_FIELDS, SPARK_AUTH_CONFIG, SPARK_AUTH_ROUTE_PATHS, SPARK_EXTERNAL_LOGIN_ACK_TYPE, SPARK_EXTERNAL_LOGIN_CHANNEL, SPARK_EXTERNAL_LOGIN_ERRORS, SPARK_EXTERNAL_LOGIN_MESSAGE_TYPE, SPARK_EXTERNAL_LOGIN_QUERY_PARAM, SPARK_EXTERNAL_LOGIN_TIMEOUT_MS, SPARK_EXTERNAL_PROVIDERS, defaultSparkAuthConfig, findSparkAuthRoutePaths, isSafeReturnUrl, passkeysSupported, provideSparkAccountProfileFields, resolveSignInUrl, resolveSparkExternalLoginMode, sanitizeReturnUrl, sparkAgentCanWrite, sparkExternalLoginDoneKey, sparkExternalLoginNonce, sparkExternalLoginStorageKey, sparkIsStandaloneDisplay, sparkPasskeyError };
|
|
113
116
|
|
|
114
117
|
//# sourceMappingURL=mintplayer-ng-spark-auth-models.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-auth-models.mjs","names":[],"sources":["../../auth/models/src/auth-config.ts","../../auth/models/src/auth-route-config.ts","../../auth/models/src/return-url.ts","../../auth/models/src/external-login.ts","../../auth/models/src/passkey.ts","../../auth/models/src/sign-in-redirect.ts","../../auth/models/src/account.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { SparkExternalLoginModeSetting } from './external-login';\n\nexport interface SparkAuthConfig {\n apiBasePath: string;\n defaultRedirectUrl: string;\n loginUrl: string;\n /**\n * How external sign-in and linking reach the provider by default. `'auto'` (the default) is a\n * popup in a browser tab and a full-page redirect inside an installed web app. A call's own\n * `mode` option overrides it.\n */\n externalLoginMode?: SparkExternalLoginModeSetting;\n}\n\nexport const SPARK_AUTH_CONFIG = new InjectionToken<SparkAuthConfig>('SPARK_AUTH_CONFIG');\n\nexport const defaultSparkAuthConfig: SparkAuthConfig = {\n apiBasePath: '/spark/auth',\n defaultRedirectUrl: '/',\n loginUrl: '/login',\n externalLoginMode: 'auto',\n};\n","import { InjectionToken, Type } from '@angular/core';\n\nexport type SparkAuthRouteEntry = string | { path: string; component?: Type<unknown> };\n\n/** The routable authentication pages. */\nexport interface SparkAuthRouteEntries {\n /** The provider-button landing page, mounted by `withExternalLogin()`. */\n signIn?: SparkAuthRouteEntry;\n login?: SparkAuthRouteEntry;\n twoFactor?: SparkAuthRouteEntry;\n register?: SparkAuthRouteEntry;\n forgotPassword?: SparkAuthRouteEntry;\n resetPassword?: SparkAuthRouteEntry;\n /**\n * Passkey management, mounted by `withPasskeys()`.\n *\n * Unlike its siblings this page is for a user who is already signed in, so it belongs behind\n * whatever guard the application uses for its account area rather than on the public sign-in path.\n */\n passkeys?: SparkAuthRouteEntry;\n /**\n * The link target of confirmation mails, mounted by `withAccount()`. Public (the link is opened from a\n * mailbox, possibly signed out); its path must equal the server's `Spark:Auth:Links:ConfirmEmailPath`\n * (default `/confirm-email`).\n */\n confirmEmail?: SparkAuthRouteEntry;\n /** The account overview (links to every mounted account page), mounted by `withAccount()`. */\n account?: SparkAuthRouteEntry;\n /** User name, email change, preferred mail language and app fields (`withAccount()`). */\n profile?: SparkAuthRouteEntry;\n /** Change the password, or set a first one on a social-only account (`withAccount()`). */\n changePassword?: SparkAuthRouteEntry;\n /** Authenticator enrollment, recovery codes, disabling 2FA (`withAccount()`). */\n twoFactorSetup?: SparkAuthRouteEntry;\n /** Connected external logins: link and unlink providers (`withAccount()`). */\n externalLogins?: SparkAuthRouteEntry;\n /** Download personal data and delete the account (`withAccount()`). */\n personalData?: SparkAuthRouteEntry;\n /**\n * The applications the user granted access to, with per-scope withdrawal. Mounted by\n * `withIdentityProvider(withConnectedApplications())` from `@mintplayer/ng-spark/identity-provider`.\n */\n connectedApplications?: SparkAuthRouteEntry;\n /** The developer portal's landing page (`withIdentityProvider(withDeveloperRoutes())`). */\n developers?: SparkAuthRouteEntry;\n /** The identity provider's management page (`withIdentityProvider(withManagementRoutes())`). */\n identityProviderManagement?: SparkAuthRouteEntry;\n}\n\n/**\n * An extra link on the account overview (`SparkAccountOverviewComponent`), contributed by a route\n * feature from another entry point, e.g. `withConnectedApplications()`. The overview lists it after\n * its own pages. The label carries its own translations because the contributing entry point, not\n * the auth translations, owns the text.\n */\nexport interface SparkAccountOverviewLink {\n /** Identifies the link (`track`), e.g. the {@link SparkAuthRoutePaths} key it points at. */\n key: string;\n /** The absolute router path, e.g. `/account/applications`. */\n path: string;\n /** The link text per language code; resolved in the app's current language. */\n label: Record<string, string>;\n}\n\n/**\n * The extra account-overview links of the mounted route features, provided by `sparkAuthRoutes()`\n * on its route. Optional: without it the overview shows only the auth pages.\n */\nexport const SPARK_ACCOUNT_OVERVIEW_LINKS = new InjectionToken<readonly SparkAccountOverviewLink[]>(\n 'SPARK_ACCOUNT_OVERVIEW_LINKS',\n);\n\n/**\n * The paths of the pages that were actually mounted.\n *\n * Partial, and that is the change: pages are opted into individually now, so a template cannot\n * assume its siblings exist. `login` links to `register` only when registration was opted into, and\n * the sign-in landing page links to `login` only when local login was. A `[routerLink]` bound to\n * `undefined` silently navigates to the current route, so every cross-feature link is guarded.\n */\nexport type SparkAuthRoutePaths = Partial<Record<keyof SparkAuthRouteEntries, string>>;\n\nexport const SPARK_AUTH_ROUTE_PATHS = new InjectionToken<SparkAuthRoutePaths>('SPARK_AUTH_ROUTE_PATHS');\n\n/** The slice of an Angular `Route` the lookup below reads — kept structural so `/models` stays router-free. */\ninterface RouteLike {\n providers?: readonly unknown[];\n children?: readonly RouteLike[];\n}\n\n/**\n * The paths `sparkAuthRoutes()` mounted, read from the router configuration.\n *\n * For components that live **outside** that route subtree, such as `<spark-auth-bar>` in the\n * application shell: `SPARK_AUTH_ROUTE_PATHS` is provided on the subtree's own route, so it is not\n * injectable from there. Walks the eagerly declared routes only — a `sparkAuthRoutes()` behind a\n * `loadChildren` is not loaded yet and is not found, which reads as \"nothing mounted\".\n */\nexport function findSparkAuthRoutePaths(routes: readonly RouteLike[] | undefined): SparkAuthRoutePaths | null {\n for (const route of routes ?? []) {\n for (const provider of route.providers ?? []) {\n const p = provider as { provide?: unknown; useValue?: SparkAuthRoutePaths };\n if (p?.provide === SPARK_AUTH_ROUTE_PATHS && p.useValue) return p.useValue;\n }\n const nested = findSparkAuthRoutePaths(route.children);\n if (nested) return nested;\n }\n return null;\n}\n\n/**\n * Presentation for one external provider's button — an icon, a label, an ordering.\n *\n * **It decorates; it does not declare.** The server stays authoritative over which providers exist\n * (`GET /spark/auth/capabilities`), because letting the client declare them would reintroduce exactly\n * the string-literal mismatch that endpoint exists to prevent. A scheme the server reports with no\n * matching declaration falls back to the default button, so adding a provider server-side never\n * yields a blank page; a declaration matching no reported scheme is simply unused.\n */\nexport interface SparkExternalProviderPresentation {\n /** The ASP.NET Core authentication scheme, as the server reports it. Matched case-insensitively. */\n scheme: string;\n /** Overrides the server's display name. */\n displayName?: string;\n /** A CSS class for an icon element rendered before the label. */\n iconClass?: string;\n /** Lower sorts first. Providers with no declared order keep the server's order, after declared ones. */\n order?: number;\n}\n\nexport const SPARK_EXTERNAL_PROVIDERS = new InjectionToken<SparkExternalProviderPresentation[]>(\n 'SPARK_EXTERNAL_PROVIDERS',\n);\n","/**\n * Shared validator for `returnUrl` query parameters consumed by the auth\n * components and interceptor. The framework-side equivalent in\n * `SparkAuthenticationExtensions.SanitizeReturnUrl` enforces the same rule;\n * keep them in sync.\n *\n * Accepts only local in-app paths:\n * - Must start with a single `/`.\n * - Must not start with `//` or `/\\` (protocol-relative navigation that some\n * browsers and routers interpret as an external host).\n * - Must not contain CR/LF (defensive — the value flows into Router state and\n * eventually back into a `router.navigateByUrl` call).\n *\n * Returns the supplied default for anything else, never throws. Per R2-H9.\n */\nexport function isSafeReturnUrl(value: string | null | undefined): boolean {\n if (!value) return false;\n if (/[\\r\\n]/.test(value)) return false;\n if (!value.startsWith('/')) return false;\n if (value.length >= 2 && (value[1] === '/' || value[1] === '\\\\')) return false;\n return true;\n}\n\nexport function sanitizeReturnUrl(\n value: string | null | undefined,\n defaultUrl: string,\n): string {\n return isSafeReturnUrl(value) ? (value as string) : defaultUrl;\n}\n","/** How the user is sent to the external provider, and how the result comes back. */\nexport type SparkExternalLoginMode = 'popup' | 'redirect';\n\n/**\n * The app-wide default for {@link SparkExternalLoginMode}, set through\n * `provideSparkAuth({ externalLoginMode })`.\n *\n * `'auto'` (the default) uses `'redirect'` inside an installed web app (`display-mode: standalone`,\n * or iOS's `navigator.standalone`) and `'popup'` in a browser tab. Inside an installed app a popup is\n * captured into the app, or lands in a custom tab with no usable opener, so only a full-page\n * redirect is reliable there.\n */\nexport type SparkExternalLoginModeSetting = 'auto' | SparkExternalLoginMode;\n\nexport interface SparkExternalLoginOptions {\n /**\n * Where to land after a successful provider round trip. Server-side sanitized to an in-app path.\n */\n returnUrl?: string;\n /**\n * `'redirect'` mode only: where a failure lands, with `?sparkExternalLogin=<code>` added.\n * Server-side sanitized like `returnUrl`. Defaults to the page the attempt started from (the\n * router URL with any earlier `sparkExternalLogin` removed), which is the page that reads and\n * shows the code. Ignored in popup mode, where the outcome comes back to the opener instead.\n */\n errorUrl?: string;\n /** Overrides the configured `externalLoginMode` (default `'auto'`) for this one call. */\n mode?: SparkExternalLoginMode;\n}\n\n/** Whether this document runs as an installed web app rather than in a browser tab. */\nexport function sparkIsStandaloneDisplay(): boolean {\n if (typeof window === 'undefined') return false;\n const standaloneQuery = typeof window.matchMedia === 'function'\n && window.matchMedia('(display-mode: standalone)')?.matches === true;\n return standaloneQuery || (window.navigator as { standalone?: boolean } | undefined)?.standalone === true;\n}\n\n/**\n * The value of `externalLoginMode` resolved against this document: `'auto'` becomes `'redirect'` in\n * an installed web app and `'popup'` everywhere else.\n */\nexport function resolveSparkExternalLoginMode(setting: SparkExternalLoginModeSetting | undefined): SparkExternalLoginMode {\n if (setting === 'popup' || setting === 'redirect') return setting;\n return sparkIsStandaloneDisplay() ? 'redirect' : 'popup';\n}\n\n/**\n * Why an external login did not sign anyone in. Everything but the two `popup_*` codes comes\n * from the server; those two are raised here, because the browser is the only place that can\n * observe them.\n *\n * ⚠️ `link_confirmation_sent` is **not a failure**. It travels on this channel because no session\n * was created, which is what `success: false` means to the opener — but the right response is to\n * tell the user to check their mail, not to show an error.\n *\n * ⚠️ `email_already_registered` admits that an account exists, which the older codes deliberately\n * avoid doing. That trade is made on the server (the alternative was a generic failure that reads\n * as \"this application is broken\"); a client must not widen it by pairing the code with the address\n * it was asked about.\n */\nexport type SparkExternalLoginError =\n | 'no_login_info'\n | 'email_not_verified'\n | 'account_creation_failed'\n | 'email_already_registered'\n | 'sign_in_to_link'\n | 'link_confirmation_sent'\n | 'login_already_associated'\n | 'link_failed'\n /** The provider reported an error, or the correlation of its answer failed. */\n | 'remote_failure'\n /** The challenge refused the nonce (HTTP 400). It never travels in a popup payload. */\n | 'invalid_nonce'\n | 'popup_blocked'\n | 'popup_closed';\n\n/** Every {@link SparkExternalLoginError}; a code read from a URL is checked against it. */\nexport const SPARK_EXTERNAL_LOGIN_ERRORS: readonly SparkExternalLoginError[] = [\n 'no_login_info', 'email_not_verified', 'account_creation_failed', 'email_already_registered',\n 'sign_in_to_link', 'link_confirmation_sent', 'login_already_associated', 'link_failed',\n 'remote_failure', 'invalid_nonce', 'popup_blocked', 'popup_closed',\n];\n\nexport interface SparkExternalLoginResult {\n success: boolean;\n error?: SparkExternalLoginError;\n}\n\n/**\n * The payload the callback page hands back: posted to the opener, posted on the\n * {@link SPARK_EXTERNAL_LOGIN_CHANNEL} BroadcastChannel, and written to `localStorage` under\n * {@link sparkExternalLoginStorageKey} (with an `at` timestamp). Any of the three may be the one\n * that arrives: COOP on the provider's pages cuts the opener, and a frozen background tab may only\n * see the storage entry once it is shown again.\n */\nexport interface SparkExternalLoginMessage {\n type: typeof SPARK_EXTERNAL_LOGIN_MESSAGE_TYPE;\n success: boolean;\n error?: SparkExternalLoginError | null;\n /** The nonce the opener sent on the challenge; `null` from a challenge that carried none. */\n nonce?: string | null;\n}\n\n/** The opener's answer on the BroadcastChannel: the callback page sets its done-marker and closes. */\nexport interface SparkExternalLoginAck {\n type: typeof SPARK_EXTERNAL_LOGIN_ACK_TYPE;\n nonce: string;\n}\n\nexport const SPARK_EXTERNAL_LOGIN_MESSAGE_TYPE = 'spark:external-login';\nexport const SPARK_EXTERNAL_LOGIN_ACK_TYPE = 'spark:external-login-ack';\n\n/** The BroadcastChannel the callback page and the opener share. */\nexport const SPARK_EXTERNAL_LOGIN_CHANNEL = 'spark:external-login';\n\n/** The query parameter a redirect-mode failure comes back with. */\nexport const SPARK_EXTERNAL_LOGIN_QUERY_PARAM = 'sparkExternalLogin';\n\n/** How long a popup attempt waits for its result before it settles `popup_closed`. */\nexport const SPARK_EXTERNAL_LOGIN_TIMEOUT_MS = 10 * 60 * 1000;\n\n/** The `localStorage` key the callback page writes its payload to. */\nexport function sparkExternalLoginStorageKey(nonce: string): string {\n return `spark:external-login:${nonce}`;\n}\n\n/**\n * The `localStorage` done-marker: set once the opener has the result. The callback page reads it to\n * restore itself to the app when an installed web app shows it again.\n */\nexport function sparkExternalLoginDoneKey(nonce: string): string {\n return `spark:external-login-done:${nonce}`;\n}\n\n/**\n * A fresh nonce for one popup attempt: 32 base64url characters from `crypto.getRandomValues`\n * (24 bytes, so no padding). The server accepts `^[A-Za-z0-9_-]{16,64}$`.\n */\nexport function sparkExternalLoginNonce(): string {\n const bytes = new Uint8Array(24);\n crypto.getRandomValues(bytes);\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_');\n}\n\n/** One external login attached to the signed-in account. */\nexport interface SparkLinkedExternalLogin {\n provider: string;\n providerKey: string;\n displayName: string;\n /**\n * ⚠️ `false` when removing this login would leave the account with no way to sign in.\n *\n * Served by the server so the UI can disable the control rather than offer an action that will\n * be refused. It is **not** the enforcement — the server refuses the request too — and a client\n * that treats it as advisory only is behaving correctly.\n */\n canUnlink: boolean;\n}\n\n/** A provider that is configured but not yet attached to the signed-in account. */\nexport interface SparkAvailableExternalProvider {\n provider: string;\n displayName: string;\n}\n\nexport interface SparkExternalLogins {\n linked: SparkLinkedExternalLogin[];\n available: SparkAvailableExternalProvider[];\n}\n\n/** Why a login was not detached. */\nexport type SparkUnlinkError =\n /** ⚠️ It was the account's last way in, so removing it would have locked the owner out for good. */\n | 'last_credential'\n /** Not attached to this account. */\n | 'login_not_found'\n /** The store refused, for a reason that is not either of the above. */\n | 'unlink_failed';\n\nexport interface SparkUnlinkResult {\n success: boolean;\n error?: SparkUnlinkError;\n}\n","/**\n * Why a passkey ceremony did not complete.\n *\n * ⚠️ Every server-side sign-in rejection arrives as `failed`, and that is deliberate rather than\n * lossy: distinguishing \"unknown credential\" from \"bad signature\" would tell an anonymous caller\n * whether a credential exists. Do not try to recover the detail — it is not sent.\n *\n * The three codes the server never sends are raised here, because the browser is the only place\n * that can observe them.\n */\nexport type SparkPasskeyError =\n /** The browser cannot run the ceremony at all. Offer another sign-in method. */\n | 'unsupported'\n /** The user dismissed the authenticator prompt. Not an error worth showing. */\n | 'cancelled'\n /** The authenticator produced nothing — no matching credential, or the user gave up. */\n | 'no_credential'\n /** The account is locked out. The one server-side outcome worth distinguishing. */\n | 'locked_out'\n /** Everything else, server or client. */\n | 'failed';\n\nexport interface SparkPasskeyResult {\n success: boolean;\n error?: SparkPasskeyError;\n}\n\n/**\n * Collapses everything that can go wrong in a ceremony into the closed error union.\n *\n * `AbortError` and `NotAllowedError` are how a browser reports \"the user dismissed the prompt\" and\n * \"no credential was produced\" — neither is a fault, and neither should surface as a red banner.\n */\nexport function sparkPasskeyError(error: unknown): SparkPasskeyError {\n if (error instanceof DOMException) {\n if (error.name === 'AbortError') return 'cancelled';\n if (error.name === 'NotAllowedError') return 'no_credential';\n return 'failed';\n }\n\n const code = (error as { error?: { error?: string } })?.error?.error;\n if (code === 'locked_out') return 'locked_out';\n\n return 'failed';\n}\n\n/**\n * Whether this browser can run a passkey ceremony.\n *\n * Checks the native JSON helpers as well as `navigator.credentials`, because the ceremony is built\n * on them: the server speaks WebAuthn's JSON encoding, and without\n * `parseCreationOptionsFromJSON` / `parseRequestOptionsFromJSON` the client would have to hand-roll\n * base64url conversion over every field of the options — on the security-critical path, to support\n * browsers that a user holding a passkey is unlikely to be running.\n *\n * ⚠️ `window.isSecureContext` is part of the test rather than an afterthought: WebAuthn is\n * unavailable on an insecure origin, and without this the button would render and then fail at the\n * moment the user clicks it.\n */\nexport function passkeysSupported(): boolean {\n return (\n typeof window !== 'undefined' &&\n window.isSecureContext === true &&\n typeof PublicKeyCredential !== 'undefined' &&\n typeof navigator?.credentials?.create === 'function' &&\n typeof navigator?.credentials?.get === 'function' &&\n typeof PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&\n typeof PublicKeyCredential.parseRequestOptionsFromJSON === 'function'\n );\n}\n","import { isDevMode } from '@angular/core';\nimport { Route, Router } from '@angular/router';\nimport { SparkAuthConfig } from './auth-config';\n\n/**\n * Where to send someone who is not signed in.\n *\n * `loginUrl` is the single source of truth for this, and the only thing the guard, the interceptor\n * and the auth bar should consult — an application that turns off local credentials points it at\n * its own sign-in landing route.\n *\n * The reason this is a function rather than a property read is the failure mode it closes: nothing\n * connects `loginUrl` to the routes that actually exist, so pointing it at a route that was never\n * registered produces a redirect into a blank page with no type error, no build failure and no\n * runtime error. In development that now warns, once, naming the value and the fix.\n */\nexport function resolveSignInUrl(config: SparkAuthConfig, router: Router): string {\n if (isDevMode() && !warned && !routeExists(router, config.loginUrl)) {\n warned = true;\n console.warn(\n `[ng-spark-auth] loginUrl is \"${config.loginUrl}\", but no route matches it, so redirecting an `\n + `unauthenticated user will land on a blank page. If this app runs with local credentials `\n + `disabled, point loginUrl at its sign-in route (for example provideSparkAuth({ loginUrl: '/sign-in' })).`,\n );\n }\n\n return config.loginUrl;\n}\n\nlet warned = false;\n\nfunction routeExists(router: Router, url: string): boolean {\n const target = url.replace(/^\\/+/, '').split('?')[0].split('#')[0];\n return matches(router.config, target);\n}\n\nfunction matches(routes: Route[], target: string, prefix = ''): boolean {\n return routes.some(route => {\n const path = [prefix, route.path ?? ''].filter(Boolean).join('/');\n if (path === target) return true;\n // A wildcard route would match anything, including a URL nobody registered — treating it as a\n // match would make this check always pass and warn about nothing.\n return route.children ? matches(route.children, target, path) : false;\n });\n}\n","import { InjectionToken, Provider } from '@angular/core';\n\n/** `GET/POST /spark/auth/manage/profile` (#460, D16). */\nexport interface SparkAccountProfile {\n userName: string | null;\n email: string | null;\n /** App-contributed fields (`ISparkProfileContributor<TUser>`), by name. */\n fields: Record<string, unknown>;\n /** The culture account mail is written in (`SparkUser.PreferredCulture`); null = the app default. */\n preferredCulture: string | null;\n}\n\n/** The body of `POST /spark/auth/manage/profile`. Absent keeps a value; `preferredCulture: null` clears it. */\nexport interface SparkAccountProfileUpdate {\n userName?: string | null;\n fields?: Record<string, unknown>;\n preferredCulture?: string | null;\n}\n\n/** `GET /spark/auth/manage/info` / answer of `POST manage/info`. */\nexport interface SparkAccountInfo {\n email: string;\n isEmailConfirmed: boolean;\n}\n\n/** Answer of `POST /spark/auth/manage/2fa` (ASP.NET Core Identity's contract). */\nexport interface SparkTwoFactorState {\n sharedKey: string | null;\n recoveryCodesLeft: number;\n /** Only present right after codes were (re)generated — show them once. */\n recoveryCodes?: string[] | null;\n isTwoFactorEnabled: boolean;\n isMachineRemembered: boolean;\n}\n\n/** The body of `POST /spark/auth/manage/2fa`. An empty body reads the state (and creates a key if none). */\nexport interface SparkTwoFactorRequest {\n enable?: boolean;\n twoFactorCode?: string;\n resetSharedKey?: boolean;\n resetRecoveryCodes?: boolean;\n forgetMachine?: boolean;\n}\n\n/** `GET /spark/auth/manage/2fa/authenticator-uri`: the key, its otpauth URI and a server-rendered QR SVG. */\n/**\n * `GET`/`POST /manage/external-login-two-factor`: whether a sign-in with an external account skips\n * the second factor for this user, and whether two-factor is enabled at all.\n */\nexport interface SparkExternalLoginTwoFactor {\n bypass: boolean;\n twoFactorEnabled: boolean;\n}\n\nexport interface SparkAuthenticatorUri {\n sharedKey: string;\n authenticatorUri: string;\n qrCodeSvg: string;\n}\n\n/**\n * The outcome of an account call that can fail with a message for the user.\n *\n * `errors` flattens an RFC 7807 validation problem (Identity codes, `PreferredCulture`, contributor\n * fields) into `field → messages`; `error` carries a Spark error code (`reauthentication_required`,\n * `no_authenticator_key`, …) when the server sent one.\n */\nexport interface SparkAccountResult<T = void> {\n success: boolean;\n value?: T;\n error?: string;\n errors?: Record<string, string[]>;\n status?: number;\n}\n\n/**\n * One application field on the profile page (#460, D16), validated and stored server-side by an\n * `ISparkProfileContributor<TUser>` declaring the same `name`.\n */\nexport interface SparkAccountProfileField {\n /** The field name, as the contributor declares it (matched case-insensitively by the server). */\n name: string;\n /** A translation key (looked up with the `t` pipe) or literal text. */\n label: string;\n type?: 'text' | 'textarea' | 'email' | 'url' | 'tel' | 'number' | 'date' | 'checkbox' | 'select';\n /** For `select`. `label` is a translation key or literal text. */\n options?: { value: string; label: string }[];\n required?: boolean;\n maxLength?: number;\n /** A translation key or literal text shown under the control. */\n hint?: string;\n /** Ascending; unordered fields follow in registration order. */\n order?: number;\n}\n\n/** Multi-provider: the application's profile fields. Use {@link provideSparkAccountProfileFields}. */\nexport const SPARK_ACCOUNT_PROFILE_FIELDS = new InjectionToken<SparkAccountProfileField[]>('SPARK_ACCOUNT_PROFILE_FIELDS');\n\n/** Registers profile fields; every call adds. */\nexport function provideSparkAccountProfileFields(...fields: SparkAccountProfileField[]): Provider[] {\n return fields.map(field => ({ provide: SPARK_ACCOUNT_PROFILE_FIELDS, useValue: field, multi: true }));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-auth-models.mjs","names":[],"sources":["../../auth/models/src/auth-config.ts","../../auth/models/src/auth-route-config.ts","../../auth/models/src/return-url.ts","../../auth/models/src/external-login.ts","../../auth/models/src/passkey.ts","../../auth/models/src/sign-in-redirect.ts","../../auth/models/src/account.ts","../../auth/models/src/agents.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { SparkExternalLoginModeSetting } from './external-login';\n\nexport interface SparkAuthConfig {\n apiBasePath: string;\n defaultRedirectUrl: string;\n loginUrl: string;\n /**\n * How external sign-in and linking reach the provider by default. `'auto'` (the default) is a\n * popup in a browser tab and a full-page redirect inside an installed web app. A call's own\n * `mode` option overrides it.\n */\n externalLoginMode?: SparkExternalLoginModeSetting;\n}\n\nexport const SPARK_AUTH_CONFIG = new InjectionToken<SparkAuthConfig>('SPARK_AUTH_CONFIG');\n\nexport const defaultSparkAuthConfig: SparkAuthConfig = {\n apiBasePath: '/spark/auth',\n defaultRedirectUrl: '/',\n loginUrl: '/login',\n externalLoginMode: 'auto',\n};\n","import { InjectionToken, Type } from '@angular/core';\n\nexport type SparkAuthRouteEntry = string | { path: string; component?: Type<unknown> };\n\n/** The routable authentication pages. */\nexport interface SparkAuthRouteEntries {\n /** The provider-button landing page, mounted by `withExternalLogin()`. */\n signIn?: SparkAuthRouteEntry;\n login?: SparkAuthRouteEntry;\n twoFactor?: SparkAuthRouteEntry;\n register?: SparkAuthRouteEntry;\n forgotPassword?: SparkAuthRouteEntry;\n resetPassword?: SparkAuthRouteEntry;\n /**\n * Passkey management, mounted by `withPasskeys()`.\n *\n * Unlike its siblings this page is for a user who is already signed in, so it belongs behind\n * whatever guard the application uses for its account area rather than on the public sign-in path.\n */\n passkeys?: SparkAuthRouteEntry;\n /**\n * The link target of confirmation mails, mounted by `withAccount()`. Public (the link is opened from a\n * mailbox, possibly signed out); its path must equal the server's `Spark:Auth:Links:ConfirmEmailPath`\n * (default `/confirm-email`).\n */\n confirmEmail?: SparkAuthRouteEntry;\n /** The account overview (links to every mounted account page), mounted by `withAccount()`. */\n account?: SparkAuthRouteEntry;\n /** User name, email change, preferred mail language and app fields (`withAccount()`). */\n profile?: SparkAuthRouteEntry;\n /** Change the password, or set a first one on a social-only account (`withAccount()`). */\n changePassword?: SparkAuthRouteEntry;\n /** Authenticator enrollment, recovery codes, disabling 2FA (`withAccount()`). */\n twoFactorSetup?: SparkAuthRouteEntry;\n /** Connected external logins: link and unlink providers (`withAccount()`). */\n externalLogins?: SparkAuthRouteEntry;\n /** Download personal data and delete the account (`withAccount()`). */\n personalData?: SparkAuthRouteEntry;\n /**\n * The applications the user granted access to, with per-scope withdrawal. Mounted by\n * `withIdentityProvider(withConnectedApplications())` from `@mintplayer/ng-spark/identity-provider`.\n */\n connectedApplications?: SparkAuthRouteEntry;\n /**\n * The AI agents and personal access tokens the user connected (MCP), mounted by `withConnectedAgents()`.\n * Needs the server's `MintPlayer.Spark.Mcp` package.\n */\n agents?: SparkAuthRouteEntry;\n /** The developer portal's landing page (`withIdentityProvider(withDeveloperRoutes())`). */\n developers?: SparkAuthRouteEntry;\n /** The identity provider's management page (`withIdentityProvider(withManagementRoutes())`). */\n identityProviderManagement?: SparkAuthRouteEntry;\n}\n\n/**\n * An extra link on the account overview (`SparkAccountOverviewComponent`), contributed by a route\n * feature from another entry point, e.g. `withConnectedApplications()`. The overview lists it after\n * its own pages. The label carries its own translations because the contributing entry point, not\n * the auth translations, owns the text.\n */\nexport interface SparkAccountOverviewLink {\n /** Identifies the link (`track`), e.g. the {@link SparkAuthRoutePaths} key it points at. */\n key: string;\n /** The absolute router path, e.g. `/account/applications`. */\n path: string;\n /** The link text per language code; resolved in the app's current language. */\n label: Record<string, string>;\n}\n\n/**\n * The extra account-overview links of the mounted route features, provided by `sparkAuthRoutes()`\n * on its route. Optional: without it the overview shows only the auth pages.\n */\nexport const SPARK_ACCOUNT_OVERVIEW_LINKS = new InjectionToken<readonly SparkAccountOverviewLink[]>(\n 'SPARK_ACCOUNT_OVERVIEW_LINKS',\n);\n\n/**\n * The paths of the pages that were actually mounted.\n *\n * Partial, and that is the change: pages are opted into individually now, so a template cannot\n * assume its siblings exist. `login` links to `register` only when registration was opted into, and\n * the sign-in landing page links to `login` only when local login was. A `[routerLink]` bound to\n * `undefined` silently navigates to the current route, so every cross-feature link is guarded.\n */\nexport type SparkAuthRoutePaths = Partial<Record<keyof SparkAuthRouteEntries, string>>;\n\nexport const SPARK_AUTH_ROUTE_PATHS = new InjectionToken<SparkAuthRoutePaths>('SPARK_AUTH_ROUTE_PATHS');\n\n/** The slice of an Angular `Route` the lookup below reads — kept structural so `/models` stays router-free. */\ninterface RouteLike {\n providers?: readonly unknown[];\n children?: readonly RouteLike[];\n}\n\n/**\n * The paths `sparkAuthRoutes()` mounted, read from the router configuration.\n *\n * For components that live **outside** that route subtree, such as `<spark-auth-bar>` in the\n * application shell: `SPARK_AUTH_ROUTE_PATHS` is provided on the subtree's own route, so it is not\n * injectable from there. Walks the eagerly declared routes only — a `sparkAuthRoutes()` behind a\n * `loadChildren` is not loaded yet and is not found, which reads as \"nothing mounted\".\n */\nexport function findSparkAuthRoutePaths(routes: readonly RouteLike[] | undefined): SparkAuthRoutePaths | null {\n for (const route of routes ?? []) {\n for (const provider of route.providers ?? []) {\n const p = provider as { provide?: unknown; useValue?: SparkAuthRoutePaths };\n if (p?.provide === SPARK_AUTH_ROUTE_PATHS && p.useValue) return p.useValue;\n }\n const nested = findSparkAuthRoutePaths(route.children);\n if (nested) return nested;\n }\n return null;\n}\n\n/**\n * Presentation for one external provider's button — an icon, a label, an ordering.\n *\n * **It decorates; it does not declare.** The server stays authoritative over which providers exist\n * (`GET /spark/auth/capabilities`), because letting the client declare them would reintroduce exactly\n * the string-literal mismatch that endpoint exists to prevent. A scheme the server reports with no\n * matching declaration falls back to the default button, so adding a provider server-side never\n * yields a blank page; a declaration matching no reported scheme is simply unused.\n */\nexport interface SparkExternalProviderPresentation {\n /** The ASP.NET Core authentication scheme, as the server reports it. Matched case-insensitively. */\n scheme: string;\n /** Overrides the server's display name. */\n displayName?: string;\n /** A CSS class for an icon element rendered before the label. */\n iconClass?: string;\n /** Lower sorts first. Providers with no declared order keep the server's order, after declared ones. */\n order?: number;\n}\n\nexport const SPARK_EXTERNAL_PROVIDERS = new InjectionToken<SparkExternalProviderPresentation[]>(\n 'SPARK_EXTERNAL_PROVIDERS',\n);\n","/**\n * Shared validator for `returnUrl` query parameters consumed by the auth\n * components and interceptor. The framework-side equivalent in\n * `SparkAuthenticationExtensions.SanitizeReturnUrl` enforces the same rule;\n * keep them in sync.\n *\n * Accepts only local in-app paths:\n * - Must start with a single `/`.\n * - Must not start with `//` or `/\\` (protocol-relative navigation that some\n * browsers and routers interpret as an external host).\n * - Must not contain CR/LF (defensive — the value flows into Router state and\n * eventually back into a `router.navigateByUrl` call).\n *\n * Returns the supplied default for anything else, never throws. Per R2-H9.\n */\nexport function isSafeReturnUrl(value: string | null | undefined): boolean {\n if (!value) return false;\n if (/[\\r\\n]/.test(value)) return false;\n if (!value.startsWith('/')) return false;\n if (value.length >= 2 && (value[1] === '/' || value[1] === '\\\\')) return false;\n return true;\n}\n\nexport function sanitizeReturnUrl(\n value: string | null | undefined,\n defaultUrl: string,\n): string {\n return isSafeReturnUrl(value) ? (value as string) : defaultUrl;\n}\n","/** How the user is sent to the external provider, and how the result comes back. */\nexport type SparkExternalLoginMode = 'popup' | 'redirect';\n\n/**\n * The app-wide default for {@link SparkExternalLoginMode}, set through\n * `provideSparkAuth({ externalLoginMode })`.\n *\n * `'auto'` (the default) uses `'redirect'` inside an installed web app (`display-mode: standalone`,\n * or iOS's `navigator.standalone`) and `'popup'` in a browser tab. Inside an installed app a popup is\n * captured into the app, or lands in a custom tab with no usable opener, so only a full-page\n * redirect is reliable there.\n */\nexport type SparkExternalLoginModeSetting = 'auto' | SparkExternalLoginMode;\n\nexport interface SparkExternalLoginOptions {\n /**\n * Where to land after a successful provider round trip. Server-side sanitized to an in-app path.\n */\n returnUrl?: string;\n /**\n * `'redirect'` mode only: where a failure lands, with `?sparkExternalLogin=<code>` added.\n * Server-side sanitized like `returnUrl`. Defaults to the page the attempt started from (the\n * router URL with any earlier `sparkExternalLogin` removed), which is the page that reads and\n * shows the code. Ignored in popup mode, where the outcome comes back to the opener instead.\n */\n errorUrl?: string;\n /** Overrides the configured `externalLoginMode` (default `'auto'`) for this one call. */\n mode?: SparkExternalLoginMode;\n}\n\n/** Whether this document runs as an installed web app rather than in a browser tab. */\nexport function sparkIsStandaloneDisplay(): boolean {\n if (typeof window === 'undefined') return false;\n const standaloneQuery = typeof window.matchMedia === 'function'\n && window.matchMedia('(display-mode: standalone)')?.matches === true;\n return standaloneQuery || (window.navigator as { standalone?: boolean } | undefined)?.standalone === true;\n}\n\n/**\n * The value of `externalLoginMode` resolved against this document: `'auto'` becomes `'redirect'` in\n * an installed web app and `'popup'` everywhere else.\n */\nexport function resolveSparkExternalLoginMode(setting: SparkExternalLoginModeSetting | undefined): SparkExternalLoginMode {\n if (setting === 'popup' || setting === 'redirect') return setting;\n return sparkIsStandaloneDisplay() ? 'redirect' : 'popup';\n}\n\n/**\n * Why an external login did not sign anyone in. Everything but the two `popup_*` codes comes\n * from the server; those two are raised here, because the browser is the only place that can\n * observe them.\n *\n * ⚠️ `link_confirmation_sent` is **not a failure**. It travels on this channel because no session\n * was created, which is what `success: false` means to the opener — but the right response is to\n * tell the user to check their mail, not to show an error.\n *\n * ⚠️ `email_already_registered` admits that an account exists, which the older codes deliberately\n * avoid doing. That trade is made on the server (the alternative was a generic failure that reads\n * as \"this application is broken\"); a client must not widen it by pairing the code with the address\n * it was asked about.\n */\nexport type SparkExternalLoginError =\n | 'no_login_info'\n | 'email_not_verified'\n | 'account_creation_failed'\n | 'email_already_registered'\n | 'sign_in_to_link'\n | 'link_confirmation_sent'\n | 'login_already_associated'\n | 'link_failed'\n /** The provider reported an error, or the correlation of its answer failed. */\n | 'remote_failure'\n /** The challenge refused the nonce (HTTP 400). It never travels in a popup payload. */\n | 'invalid_nonce'\n | 'popup_blocked'\n | 'popup_closed';\n\n/** Every {@link SparkExternalLoginError}; a code read from a URL is checked against it. */\nexport const SPARK_EXTERNAL_LOGIN_ERRORS: readonly SparkExternalLoginError[] = [\n 'no_login_info', 'email_not_verified', 'account_creation_failed', 'email_already_registered',\n 'sign_in_to_link', 'link_confirmation_sent', 'login_already_associated', 'link_failed',\n 'remote_failure', 'invalid_nonce', 'popup_blocked', 'popup_closed',\n];\n\nexport interface SparkExternalLoginResult {\n success: boolean;\n error?: SparkExternalLoginError;\n}\n\n/**\n * The payload the callback page hands back: posted to the opener, posted on the\n * {@link SPARK_EXTERNAL_LOGIN_CHANNEL} BroadcastChannel, and written to `localStorage` under\n * {@link sparkExternalLoginStorageKey} (with an `at` timestamp). Any of the three may be the one\n * that arrives: COOP on the provider's pages cuts the opener, and a frozen background tab may only\n * see the storage entry once it is shown again.\n */\nexport interface SparkExternalLoginMessage {\n type: typeof SPARK_EXTERNAL_LOGIN_MESSAGE_TYPE;\n success: boolean;\n error?: SparkExternalLoginError | null;\n /** The nonce the opener sent on the challenge; `null` from a challenge that carried none. */\n nonce?: string | null;\n}\n\n/** The opener's answer on the BroadcastChannel: the callback page sets its done-marker and closes. */\nexport interface SparkExternalLoginAck {\n type: typeof SPARK_EXTERNAL_LOGIN_ACK_TYPE;\n nonce: string;\n}\n\nexport const SPARK_EXTERNAL_LOGIN_MESSAGE_TYPE = 'spark:external-login';\nexport const SPARK_EXTERNAL_LOGIN_ACK_TYPE = 'spark:external-login-ack';\n\n/** The BroadcastChannel the callback page and the opener share. */\nexport const SPARK_EXTERNAL_LOGIN_CHANNEL = 'spark:external-login';\n\n/** The query parameter a redirect-mode failure comes back with. */\nexport const SPARK_EXTERNAL_LOGIN_QUERY_PARAM = 'sparkExternalLogin';\n\n/** How long a popup attempt waits for its result before it settles `popup_closed`. */\nexport const SPARK_EXTERNAL_LOGIN_TIMEOUT_MS = 10 * 60 * 1000;\n\n/** The `localStorage` key the callback page writes its payload to. */\nexport function sparkExternalLoginStorageKey(nonce: string): string {\n return `spark:external-login:${nonce}`;\n}\n\n/**\n * The `localStorage` done-marker: set once the opener has the result. The callback page reads it to\n * restore itself to the app when an installed web app shows it again.\n */\nexport function sparkExternalLoginDoneKey(nonce: string): string {\n return `spark:external-login-done:${nonce}`;\n}\n\n/**\n * A fresh nonce for one popup attempt: 32 base64url characters from `crypto.getRandomValues`\n * (24 bytes, so no padding). The server accepts `^[A-Za-z0-9_-]{16,64}$`.\n */\nexport function sparkExternalLoginNonce(): string {\n const bytes = new Uint8Array(24);\n crypto.getRandomValues(bytes);\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_');\n}\n\n/** One external login attached to the signed-in account. */\nexport interface SparkLinkedExternalLogin {\n provider: string;\n providerKey: string;\n displayName: string;\n /**\n * ⚠️ `false` when removing this login would leave the account with no way to sign in.\n *\n * Served by the server so the UI can disable the control rather than offer an action that will\n * be refused. It is **not** the enforcement — the server refuses the request too — and a client\n * that treats it as advisory only is behaving correctly.\n */\n canUnlink: boolean;\n}\n\n/** A provider that is configured but not yet attached to the signed-in account. */\nexport interface SparkAvailableExternalProvider {\n provider: string;\n displayName: string;\n}\n\nexport interface SparkExternalLogins {\n linked: SparkLinkedExternalLogin[];\n available: SparkAvailableExternalProvider[];\n}\n\n/** Why a login was not detached. */\nexport type SparkUnlinkError =\n /** ⚠️ It was the account's last way in, so removing it would have locked the owner out for good. */\n | 'last_credential'\n /** Not attached to this account. */\n | 'login_not_found'\n /** The store refused, for a reason that is not either of the above. */\n | 'unlink_failed';\n\nexport interface SparkUnlinkResult {\n success: boolean;\n error?: SparkUnlinkError;\n}\n","/**\n * Why a passkey ceremony did not complete.\n *\n * ⚠️ Every server-side sign-in rejection arrives as `failed`, and that is deliberate rather than\n * lossy: distinguishing \"unknown credential\" from \"bad signature\" would tell an anonymous caller\n * whether a credential exists. Do not try to recover the detail — it is not sent.\n *\n * The three codes the server never sends are raised here, because the browser is the only place\n * that can observe them.\n */\nexport type SparkPasskeyError =\n /** The browser cannot run the ceremony at all. Offer another sign-in method. */\n | 'unsupported'\n /** The user dismissed the authenticator prompt. Not an error worth showing. */\n | 'cancelled'\n /** The authenticator produced nothing — no matching credential, or the user gave up. */\n | 'no_credential'\n /** The account is locked out. The one server-side outcome worth distinguishing. */\n | 'locked_out'\n /** Everything else, server or client. */\n | 'failed';\n\nexport interface SparkPasskeyResult {\n success: boolean;\n error?: SparkPasskeyError;\n}\n\n/**\n * Collapses everything that can go wrong in a ceremony into the closed error union.\n *\n * `AbortError` and `NotAllowedError` are how a browser reports \"the user dismissed the prompt\" and\n * \"no credential was produced\" — neither is a fault, and neither should surface as a red banner.\n */\nexport function sparkPasskeyError(error: unknown): SparkPasskeyError {\n if (error instanceof DOMException) {\n if (error.name === 'AbortError') return 'cancelled';\n if (error.name === 'NotAllowedError') return 'no_credential';\n return 'failed';\n }\n\n const code = (error as { error?: { error?: string } })?.error?.error;\n if (code === 'locked_out') return 'locked_out';\n\n return 'failed';\n}\n\n/**\n * Whether this browser can run a passkey ceremony.\n *\n * Checks the native JSON helpers as well as `navigator.credentials`, because the ceremony is built\n * on them: the server speaks WebAuthn's JSON encoding, and without\n * `parseCreationOptionsFromJSON` / `parseRequestOptionsFromJSON` the client would have to hand-roll\n * base64url conversion over every field of the options — on the security-critical path, to support\n * browsers that a user holding a passkey is unlikely to be running.\n *\n * ⚠️ `window.isSecureContext` is part of the test rather than an afterthought: WebAuthn is\n * unavailable on an insecure origin, and without this the button would render and then fail at the\n * moment the user clicks it.\n */\nexport function passkeysSupported(): boolean {\n return (\n typeof window !== 'undefined' &&\n window.isSecureContext === true &&\n typeof PublicKeyCredential !== 'undefined' &&\n typeof navigator?.credentials?.create === 'function' &&\n typeof navigator?.credentials?.get === 'function' &&\n typeof PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&\n typeof PublicKeyCredential.parseRequestOptionsFromJSON === 'function'\n );\n}\n","import { isDevMode } from '@angular/core';\nimport { Route, Router } from '@angular/router';\nimport { SparkAuthConfig } from './auth-config';\n\n/**\n * Where to send someone who is not signed in.\n *\n * `loginUrl` is the single source of truth for this, and the only thing the guard, the interceptor\n * and the auth bar should consult — an application that turns off local credentials points it at\n * its own sign-in landing route.\n *\n * The reason this is a function rather than a property read is the failure mode it closes: nothing\n * connects `loginUrl` to the routes that actually exist, so pointing it at a route that was never\n * registered produces a redirect into a blank page with no type error, no build failure and no\n * runtime error. In development that now warns, once, naming the value and the fix.\n */\nexport function resolveSignInUrl(config: SparkAuthConfig, router: Router): string {\n if (isDevMode() && !warned && !routeExists(router, config.loginUrl)) {\n warned = true;\n console.warn(\n `[ng-spark-auth] loginUrl is \"${config.loginUrl}\", but no route matches it, so redirecting an `\n + `unauthenticated user will land on a blank page. If this app runs with local credentials `\n + `disabled, point loginUrl at its sign-in route (for example provideSparkAuth({ loginUrl: '/sign-in' })).`,\n );\n }\n\n return config.loginUrl;\n}\n\nlet warned = false;\n\nfunction routeExists(router: Router, url: string): boolean {\n const target = url.replace(/^\\/+/, '').split('?')[0].split('#')[0];\n return matches(router.config, target);\n}\n\nfunction matches(routes: Route[], target: string, prefix = ''): boolean {\n return routes.some(route => {\n const path = [prefix, route.path ?? ''].filter(Boolean).join('/');\n if (path === target) return true;\n // A wildcard route would match anything, including a URL nobody registered — treating it as a\n // match would make this check always pass and warn about nothing.\n return route.children ? matches(route.children, target, path) : false;\n });\n}\n","import { InjectionToken, Provider } from '@angular/core';\n\n/** `GET/POST /spark/auth/manage/profile` (#460, D16). */\nexport interface SparkAccountProfile {\n userName: string | null;\n email: string | null;\n /** App-contributed fields (`ISparkProfileContributor<TUser>`), by name. */\n fields: Record<string, unknown>;\n /** The culture account mail is written in (`SparkUser.PreferredCulture`); null = the app default. */\n preferredCulture: string | null;\n}\n\n/** The body of `POST /spark/auth/manage/profile`. Absent keeps a value; `preferredCulture: null` clears it. */\nexport interface SparkAccountProfileUpdate {\n userName?: string | null;\n fields?: Record<string, unknown>;\n preferredCulture?: string | null;\n}\n\n/** `GET /spark/auth/manage/info` / answer of `POST manage/info`. */\nexport interface SparkAccountInfo {\n email: string;\n isEmailConfirmed: boolean;\n}\n\n/** Answer of `POST /spark/auth/manage/2fa` (ASP.NET Core Identity's contract). */\nexport interface SparkTwoFactorState {\n sharedKey: string | null;\n recoveryCodesLeft: number;\n /** Only present right after codes were (re)generated — show them once. */\n recoveryCodes?: string[] | null;\n isTwoFactorEnabled: boolean;\n isMachineRemembered: boolean;\n}\n\n/** The body of `POST /spark/auth/manage/2fa`. An empty body reads the state (and creates a key if none). */\nexport interface SparkTwoFactorRequest {\n enable?: boolean;\n twoFactorCode?: string;\n resetSharedKey?: boolean;\n resetRecoveryCodes?: boolean;\n forgetMachine?: boolean;\n}\n\n/** `GET /spark/auth/manage/2fa/authenticator-uri`: the key, its otpauth URI and a server-rendered QR SVG. */\n/**\n * `GET`/`POST /manage/external-login-two-factor`: whether a sign-in with an external account skips\n * the second factor for this user, and whether two-factor is enabled at all.\n */\nexport interface SparkExternalLoginTwoFactor {\n bypass: boolean;\n twoFactorEnabled: boolean;\n}\n\nexport interface SparkAuthenticatorUri {\n sharedKey: string;\n authenticatorUri: string;\n qrCodeSvg: string;\n}\n\n/**\n * The outcome of an account call that can fail with a message for the user.\n *\n * `errors` flattens an RFC 7807 validation problem (Identity codes, `PreferredCulture`, contributor\n * fields) into `field → messages`; `error` carries a Spark error code (`reauthentication_required`,\n * `no_authenticator_key`, …) when the server sent one.\n */\nexport interface SparkAccountResult<T = void> {\n success: boolean;\n value?: T;\n error?: string;\n errors?: Record<string, string[]>;\n status?: number;\n}\n\n/**\n * One application field on the profile page (#460, D16), validated and stored server-side by an\n * `ISparkProfileContributor<TUser>` declaring the same `name`.\n */\nexport interface SparkAccountProfileField {\n /** The field name, as the contributor declares it (matched case-insensitively by the server). */\n name: string;\n /** A translation key (looked up with the `t` pipe) or literal text. */\n label: string;\n type?: 'text' | 'textarea' | 'email' | 'url' | 'tel' | 'number' | 'date' | 'checkbox' | 'select';\n /** For `select`. `label` is a translation key or literal text. */\n options?: { value: string; label: string }[];\n required?: boolean;\n maxLength?: number;\n /** A translation key or literal text shown under the control. */\n hint?: string;\n /** Ascending; unordered fields follow in registration order. */\n order?: number;\n}\n\n/** Multi-provider: the application's profile fields. Use {@link provideSparkAccountProfileFields}. */\nexport const SPARK_ACCOUNT_PROFILE_FIELDS = new InjectionToken<SparkAccountProfileField[]>('SPARK_ACCOUNT_PROFILE_FIELDS');\n\n/** Registers profile fields; every call adds. */\nexport function provideSparkAccountProfileFields(...fields: SparkAccountProfileField[]): Provider[] {\n return fields.map(field => ({ provide: SPARK_ACCOUNT_PROFILE_FIELDS, useValue: field, multi: true }));\n}\n","/**\n * What an AI agent may do, as the server writes `EAgentPermission`: `'Read'`, `'Read, Write'`, or `'None'`\n * (MCP PRD D6). \"Write\" is shown to users as \"changes\".\n */\nexport type SparkAgentPermission = 'None' | 'Read' | 'Write' | 'Read, Write';\n\n/** Whether `permission` includes changes (`Write`). */\nexport function sparkAgentCanWrite(permission: SparkAgentPermission | null | undefined): boolean {\n return !!permission && permission.split(',').some(p => p.trim() === 'Write');\n}\n\n/** One chosen type of a grant. */\nexport interface SparkAgentGrantType {\n name: string;\n permissions: SparkAgentPermission;\n}\n\n/** An entity type the application makes available to agents and the user may see, with its own most. */\nexport interface SparkAgentExposedType {\n name: string;\n label: string;\n permissions: SparkAgentPermission;\n /** Labels of the type's queries, shown under it. */\n queries: string[];\n}\n\n/**\n * One agent grant (`AgentGrant`): what the user allowed one agent. `types: null` means everything the\n * application makes available to agents, including types exposed later.\n */\nexport interface SparkAgentGrant {\n /** Identifies the grant in requests (`pat/…`, `ext/…`, `app/…`); the user's own grants only. */\n key: string;\n /** `PersonalAccessToken`, `MetadataDocument`, `DynamicRegistration`, `PreRegistered`, `ExternalAuthorizationServer`. */\n kind: string;\n /** A token's name, or the name an OAuth client gives itself (unverified unless `verifiedHost` is set). */\n name: string | null;\n clientId: string | null;\n /** Set only when the client's identity was verified (its metadata document was fetched from this host). */\n verifiedHost: string | null;\n /** For `ExternalAuthorizationServer`: the issuer of the server the agent signed in at. */\n issuer?: string | null;\n permissions: SparkAgentPermission;\n types: SparkAgentGrantType[] | null;\n /** The most this grant may be given (the application's limit for this agent). */\n maxPermissions: SparkAgentPermission;\n createdAt: string;\n expiresAt: string | null;\n lastUsedAt: string | null;\n /**\n * `revoked`: the token stopped working because the password or sign-in methods changed. `disconnected`: an external\n * authorization server's agent the user disconnected, or one a password or sign-in-method change disconnected; an\n * edit allows it again.\n */\n status: 'active' | 'expired' | 'revoked' | 'disconnected';\n}\n\n/** An entry another package contributed (e.g. sign-ins at other websites, from the identity provider). */\nexport interface SparkContributedApplication {\n source: string;\n id: string;\n name: string;\n description: string | null;\n createdAt: string | null;\n lastUsedAt: string | null;\n}\n\n/** `GET /spark/auth/manage/agents`. */\nexport interface SparkAgentGrantsOverview {\n /** Whether personal access tokens can be created here. */\n personalAccessTokens: boolean;\n defaultTokenLifetimeDays: number;\n maxTokenLifetimeDays: number;\n /** The most a new token may be granted. */\n tokenPermissions: SparkAgentPermission;\n types: SparkAgentExposedType[];\n grants: SparkAgentGrant[];\n applications: SparkContributedApplication[];\n}\n\n/** The body of `POST /spark/auth/manage/agents/tokens`. Omit `types` for everything exposed. */\nexport interface SparkAgentTokenRequest {\n name: string;\n expiresInDays?: number;\n permissions: SparkAgentPermission;\n types?: SparkAgentGrantType[] | null;\n}\n\n/** The answer of `POST /spark/auth/manage/agents/tokens`: the only time the token is ever shown. */\nexport interface SparkAgentTokenCreated {\n token: string;\n key: string;\n expiresAt: string;\n}\n\n/** The body of `POST /spark/auth/manage/agents/update`. `types: null` for everything exposed. */\nexport interface SparkAgentGrantUpdate {\n key: string;\n permissions: SparkAgentPermission;\n types: SparkAgentGrantType[] | null;\n}\n"],"mappings":"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"}
|
|
@@ -2,14 +2,14 @@ import * as i0 from "@angular/core";
|
|
|
2
2
|
import { ChangeDetectionStrategy, Component, inject, signal } from "@angular/core";
|
|
3
3
|
import * as i1 from "@angular/forms";
|
|
4
4
|
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
|
5
|
-
import { Router, RouterLink } from "@angular/router";
|
|
5
|
+
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
|
|
6
6
|
import { HttpErrorResponse } from "@angular/common/http";
|
|
7
7
|
import { Color } from "@mintplayer/ng-bootstrap";
|
|
8
8
|
import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
|
|
9
9
|
import { BsCardComponent, BsCardHeaderComponent } from "@mintplayer/ng-bootstrap/card";
|
|
10
10
|
import { BsFormComponent, BsFormControlDirective } from "@mintplayer/ng-bootstrap/form";
|
|
11
11
|
import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
|
|
12
|
-
import { SPARK_AUTH_ROUTE_PATHS } from "@mintplayer/ng-spark/auth/models";
|
|
12
|
+
import { SPARK_AUTH_ROUTE_PATHS, isSafeReturnUrl } from "@mintplayer/ng-spark/auth/models";
|
|
13
13
|
import { SparkAuthService, SparkAuthTranslationService } from "@mintplayer/ng-spark/auth/core";
|
|
14
14
|
import { TranslateKeyPipe } from "@mintplayer/ng-spark/auth/pipes";
|
|
15
15
|
function passwordMatchValidator(control) {
|
|
@@ -23,7 +23,10 @@ var SparkRegisterComponent = class SparkRegisterComponent {
|
|
|
23
23
|
authService = inject(SparkAuthService);
|
|
24
24
|
router = inject(Router);
|
|
25
25
|
translation = inject(SparkAuthTranslationService);
|
|
26
|
+
route = inject(ActivatedRoute);
|
|
26
27
|
routePaths = inject(SPARK_AUTH_ROUTE_PATHS);
|
|
28
|
+
returnUrl = this.route.snapshot.queryParamMap.get("returnUrl");
|
|
29
|
+
returnUrlParams = isSafeReturnUrl(this.returnUrl) ? { returnUrl: this.returnUrl } : {};
|
|
27
30
|
colors = Color;
|
|
28
31
|
loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
|
|
29
32
|
errorMessage = signal("", ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
|
|
@@ -33,6 +36,9 @@ var SparkRegisterComponent = class SparkRegisterComponent {
|
|
|
33
36
|
password: ["", Validators.required],
|
|
34
37
|
confirmPassword: ["", Validators.required]
|
|
35
38
|
}, { validators: passwordMatchValidator });
|
|
39
|
+
leaveTo(url) {
|
|
40
|
+
window.location.assign(url);
|
|
41
|
+
}
|
|
36
42
|
async onSubmit() {
|
|
37
43
|
if (this.form.invalid) {
|
|
38
44
|
this.form.markAllAsTouched();
|
|
@@ -43,7 +49,14 @@ var SparkRegisterComponent = class SparkRegisterComponent {
|
|
|
43
49
|
const { email, userName, password } = this.form.value;
|
|
44
50
|
try {
|
|
45
51
|
await this.authService.register(email, password, userName.trim());
|
|
46
|
-
this.
|
|
52
|
+
if (this.returnUrl && isSafeReturnUrl(this.returnUrl) && this.returnUrl.startsWith("/connect/")) {
|
|
53
|
+
this.leaveTo(`/connect/login?returnUrl=${encodeURIComponent(this.returnUrl)}`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.router.navigate([this.routePaths.login ?? "/"], { queryParams: {
|
|
57
|
+
registered: "true",
|
|
58
|
+
...this.returnUrlParams
|
|
59
|
+
} });
|
|
47
60
|
} catch (err) {
|
|
48
61
|
if (err instanceof HttpErrorResponse) {
|
|
49
62
|
if (err.status === 400 && err.error?.errors) {
|
|
@@ -71,7 +84,7 @@ var SparkRegisterComponent = class SparkRegisterComponent {
|
|
|
71
84
|
isStandalone: true,
|
|
72
85
|
selector: "spark-register",
|
|
73
86
|
ngImport: i0,
|
|
74
|
-
template: "<div class=\"d-flex justify-content-center\">\n <bs-card style=\"width: 100%; max-width: 400px;\">\n <bs-card-header>\n <h3 class=\"mb-0 text-center\">{{ 'auth.register' | t }}</h3>\n </bs-card-header>\n <div class=\"p-4\">\n @if (errorMessage()) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">{{ errorMessage() }}</bs-alert>\n }\n\n <bs-form>\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">{{ 'auth.email' | t }}</label>\n <input\n type=\"email\"\n id=\"email\"\n formControlName=\"email\"\n autocomplete=\"email\"\n />\n @if (form.get('email')?.touched && form.get('email')?.hasError('email')) {\n <div class=\"text-danger mt-1\">{{ 'auth.invalidEmail' | t }}</div>\n }\n </div>\n\n <div class=\"mb-3\">\n <label for=\"userName\" class=\"form-label\">{{ 'auth.userName' | t }}</label>\n <input\n type=\"text\"\n id=\"userName\"\n formControlName=\"userName\"\n autocomplete=\"username\"\n />\n <div class=\"form-text\" [class.text-danger]=\"form.get('userName')?.touched && form.get('userName')?.invalid\">{{ 'auth.userNameHint' | t }}</div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"password\" class=\"form-label\">{{ 'auth.password' | t }}</label>\n <input\n type=\"password\"\n id=\"password\"\n formControlName=\"password\"\n autocomplete=\"new-password\"\n />\n </div>\n\n <div class=\"mb-3\">\n <label for=\"confirmPassword\" class=\"form-label\">{{ 'auth.confirmPassword' | t }}</label>\n <input\n type=\"password\"\n id=\"confirmPassword\"\n formControlName=\"confirmPassword\"\n autocomplete=\"new-password\"\n />\n @if (form.touched && form.hasError('passwordMismatch')) {\n <div class=\"text-danger mt-1\">{{ 'auth.passwordMismatch' | t }}</div>\n }\n </div>\n\n <button\n type=\"submit\"\n class=\"btn btn-primary w-100\"\n [disabled]=\"loading()\"\n >\n @if (loading()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'auth.register' | t }}\n </button>\n </form>\n </bs-form>\n\n <!-- withRegistration() is opt-in independently of withLocalLogin(), so the password form may\n not be mounted. An unguarded [routerLink]=\"undefined\" navigates to the current route. -->\n @if (routePaths.login) {\n <div class=\"mt-3 text-center\">\n <span>{{ 'auth.alreadyHaveAccount' | t }} </span>\n <a [routerLink]=\"routePaths.login\">{{ 'auth.login' | t }}</a>\n </div>\n }\n </div>\n </bs-card>\n</div>\n",
|
|
87
|
+
template: "<div class=\"d-flex justify-content-center\">\n <bs-card style=\"width: 100%; max-width: 400px;\">\n <bs-card-header>\n <h3 class=\"mb-0 text-center\">{{ 'auth.register' | t }}</h3>\n </bs-card-header>\n <div class=\"p-4\">\n @if (errorMessage()) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">{{ errorMessage() }}</bs-alert>\n }\n\n <bs-form>\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">{{ 'auth.email' | t }}</label>\n <input\n type=\"email\"\n id=\"email\"\n formControlName=\"email\"\n autocomplete=\"email\"\n />\n @if (form.get('email')?.touched && form.get('email')?.hasError('email')) {\n <div class=\"text-danger mt-1\">{{ 'auth.invalidEmail' | t }}</div>\n }\n </div>\n\n <div class=\"mb-3\">\n <label for=\"userName\" class=\"form-label\">{{ 'auth.userName' | t }}</label>\n <input\n type=\"text\"\n id=\"userName\"\n formControlName=\"userName\"\n autocomplete=\"username\"\n />\n <div class=\"form-text\" [class.text-danger]=\"form.get('userName')?.touched && form.get('userName')?.invalid\">{{ 'auth.userNameHint' | t }}</div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"password\" class=\"form-label\">{{ 'auth.password' | t }}</label>\n <input\n type=\"password\"\n id=\"password\"\n formControlName=\"password\"\n autocomplete=\"new-password\"\n />\n </div>\n\n <div class=\"mb-3\">\n <label for=\"confirmPassword\" class=\"form-label\">{{ 'auth.confirmPassword' | t }}</label>\n <input\n type=\"password\"\n id=\"confirmPassword\"\n formControlName=\"confirmPassword\"\n autocomplete=\"new-password\"\n />\n @if (form.touched && form.hasError('passwordMismatch')) {\n <div class=\"text-danger mt-1\">{{ 'auth.passwordMismatch' | t }}</div>\n }\n </div>\n\n <button\n type=\"submit\"\n class=\"btn btn-primary w-100\"\n [disabled]=\"loading()\"\n >\n @if (loading()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'auth.register' | t }}\n </button>\n </form>\n </bs-form>\n\n <!-- withRegistration() is opt-in independently of withLocalLogin(), so the password form may\n not be mounted. An unguarded [routerLink]=\"undefined\" navigates to the current route. -->\n @if (routePaths.login) {\n <div class=\"mt-3 text-center\">\n <span>{{ 'auth.alreadyHaveAccount' | t }} </span>\n <a [routerLink]=\"routePaths.login\" [queryParams]=\"returnUrlParams\">{{ 'auth.login' | t }}</a>\n </div>\n }\n </div>\n </bs-card>\n</div>\n",
|
|
75
88
|
dependencies: [
|
|
76
89
|
{
|
|
77
90
|
kind: "ngmodule",
|
|
@@ -207,7 +220,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
207
220
|
BsSpinnerComponent,
|
|
208
221
|
TranslateKeyPipe
|
|
209
222
|
],
|
|
210
|
-
template: "<div class=\"d-flex justify-content-center\">\n <bs-card style=\"width: 100%; max-width: 400px;\">\n <bs-card-header>\n <h3 class=\"mb-0 text-center\">{{ 'auth.register' | t }}</h3>\n </bs-card-header>\n <div class=\"p-4\">\n @if (errorMessage()) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">{{ errorMessage() }}</bs-alert>\n }\n\n <bs-form>\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">{{ 'auth.email' | t }}</label>\n <input\n type=\"email\"\n id=\"email\"\n formControlName=\"email\"\n autocomplete=\"email\"\n />\n @if (form.get('email')?.touched && form.get('email')?.hasError('email')) {\n <div class=\"text-danger mt-1\">{{ 'auth.invalidEmail' | t }}</div>\n }\n </div>\n\n <div class=\"mb-3\">\n <label for=\"userName\" class=\"form-label\">{{ 'auth.userName' | t }}</label>\n <input\n type=\"text\"\n id=\"userName\"\n formControlName=\"userName\"\n autocomplete=\"username\"\n />\n <div class=\"form-text\" [class.text-danger]=\"form.get('userName')?.touched && form.get('userName')?.invalid\">{{ 'auth.userNameHint' | t }}</div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"password\" class=\"form-label\">{{ 'auth.password' | t }}</label>\n <input\n type=\"password\"\n id=\"password\"\n formControlName=\"password\"\n autocomplete=\"new-password\"\n />\n </div>\n\n <div class=\"mb-3\">\n <label for=\"confirmPassword\" class=\"form-label\">{{ 'auth.confirmPassword' | t }}</label>\n <input\n type=\"password\"\n id=\"confirmPassword\"\n formControlName=\"confirmPassword\"\n autocomplete=\"new-password\"\n />\n @if (form.touched && form.hasError('passwordMismatch')) {\n <div class=\"text-danger mt-1\">{{ 'auth.passwordMismatch' | t }}</div>\n }\n </div>\n\n <button\n type=\"submit\"\n class=\"btn btn-primary w-100\"\n [disabled]=\"loading()\"\n >\n @if (loading()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'auth.register' | t }}\n </button>\n </form>\n </bs-form>\n\n <!-- withRegistration() is opt-in independently of withLocalLogin(), so the password form may\n not be mounted. An unguarded [routerLink]=\"undefined\" navigates to the current route. -->\n @if (routePaths.login) {\n <div class=\"mt-3 text-center\">\n <span>{{ 'auth.alreadyHaveAccount' | t }} </span>\n <a [routerLink]=\"routePaths.login\">{{ 'auth.login' | t }}</a>\n </div>\n }\n </div>\n </bs-card>\n</div>\n"
|
|
223
|
+
template: "<div class=\"d-flex justify-content-center\">\n <bs-card style=\"width: 100%; max-width: 400px;\">\n <bs-card-header>\n <h3 class=\"mb-0 text-center\">{{ 'auth.register' | t }}</h3>\n </bs-card-header>\n <div class=\"p-4\">\n @if (errorMessage()) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">{{ errorMessage() }}</bs-alert>\n }\n\n <bs-form>\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">{{ 'auth.email' | t }}</label>\n <input\n type=\"email\"\n id=\"email\"\n formControlName=\"email\"\n autocomplete=\"email\"\n />\n @if (form.get('email')?.touched && form.get('email')?.hasError('email')) {\n <div class=\"text-danger mt-1\">{{ 'auth.invalidEmail' | t }}</div>\n }\n </div>\n\n <div class=\"mb-3\">\n <label for=\"userName\" class=\"form-label\">{{ 'auth.userName' | t }}</label>\n <input\n type=\"text\"\n id=\"userName\"\n formControlName=\"userName\"\n autocomplete=\"username\"\n />\n <div class=\"form-text\" [class.text-danger]=\"form.get('userName')?.touched && form.get('userName')?.invalid\">{{ 'auth.userNameHint' | t }}</div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"password\" class=\"form-label\">{{ 'auth.password' | t }}</label>\n <input\n type=\"password\"\n id=\"password\"\n formControlName=\"password\"\n autocomplete=\"new-password\"\n />\n </div>\n\n <div class=\"mb-3\">\n <label for=\"confirmPassword\" class=\"form-label\">{{ 'auth.confirmPassword' | t }}</label>\n <input\n type=\"password\"\n id=\"confirmPassword\"\n formControlName=\"confirmPassword\"\n autocomplete=\"new-password\"\n />\n @if (form.touched && form.hasError('passwordMismatch')) {\n <div class=\"text-danger mt-1\">{{ 'auth.passwordMismatch' | t }}</div>\n }\n </div>\n\n <button\n type=\"submit\"\n class=\"btn btn-primary w-100\"\n [disabled]=\"loading()\"\n >\n @if (loading()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'auth.register' | t }}\n </button>\n </form>\n </bs-form>\n\n <!-- withRegistration() is opt-in independently of withLocalLogin(), so the password form may\n not be mounted. An unguarded [routerLink]=\"undefined\" navigates to the current route. -->\n @if (routePaths.login) {\n <div class=\"mt-3 text-center\">\n <span>{{ 'auth.alreadyHaveAccount' | t }} </span>\n <a [routerLink]=\"routePaths.login\" [queryParams]=\"returnUrlParams\">{{ 'auth.login' | t }}</a>\n </div>\n }\n </div>\n </bs-card>\n</div>\n"
|
|
211
224
|
}]
|
|
212
225
|
}]
|
|
213
226
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-auth-register.mjs","names":[],"sources":["../../auth/register/src/spark-register.component.ts","../../auth/register/src/spark-register.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';\nimport { ReactiveFormsModule, FormBuilder, Validators, AbstractControl, ValidationErrors } from '@angular/forms';\nimport { RouterLink, Router } from '@angular/router';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';\nimport { BsFormComponent, BsFormControlDirective } from '@mintplayer/ng-bootstrap/form';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { SPARK_AUTH_ROUTE_PATHS } from '@mintplayer/ng-spark/auth/models';\nimport { SparkAuthService, SparkAuthTranslationService } from '@mintplayer/ng-spark/auth/core';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/auth/pipes';\n\nfunction passwordMatchValidator(control: AbstractControl): ValidationErrors | null {\n const password = control.get('password');\n const confirmPassword = control.get('confirmPassword');\n if (password && confirmPassword && password.value !== confirmPassword.value) {\n return { passwordMismatch: true };\n }\n return null;\n}\n\n@Component({\n selector: 'spark-register',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [ReactiveFormsModule, RouterLink, BsAlertComponent, BsCardComponent, BsCardHeaderComponent, BsFormComponent, BsFormControlDirective, BsSpinnerComponent, TranslateKeyPipe],\n templateUrl: './spark-register.component.html',\n})\nexport class SparkRegisterComponent {\n private readonly fb = inject(FormBuilder);\n private readonly authService = inject(SparkAuthService);\n private readonly router = inject(Router);\n private readonly translation = inject(SparkAuthTranslationService);\n readonly routePaths = inject(SPARK_AUTH_ROUTE_PATHS);\n\n colors = Color;\n readonly loading = signal(false);\n readonly errorMessage = signal('');\n\n readonly form = this.fb.group({\n email: ['', [Validators.required, Validators.email]],\n // The public handle other users see; the server refuses '@' too (never an email address).\n userName: ['', [Validators.required, Validators.pattern(/^[^@]*$/)]],\n password: ['', Validators.required],\n confirmPassword: ['', Validators.required],\n }, { validators: passwordMatchValidator });\n\n async onSubmit(): Promise<void> {\n if (this.form.invalid) {\n this.form.markAllAsTouched();\n return;\n }\n\n this.loading.set(true);\n this.errorMessage.set('');\n\n const { email, userName, password } = this.form.value;\n\n try {\n await this.authService.register(email!, password!, userName!.trim());\n // An app can mount registration without the password form (external-only sign-in with\n // self-service account creation). Landing on the site root beats navigating to `undefined`,\n // which the router resolves to the current route — leaving the user on a form they just\n // submitted successfully.\n this.router.navigate([this.routePaths.login ?? '/'], {\n queryParams: { registered: 'true' },\n });\n } catch (err: any) {\n if (err instanceof HttpErrorResponse) {\n if (err.status === 400 && err.error?.errors) {\n const messages = ([] as string[]).concat(...Object.values(err.error.errors) as string[][]);\n this.errorMessage.set(messages.join(' '));\n } else if (err.error?.detail) {\n this.errorMessage.set(err.error.detail);\n } else {\n this.errorMessage.set(this.translation.t('auth.registrationFailed'));\n }\n } else {\n this.errorMessage.set(this.translation.t('auth.registrationFailed'));\n }\n } finally {\n this.loading.set(false);\n }\n }\n}\n\nexport default SparkRegisterComponent;\n","<div class=\"d-flex justify-content-center\">\n <bs-card style=\"width: 100%; max-width: 400px;\">\n <bs-card-header>\n <h3 class=\"mb-0 text-center\">{{ 'auth.register' | t }}</h3>\n </bs-card-header>\n <div class=\"p-4\">\n @if (errorMessage()) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">{{ errorMessage() }}</bs-alert>\n }\n\n <bs-form>\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">{{ 'auth.email' | t }}</label>\n <input\n type=\"email\"\n id=\"email\"\n formControlName=\"email\"\n autocomplete=\"email\"\n />\n @if (form.get('email')?.touched && form.get('email')?.hasError('email')) {\n <div class=\"text-danger mt-1\">{{ 'auth.invalidEmail' | t }}</div>\n }\n </div>\n\n <div class=\"mb-3\">\n <label for=\"userName\" class=\"form-label\">{{ 'auth.userName' | t }}</label>\n <input\n type=\"text\"\n id=\"userName\"\n formControlName=\"userName\"\n autocomplete=\"username\"\n />\n <div class=\"form-text\" [class.text-danger]=\"form.get('userName')?.touched && form.get('userName')?.invalid\">{{ 'auth.userNameHint' | t }}</div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"password\" class=\"form-label\">{{ 'auth.password' | t }}</label>\n <input\n type=\"password\"\n id=\"password\"\n formControlName=\"password\"\n autocomplete=\"new-password\"\n />\n </div>\n\n <div class=\"mb-3\">\n <label for=\"confirmPassword\" class=\"form-label\">{{ 'auth.confirmPassword' | t }}</label>\n <input\n type=\"password\"\n id=\"confirmPassword\"\n formControlName=\"confirmPassword\"\n autocomplete=\"new-password\"\n />\n @if (form.touched && form.hasError('passwordMismatch')) {\n <div class=\"text-danger mt-1\">{{ 'auth.passwordMismatch' | t }}</div>\n }\n </div>\n\n <button\n type=\"submit\"\n class=\"btn btn-primary w-100\"\n [disabled]=\"loading()\"\n >\n @if (loading()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'auth.register' | t }}\n </button>\n </form>\n </bs-form>\n\n <!-- withRegistration() is opt-in independently of withLocalLogin(), so the password form may\n not be mounted. An unguarded [routerLink]=\"undefined\" navigates to the current route. -->\n @if (routePaths.login) {\n <div class=\"mt-3 text-center\">\n <span>{{ 'auth.alreadyHaveAccount' | t }} </span>\n <a [routerLink]=\"routePaths.login\">{{ 'auth.login' | t }}</a>\n </div>\n }\n </div>\n </bs-card>\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-auth-register.mjs","names":[],"sources":["../../auth/register/src/spark-register.component.ts","../../auth/register/src/spark-register.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';\nimport { ReactiveFormsModule, FormBuilder, Validators, AbstractControl, ValidationErrors } from '@angular/forms';\nimport { RouterLink, Router, ActivatedRoute } from '@angular/router';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';\nimport { BsFormComponent, BsFormControlDirective } from '@mintplayer/ng-bootstrap/form';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { SPARK_AUTH_ROUTE_PATHS, isSafeReturnUrl } from '@mintplayer/ng-spark/auth/models';\nimport { SparkAuthService, SparkAuthTranslationService } from '@mintplayer/ng-spark/auth/core';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/auth/pipes';\n\nfunction passwordMatchValidator(control: AbstractControl): ValidationErrors | null {\n const password = control.get('password');\n const confirmPassword = control.get('confirmPassword');\n if (password && confirmPassword && password.value !== confirmPassword.value) {\n return { passwordMismatch: true };\n }\n return null;\n}\n\n@Component({\n selector: 'spark-register',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [ReactiveFormsModule, RouterLink, BsAlertComponent, BsCardComponent, BsCardHeaderComponent, BsFormComponent, BsFormControlDirective, BsSpinnerComponent, TranslateKeyPipe],\n templateUrl: './spark-register.component.html',\n})\nexport class SparkRegisterComponent {\n private readonly fb = inject(FormBuilder);\n private readonly authService = inject(SparkAuthService);\n private readonly router = inject(Router);\n private readonly translation = inject(SparkAuthTranslationService);\n private readonly route = inject(ActivatedRoute);\n readonly routePaths = inject(SPARK_AUTH_ROUTE_PATHS);\n\n /**\n * Carries a local `returnUrl` on to the sign-in step, e.g. the identity provider's pending\n * authorization from its sign-in page's \"Create an account\" link. Anything else is dropped (R2-H9).\n */\n private readonly returnUrl = this.route.snapshot.queryParamMap.get('returnUrl');\n readonly returnUrlParams = isSafeReturnUrl(this.returnUrl) ? { returnUrl: this.returnUrl } : {};\n\n colors = Color;\n readonly loading = signal(false);\n readonly errorMessage = signal('');\n\n readonly form = this.fb.group({\n email: ['', [Validators.required, Validators.email]],\n // The public handle other users see; the server refuses '@' too (never an email address).\n userName: ['', [Validators.required, Validators.pattern(/^[^@]*$/)]],\n password: ['', Validators.required],\n confirmPassword: ['', Validators.required],\n }, { validators: passwordMatchValidator });\n\n /** A full page load, for a server route the router can't reach. A method so tests can replace it. */\n protected leaveTo(url: string): void {\n window.location.assign(url);\n }\n\n async onSubmit(): Promise<void> {\n if (this.form.invalid) {\n this.form.markAllAsTouched();\n return;\n }\n\n this.loading.set(true);\n this.errorMessage.set('');\n\n const { email, userName, password } = this.form.value;\n\n try {\n await this.authService.register(email!, password!, userName!.trim());\n // A pending identity-provider request (/connect/authorize, …) is a server route the router\n // can't reach: go back to the provider's own sign-in page, which continues it.\n if (this.returnUrl && isSafeReturnUrl(this.returnUrl) && this.returnUrl.startsWith('/connect/')) {\n this.leaveTo(`/connect/login?returnUrl=${encodeURIComponent(this.returnUrl)}`);\n return;\n }\n // An app can mount registration without the password form (external-only sign-in with\n // self-service account creation). Landing on the site root beats navigating to `undefined`,\n // which the router resolves to the current route — leaving the user on a form they just\n // submitted successfully.\n this.router.navigate([this.routePaths.login ?? '/'], {\n queryParams: { registered: 'true', ...this.returnUrlParams },\n });\n } catch (err: any) {\n if (err instanceof HttpErrorResponse) {\n if (err.status === 400 && err.error?.errors) {\n const messages = ([] as string[]).concat(...Object.values(err.error.errors) as string[][]);\n this.errorMessage.set(messages.join(' '));\n } else if (err.error?.detail) {\n this.errorMessage.set(err.error.detail);\n } else {\n this.errorMessage.set(this.translation.t('auth.registrationFailed'));\n }\n } else {\n this.errorMessage.set(this.translation.t('auth.registrationFailed'));\n }\n } finally {\n this.loading.set(false);\n }\n }\n}\n\nexport default SparkRegisterComponent;\n","<div class=\"d-flex justify-content-center\">\n <bs-card style=\"width: 100%; max-width: 400px;\">\n <bs-card-header>\n <h3 class=\"mb-0 text-center\">{{ 'auth.register' | t }}</h3>\n </bs-card-header>\n <div class=\"p-4\">\n @if (errorMessage()) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">{{ errorMessage() }}</bs-alert>\n }\n\n <bs-form>\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">{{ 'auth.email' | t }}</label>\n <input\n type=\"email\"\n id=\"email\"\n formControlName=\"email\"\n autocomplete=\"email\"\n />\n @if (form.get('email')?.touched && form.get('email')?.hasError('email')) {\n <div class=\"text-danger mt-1\">{{ 'auth.invalidEmail' | t }}</div>\n }\n </div>\n\n <div class=\"mb-3\">\n <label for=\"userName\" class=\"form-label\">{{ 'auth.userName' | t }}</label>\n <input\n type=\"text\"\n id=\"userName\"\n formControlName=\"userName\"\n autocomplete=\"username\"\n />\n <div class=\"form-text\" [class.text-danger]=\"form.get('userName')?.touched && form.get('userName')?.invalid\">{{ 'auth.userNameHint' | t }}</div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"password\" class=\"form-label\">{{ 'auth.password' | t }}</label>\n <input\n type=\"password\"\n id=\"password\"\n formControlName=\"password\"\n autocomplete=\"new-password\"\n />\n </div>\n\n <div class=\"mb-3\">\n <label for=\"confirmPassword\" class=\"form-label\">{{ 'auth.confirmPassword' | t }}</label>\n <input\n type=\"password\"\n id=\"confirmPassword\"\n formControlName=\"confirmPassword\"\n autocomplete=\"new-password\"\n />\n @if (form.touched && form.hasError('passwordMismatch')) {\n <div class=\"text-danger mt-1\">{{ 'auth.passwordMismatch' | t }}</div>\n }\n </div>\n\n <button\n type=\"submit\"\n class=\"btn btn-primary w-100\"\n [disabled]=\"loading()\"\n >\n @if (loading()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'auth.register' | t }}\n </button>\n </form>\n </bs-form>\n\n <!-- withRegistration() is opt-in independently of withLocalLogin(), so the password form may\n not be mounted. An unguarded [routerLink]=\"undefined\" navigates to the current route. -->\n @if (routePaths.login) {\n <div class=\"mt-3 text-center\">\n <span>{{ 'auth.alreadyHaveAccount' | t }} </span>\n <a [routerLink]=\"routePaths.login\" [queryParams]=\"returnUrlParams\">{{ 'auth.login' | t }}</a>\n </div>\n }\n </div>\n </bs-card>\n</div>\n"],"mappings":"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"}
|
|
@@ -140,6 +140,27 @@ function withAccount(options) {
|
|
|
140
140
|
children
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
|
+
function withConnectedAgents(options) {
|
|
144
|
+
const guard = options?.canActivate ?? [sparkAuthGuard];
|
|
145
|
+
const path = entryPath(options?.agents, "account/agents");
|
|
146
|
+
const route = child(options?.agents, path, () => import("@mintplayer/ng-spark/auth/account").then((m) => m.SparkConnectedAgentsComponent));
|
|
147
|
+
return {
|
|
148
|
+
paths: { agents: "/" + path },
|
|
149
|
+
children: [guard.length ? {
|
|
150
|
+
...route,
|
|
151
|
+
canActivate: guard
|
|
152
|
+
} : route],
|
|
153
|
+
accountLinks: [{
|
|
154
|
+
key: "agents",
|
|
155
|
+
path: "/" + path,
|
|
156
|
+
label: {
|
|
157
|
+
en: "Connected applications",
|
|
158
|
+
fr: "Applications connectées",
|
|
159
|
+
nl: "Gekoppelde toepassingen"
|
|
160
|
+
}
|
|
161
|
+
}]
|
|
162
|
+
};
|
|
163
|
+
}
|
|
143
164
|
function externalProvider(scheme, presentation) {
|
|
144
165
|
return {
|
|
145
166
|
scheme,
|
|
@@ -189,6 +210,6 @@ function oidcProvider(scheme, displayName, iconClass = "bi bi-person-badge") {
|
|
|
189
210
|
iconClass
|
|
190
211
|
});
|
|
191
212
|
}
|
|
192
|
-
export { SPARK_PASSKEYS_PAGE_URL, externalProvider, facebookProvider, githubProvider, googleProvider, linkedInProvider, microsoftProvider, oidcProvider, sparkAuthRoutes, twitterProvider, withAccount, withExternalLogin, withLocalLogin, withPasskeys, withRegistration };
|
|
213
|
+
export { SPARK_PASSKEYS_PAGE_URL, externalProvider, facebookProvider, githubProvider, googleProvider, linkedInProvider, microsoftProvider, oidcProvider, sparkAuthRoutes, twitterProvider, withAccount, withConnectedAgents, withExternalLogin, withLocalLogin, withPasskeys, withRegistration };
|
|
193
214
|
|
|
194
215
|
//# sourceMappingURL=mintplayer-ng-spark-auth-routes.mjs.map
|