@mintplayer/ng-spark 22.30.0 → 22.31.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.
Files changed (82) hide show
  1. package/README.md +2 -2
  2. package/auth/README.md +216 -0
  3. package/fesm2022/mintplayer-ng-spark-auth-account.mjs +2023 -0
  4. package/fesm2022/mintplayer-ng-spark-auth-account.mjs.map +1 -0
  5. package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs +96 -0
  6. package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs.map +1 -0
  7. package/fesm2022/mintplayer-ng-spark-auth-confirm-email.mjs +200 -0
  8. package/fesm2022/mintplayer-ng-spark-auth-confirm-email.mjs.map +1 -0
  9. package/fesm2022/mintplayer-ng-spark-auth-core.mjs +452 -0
  10. package/fesm2022/mintplayer-ng-spark-auth-core.mjs.map +1 -0
  11. package/fesm2022/mintplayer-ng-spark-auth-external-login.mjs +201 -0
  12. package/fesm2022/mintplayer-ng-spark-auth-external-login.mjs.map +1 -0
  13. package/fesm2022/mintplayer-ng-spark-auth-forgot-password.mjs +196 -0
  14. package/fesm2022/mintplayer-ng-spark-auth-forgot-password.mjs.map +1 -0
  15. package/fesm2022/mintplayer-ng-spark-auth-guards.mjs +16 -0
  16. package/fesm2022/mintplayer-ng-spark-auth-guards.mjs.map +1 -0
  17. package/fesm2022/mintplayer-ng-spark-auth-interceptors.mjs +18 -0
  18. package/fesm2022/mintplayer-ng-spark-auth-interceptors.mjs.map +1 -0
  19. package/fesm2022/mintplayer-ng-spark-auth-login.mjs +282 -0
  20. package/fesm2022/mintplayer-ng-spark-auth-login.mjs.map +1 -0
  21. package/fesm2022/mintplayer-ng-spark-auth-models.mjs +114 -0
  22. package/fesm2022/mintplayer-ng-spark-auth-models.mjs.map +1 -0
  23. package/fesm2022/mintplayer-ng-spark-auth-pipes.mjs +43 -0
  24. package/fesm2022/mintplayer-ng-spark-auth-pipes.mjs.map +1 -0
  25. package/fesm2022/mintplayer-ng-spark-auth-register.mjs +216 -0
  26. package/fesm2022/mintplayer-ng-spark-auth-register.mjs.map +1 -0
  27. package/fesm2022/mintplayer-ng-spark-auth-reset-password.mjs +223 -0
  28. package/fesm2022/mintplayer-ng-spark-auth-reset-password.mjs.map +1 -0
  29. package/fesm2022/mintplayer-ng-spark-auth-routes.mjs +194 -0
  30. package/fesm2022/mintplayer-ng-spark-auth-routes.mjs.map +1 -0
  31. package/fesm2022/mintplayer-ng-spark-auth-sign-in.mjs +210 -0
  32. package/fesm2022/mintplayer-ng-spark-auth-sign-in.mjs.map +1 -0
  33. package/fesm2022/mintplayer-ng-spark-auth-two-factor.mjs +218 -0
  34. package/fesm2022/mintplayer-ng-spark-auth-two-factor.mjs.map +1 -0
  35. package/fesm2022/mintplayer-ng-spark-auth.mjs +48 -0
  36. package/fesm2022/mintplayer-ng-spark-auth.mjs.map +1 -0
  37. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +294 -2
  38. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  39. package/fesm2022/mintplayer-ng-spark-identity-provider-connected-applications.mjs +389 -0
  40. package/fesm2022/mintplayer-ng-spark-identity-provider-connected-applications.mjs.map +1 -0
  41. package/fesm2022/mintplayer-ng-spark-identity-provider-core.mjs +204 -0
  42. package/fesm2022/mintplayer-ng-spark-identity-provider-core.mjs.map +1 -0
  43. package/fesm2022/mintplayer-ng-spark-identity-provider-developers.mjs +486 -0
  44. package/fesm2022/mintplayer-ng-spark-identity-provider-developers.mjs.map +1 -0
  45. package/fesm2022/mintplayer-ng-spark-identity-provider-management.mjs +350 -0
  46. package/fesm2022/mintplayer-ng-spark-identity-provider-management.mjs.map +1 -0
  47. package/fesm2022/mintplayer-ng-spark-identity-provider.mjs +66 -0
  48. package/fesm2022/mintplayer-ng-spark-identity-provider.mjs.map +1 -0
  49. package/fesm2022/mintplayer-ng-spark-pwa.mjs +162 -0
  50. package/fesm2022/mintplayer-ng-spark-pwa.mjs.map +1 -0
  51. package/fesm2022/mintplayer-ng-spark-services.mjs +11 -1
  52. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  53. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  54. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  55. package/package.json +99 -1
  56. package/types/mintplayer-ng-spark-auth-account.d.ts +255 -0
  57. package/types/mintplayer-ng-spark-auth-auth-bar.d.ts +22 -0
  58. package/types/mintplayer-ng-spark-auth-confirm-email.d.ts +21 -0
  59. package/types/mintplayer-ng-spark-auth-core.d.ts +197 -0
  60. package/types/mintplayer-ng-spark-auth-external-login.d.ts +132 -0
  61. package/types/mintplayer-ng-spark-auth-forgot-password.d.ts +18 -0
  62. package/types/mintplayer-ng-spark-auth-guards.d.ts +15 -0
  63. package/types/mintplayer-ng-spark-auth-interceptors.d.ts +2 -0
  64. package/types/mintplayer-ng-spark-auth-login.d.ts +46 -0
  65. package/types/mintplayer-ng-spark-auth-models.d.ts +489 -0
  66. package/types/mintplayer-ng-spark-auth-pipes.d.ts +8 -0
  67. package/types/mintplayer-ng-spark-auth-register.d.ts +21 -0
  68. package/types/mintplayer-ng-spark-auth-reset-password.d.ts +25 -0
  69. package/types/mintplayer-ng-spark-auth-routes.d.ts +154 -0
  70. package/types/mintplayer-ng-spark-auth-sign-in.d.ts +84 -0
  71. package/types/mintplayer-ng-spark-auth-two-factor.d.ts +23 -0
  72. package/types/mintplayer-ng-spark-auth.d.ts +24 -0
  73. package/types/mintplayer-ng-spark-client-operations.d.ts +78 -3
  74. package/types/mintplayer-ng-spark-identity-provider-connected-applications.d.ts +35 -0
  75. package/types/mintplayer-ng-spark-identity-provider-core.d.ts +144 -0
  76. package/types/mintplayer-ng-spark-identity-provider-developers.d.ts +55 -0
  77. package/types/mintplayer-ng-spark-identity-provider-management.d.ts +33 -0
  78. package/types/mintplayer-ng-spark-identity-provider.d.ts +64 -0
  79. package/types/mintplayer-ng-spark-pwa.d.ts +89 -0
  80. package/types/mintplayer-ng-spark-services.d.ts +10 -0
  81. package/types/mintplayer-ng-spark-shell.d.ts +2 -2
  82. package/types/mintplayer-ng-spark.d.ts +4 -4
@@ -0,0 +1,201 @@
1
+ import * as i0 from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from "@angular/core";
3
+ import { NgTemplateOutlet } from "@angular/common";
4
+ import { ActivatedRoute, Router } from "@angular/router";
5
+ import { Color } from "@mintplayer/ng-bootstrap";
6
+ import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
7
+ import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
8
+ import { SPARK_AUTH_CONFIG, SPARK_EXTERNAL_PROVIDERS, isSafeReturnUrl } from "@mintplayer/ng-spark/auth/models";
9
+ import { SparkAuthService } from "@mintplayer/ng-spark/auth/core";
10
+ import { TranslateKeyPipe } from "@mintplayer/ng-spark/auth/pipes";
11
+ var SparkExternalLoginButtonsComponent = class SparkExternalLoginButtonsComponent {
12
+ authService = inject(SparkAuthService);
13
+ declaredProviders = inject(SPARK_EXTERNAL_PROVIDERS, { optional: true }) ?? [];
14
+ route = inject(ActivatedRoute, { optional: true });
15
+ router = inject(Router);
16
+ config = inject(SPARK_AUTH_CONFIG);
17
+ colors = Color;
18
+ returnUrl = input(void 0, ...ngDevMode ? [{ debugName: "returnUrl" }] : /* istanbul ignore next */ []);
19
+ providerTemplate = input(null, ...ngDevMode ? [{ debugName: "providerTemplate" }] : /* istanbul ignore next */ []);
20
+ reported = signal([], ...ngDevMode ? [{ debugName: "reported" }] : /* istanbul ignore next */ []);
21
+ loaded = signal(false, ...ngDevMode ? [{ debugName: "loaded" }] : /* istanbul ignore next */ []);
22
+ externalError = signal("", ...ngDevMode ? [{ debugName: "externalError" }] : /* istanbul ignore next */ []);
23
+ externalErrorIsNotice = computed(() => this.externalError() === "auth.externalLoginError.link_confirmation_sent", ...ngDevMode ? [{ debugName: "externalErrorIsNotice" }] : /* istanbul ignore next */ []);
24
+ blockedProvider = signal(null, ...ngDevMode ? [{ debugName: "blockedProvider" }] : /* istanbul ignore next */ []);
25
+ externalPending = computed(() => this.authService.externalLoginPending(), ...ngDevMode ? [{ debugName: "externalPending" }] : /* istanbul ignore next */ []);
26
+ providers = computed(() => {
27
+ const declarations = new Map(this.declaredProviders.map((p) => [p.scheme.toLowerCase(), p]));
28
+ return this.reported().map((provider, index) => {
29
+ const declared = declarations.get(provider.scheme.toLowerCase());
30
+ return {
31
+ provider: {
32
+ ...provider,
33
+ displayName: declared?.displayName ?? provider.displayName,
34
+ iconClass: declared?.iconClass
35
+ },
36
+ order: declared?.order ?? Number.MAX_SAFE_INTEGER,
37
+ index
38
+ };
39
+ }).sort((a, b) => a.order - b.order || a.index - b.index).map((entry) => entry.provider);
40
+ }, ...ngDevMode ? [{ debugName: "providers" }] : /* istanbul ignore next */ []);
41
+ hasProviders = computed(() => this.providers().length > 0, ...ngDevMode ? [{ debugName: "hasProviders" }] : /* istanbul ignore next */ []);
42
+ constructor() {
43
+ const returned = this.authService.takeExternalLoginResult();
44
+ if (returned) this.showExternalError(returned);
45
+ this.load();
46
+ }
47
+ async load() {
48
+ try {
49
+ const capabilities = await this.authService.capabilities();
50
+ this.reported.set(capabilities.externalProviders ?? []);
51
+ this.loaded.set(true);
52
+ } catch {}
53
+ }
54
+ effectiveReturnUrl() {
55
+ const explicit = this.returnUrl();
56
+ if (explicit) return explicit;
57
+ const fromQuery = this.route?.snapshot.queryParamMap.get("returnUrl");
58
+ return isSafeReturnUrl(fromQuery) ? fromQuery : void 0;
59
+ }
60
+ showExternalError(error) {
61
+ if (error === "popup_closed") return;
62
+ this.externalError.set(`auth.externalLoginError.${error ?? "no_login_info"}`);
63
+ }
64
+ contextFor(provider) {
65
+ return {
66
+ $implicit: provider,
67
+ signIn: () => this.signInWith(provider),
68
+ pending: this.externalPending()
69
+ };
70
+ }
71
+ async signInWith(provider, mode) {
72
+ this.externalError.set("");
73
+ this.blockedProvider.set(null);
74
+ const returnUrl = this.effectiveReturnUrl() ?? this.config.defaultRedirectUrl;
75
+ const result = await this.authService.loginWithProvider(provider.scheme, mode ? {
76
+ returnUrl,
77
+ mode
78
+ } : { returnUrl });
79
+ if (result.success) {
80
+ await this.router.navigateByUrl(returnUrl);
81
+ return;
82
+ }
83
+ this.showExternalError(result.error);
84
+ if (result.error === "popup_blocked") this.blockedProvider.set(provider);
85
+ }
86
+ continueInThisTab() {
87
+ const provider = this.blockedProvider();
88
+ return provider ? this.signInWith(provider, "redirect") : Promise.resolve();
89
+ }
90
+ static ɵfac = i0.ɵɵngDeclareFactory({
91
+ minVersion: "12.0.0",
92
+ version: "22.2.0",
93
+ ngImport: i0,
94
+ type: SparkExternalLoginButtonsComponent,
95
+ deps: [],
96
+ target: i0.ɵɵFactoryTarget.Component
97
+ });
98
+ static ɵcmp = i0.ɵɵngDeclareComponent({
99
+ minVersion: "17.0.0",
100
+ version: "22.2.0",
101
+ type: SparkExternalLoginButtonsComponent,
102
+ isStandalone: true,
103
+ selector: "spark-external-login-buttons",
104
+ inputs: {
105
+ returnUrl: {
106
+ classPropertyName: "returnUrl",
107
+ publicName: "returnUrl",
108
+ isSignal: true,
109
+ isRequired: false,
110
+ transformFunction: null
111
+ },
112
+ providerTemplate: {
113
+ classPropertyName: "providerTemplate",
114
+ publicName: "providerTemplate",
115
+ isSignal: true,
116
+ isRequired: false,
117
+ transformFunction: null
118
+ }
119
+ },
120
+ ngImport: i0,
121
+ template: "@if (loaded()) {\n <!-- D9: the external sign-in's outcome, from the popup or from a redirect's ?sparkExternalLogin.\n Shown even with no providers left to offer: the failure is still news. -->\n @if (externalError()) {\n <bs-alert [type]=\"externalErrorIsNotice() ? colors.info : colors.danger\" class=\"mb-2 d-block spark-external-error\">\n {{ externalError() | t }}\n @if (blockedProvider()) {\n <div class=\"mt-2\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary spark-continue-in-tab\" (click)=\"continueInThisTab()\">\n {{ 'auth.continueInThisTab' | t }}\n </button>\n </div>\n }\n </bs-alert>\n }\n\n @for (provider of providers(); track provider.scheme) {\n @if (providerTemplate(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: contextFor(provider)\" />\n } @else {\n <button type=\"button\" class=\"btn btn-outline-primary w-100 mb-2 spark-provider-button\"\n [disabled]=\"externalPending()\" (click)=\"signInWith(provider)\">\n @if (provider.iconClass) {\n <i [class]=\"provider.iconClass\" class=\"me-2\"></i>\n }\n {{ provider.displayName }}\n </button>\n }\n }\n\n <!-- While a popup is open: externalLoginPending, not the promise (D2). No Cancel button: a\n closed popup re-enables the buttons by itself, and a late result still signs the user in. -->\n @if (externalPending()) {\n <div class=\"text-center mb-2 spark-external-pending\"><bs-spinner /></div>\n }\n}\n",
122
+ dependencies: [
123
+ {
124
+ kind: "directive",
125
+ type: NgTemplateOutlet,
126
+ selector: "[ngTemplateOutlet]",
127
+ inputs: [
128
+ "ngTemplateOutletContext",
129
+ "ngTemplateOutlet",
130
+ "ngTemplateOutletInjector"
131
+ ]
132
+ },
133
+ {
134
+ kind: "component",
135
+ type: BsAlertComponent,
136
+ selector: "bs-alert",
137
+ inputs: [
138
+ "type",
139
+ "announce",
140
+ "isVisible"
141
+ ],
142
+ outputs: ["isVisibleChange", "afterOpenedOrClosed"]
143
+ },
144
+ {
145
+ kind: "component",
146
+ type: BsSpinnerComponent,
147
+ selector: "bs-spinner",
148
+ inputs: ["type", "color"]
149
+ },
150
+ {
151
+ kind: "pipe",
152
+ type: TranslateKeyPipe,
153
+ name: "t"
154
+ }
155
+ ],
156
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
157
+ });
158
+ };
159
+ i0.ɵɵngDeclareClassMetadata({
160
+ minVersion: "12.0.0",
161
+ version: "22.2.0",
162
+ ngImport: i0,
163
+ type: SparkExternalLoginButtonsComponent,
164
+ decorators: [{
165
+ type: Component,
166
+ args: [{
167
+ selector: "spark-external-login-buttons",
168
+ standalone: true,
169
+ changeDetection: ChangeDetectionStrategy.OnPush,
170
+ imports: [
171
+ NgTemplateOutlet,
172
+ BsAlertComponent,
173
+ BsSpinnerComponent,
174
+ TranslateKeyPipe
175
+ ],
176
+ template: "@if (loaded()) {\n <!-- D9: the external sign-in's outcome, from the popup or from a redirect's ?sparkExternalLogin.\n Shown even with no providers left to offer: the failure is still news. -->\n @if (externalError()) {\n <bs-alert [type]=\"externalErrorIsNotice() ? colors.info : colors.danger\" class=\"mb-2 d-block spark-external-error\">\n {{ externalError() | t }}\n @if (blockedProvider()) {\n <div class=\"mt-2\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary spark-continue-in-tab\" (click)=\"continueInThisTab()\">\n {{ 'auth.continueInThisTab' | t }}\n </button>\n </div>\n }\n </bs-alert>\n }\n\n @for (provider of providers(); track provider.scheme) {\n @if (providerTemplate(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: contextFor(provider)\" />\n } @else {\n <button type=\"button\" class=\"btn btn-outline-primary w-100 mb-2 spark-provider-button\"\n [disabled]=\"externalPending()\" (click)=\"signInWith(provider)\">\n @if (provider.iconClass) {\n <i [class]=\"provider.iconClass\" class=\"me-2\"></i>\n }\n {{ provider.displayName }}\n </button>\n }\n }\n\n <!-- While a popup is open: externalLoginPending, not the promise (D2). No Cancel button: a\n closed popup re-enables the buttons by itself, and a late result still signs the user in. -->\n @if (externalPending()) {\n <div class=\"text-center mb-2 spark-external-pending\"><bs-spinner /></div>\n }\n}\n"
177
+ }]
178
+ }],
179
+ ctorParameters: () => [],
180
+ propDecorators: {
181
+ returnUrl: [{
182
+ type: i0.Input,
183
+ args: [{
184
+ isSignal: true,
185
+ alias: "returnUrl",
186
+ required: false
187
+ }]
188
+ }],
189
+ providerTemplate: [{
190
+ type: i0.Input,
191
+ args: [{
192
+ isSignal: true,
193
+ alias: "providerTemplate",
194
+ required: false
195
+ }]
196
+ }]
197
+ }
198
+ });
199
+ export { SparkExternalLoginButtonsComponent };
200
+
201
+ //# sourceMappingURL=mintplayer-ng-spark-auth-external-login.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mintplayer-ng-spark-auth-external-login.mjs","names":[],"sources":["../../auth/external-login/src/spark-external-login-buttons.component.ts","../../auth/external-login/src/spark-external-login-buttons.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, TemplateRef, computed, inject, input, signal } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport {\n SPARK_AUTH_CONFIG,\n SPARK_EXTERNAL_PROVIDERS,\n SparkExternalProvider,\n SparkExternalProviderPresentation,\n SparkExternalLoginError,\n SparkExternalLoginMode,\n isSafeReturnUrl,\n} from '@mintplayer/ng-spark/auth/models';\nimport { SparkAuthService } from '@mintplayer/ng-spark/auth/core';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/auth/pipes';\n\n/**\n * One provider's button, as the template sees it: the provider itself, and the call that signs in\n * with it.\n *\n * **Passing the closure is the point.** A consumer never touches `provider.scheme`, so the failure\n * this component exists to prevent — a hard-coded scheme string that silently stops matching when\n * the server's registration changes — is unreachable by construction. It also means busy state,\n * `returnUrl` handling and popup-versus-redirect can move behind `signIn` later without a\n * consumer-facing change.\n */\nexport interface SparkProviderButtonContext {\n $implicit: SparkExternalProviderView;\n signIn: () => void;\n /**\n * Whether a sign-in popup is open: disable the button while it is. Read from the service's\n * `externalLoginPending`, never from the sign-in promise (D2).\n */\n pending: boolean;\n}\n\n/** A provider the server reported, merged with whatever presentation the app declared for it. */\nexport interface SparkExternalProviderView extends SparkExternalProvider {\n iconClass?: string;\n}\n\n/**\n * A button per external provider the server reports, and the outcome of the last attempt with one.\n *\n * Shared by the sign-in landing page and the password login page, so both offer the same providers\n * with the same behaviour. Hostable on any page of an application's own as well.\n *\n * The providers come from `GET /spark/auth/capabilities` rather than from a list the application\n * hard-codes: every consumer that hand-rolled this before wrote the scheme name as a string literal,\n * which is a silent mismatch waiting to happen the moment the server's provider registration\n * changes. A `withExternalLogin(githubProvider())` declaration only *decorates* what the server\n * reports — it cannot conjure a provider the server does not have.\n *\n * Renders nothing while the capabilities load, when they fail to load (the host decides what that\n * means for its page), and when the server reports no providers. {@link hasProviders} tells a host\n * which of those it is, e.g. to draw an \"or\" divider only when there is something to divide.\n */\n@Component({\n selector: 'spark-external-login-buttons',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [NgTemplateOutlet, BsAlertComponent, BsSpinnerComponent, TranslateKeyPipe],\n templateUrl: './spark-external-login-buttons.component.html',\n})\nexport class SparkExternalLoginButtonsComponent {\n private readonly authService = inject(SparkAuthService);\n private readonly declaredProviders = inject(SPARK_EXTERNAL_PROVIDERS, { optional: true }) ?? [];\n private readonly route = inject(ActivatedRoute, { optional: true });\n private readonly router = inject(Router);\n private readonly config = inject(SPARK_AUTH_CONFIG);\n readonly colors = Color;\n\n /**\n * Where to land after a successful external sign-in.\n *\n * Falls back to a `?returnUrl=` query parameter, matching what the login page already does — so a\n * plain `<a routerLink=\"/sign-in\" [queryParams]=\"{ returnUrl: '/somewhere' }\">` works with no\n * wiring, which is what a routed page needs since the router passes it no inputs. The query value\n * is validated as a local path before use; an off-site one is dropped rather than followed.\n */\n readonly returnUrl = input<string | undefined>(undefined);\n\n /**\n * Replaces the default provider button. Rendered once per provider with a\n * {@link SparkProviderButtonContext}.\n */\n readonly providerTemplate = input<TemplateRef<SparkProviderButtonContext> | null>(null);\n\n private readonly reported = signal<SparkExternalProvider[]>([]);\n\n /** The capabilities arrived. Until then, and when they fail, nothing renders. */\n readonly loaded = signal(false);\n\n /**\n * The translation key of the last external sign-in failure, from the popup's result or from the\n * `?sparkExternalLogin` a redirect came back with.\n */\n readonly externalError = signal('');\n\n /** `link_confirmation_sent` travels as a failure but is news (check your mail), not an error. */\n readonly externalErrorIsNotice = computed(() => this.externalError() === 'auth.externalLoginError.link_confirmation_sent');\n\n /**\n * The provider whose popup `window.open` refused. The component then offers \"Continue in this\n * tab\": the same provider in redirect mode. Not automatic (F10): in Firefox's installed web app a\n * null handle may still have started the flow inside the app, and a redirect would run a second one.\n */\n readonly blockedProvider = signal<SparkExternalProviderView | null>(null);\n\n /**\n * While a sign-in popup is open. The buttons follow this, not the promise: under COOP the popup\n * reads closed long before its result arrives, and the attempt keeps listening (D2).\n */\n readonly externalPending = computed(() => this.authService.externalLoginPending());\n\n /**\n * The server's list, decorated and ordered by whatever the application declared. Declared-but-not-\n * reported schemes are dropped and reported-but-not-declared ones keep a default button, so\n * neither side can produce a provider the other does not have.\n */\n readonly providers = computed<SparkExternalProviderView[]>(() => {\n const declarations = new Map<string, SparkExternalProviderPresentation>(\n this.declaredProviders.map(p => [p.scheme.toLowerCase(), p]),\n );\n\n return this.reported()\n .map((provider, index) => {\n const declared = declarations.get(provider.scheme.toLowerCase());\n return {\n provider: {\n ...provider,\n displayName: declared?.displayName ?? provider.displayName,\n iconClass: declared?.iconClass,\n } satisfies SparkExternalProviderView,\n // Undeclared providers sort after declared ones, keeping the server's relative order among\n // themselves — so adding a provider server-side appends a button rather than reshuffling.\n order: declared?.order ?? Number.MAX_SAFE_INTEGER,\n index,\n };\n })\n .sort((a, b) => a.order - b.order || a.index - b.index)\n .map(entry => entry.provider);\n });\n\n /** Whether the server reported at least one provider, i.e. whether this component shows buttons. */\n readonly hasProviders = computed(() => this.providers().length > 0);\n\n constructor() {\n // A redirect-mode round trip (inside an installed web app, or \"Continue in this tab\") comes back\n // with its failure in the URL. The service answers each failure once, so a second instance on\n // the same page cannot show it twice.\n const returned = this.authService.takeExternalLoginResult();\n if (returned) this.showExternalError(returned);\n void this.load();\n }\n\n private async load(): Promise<void> {\n try {\n // Shared with the host's own request when both ask at once (the service joins in-flight calls).\n const capabilities = await this.authService.capabilities();\n this.reported.set(capabilities.externalProviders ?? []);\n this.loaded.set(true);\n } catch {\n // The host owns the page: the sign-in page says sign-in is unavailable, the login page still\n // offers its password form. Rendering nothing here lets each do so.\n }\n }\n\n private effectiveReturnUrl(): string | undefined {\n const explicit = this.returnUrl();\n if (explicit) return explicit;\n\n const fromQuery = this.route?.snapshot.queryParamMap.get('returnUrl');\n return isSafeReturnUrl(fromQuery) ? fromQuery! : undefined;\n }\n\n /** `popup_closed` is \"not now\", not an error, so it shows nothing. */\n private showExternalError(error: SparkExternalLoginError | undefined): void {\n if (error === 'popup_closed') return;\n this.externalError.set(`auth.externalLoginError.${error ?? 'no_login_info'}`);\n }\n\n /** The closure handed to a projected template, so a consumer never names a scheme itself. */\n contextFor(provider: SparkExternalProviderView): SparkProviderButtonContext {\n return { $implicit: provider, signIn: () => this.signInWith(provider), pending: this.externalPending() };\n }\n\n /**\n * Signs in, then *leaves this page*.\n *\n * The navigation is the part that is easy to miss. In `'popup'` mode the `returnUrl` is consumed\n * by the popup — it tells the server where to send that window before it closes — and the opener,\n * which is the tab the user is actually looking at, is never touched. So without this the sign-in\n * succeeds, the topbar flips to the signed-in state, and the user is left staring at the login\n * page wondering whether it worked.\n *\n * Failures deliberately do not navigate: the component renders the error, and moving away would\n * hide it. `popup_closed` is not an error — it is \"not now\" — so it shows nothing. `popup_blocked`\n * also offers \"Continue in this tab\" ({@link continueInThisTab}).\n *\n * `mode` is left out unless given, so the configured `externalLoginMode` decides.\n */\n async signInWith(provider: SparkExternalProviderView, mode?: SparkExternalLoginMode): Promise<void> {\n this.externalError.set('');\n this.blockedProvider.set(null);\n const returnUrl = this.effectiveReturnUrl() ?? this.config.defaultRedirectUrl;\n const result = await this.authService.loginWithProvider(provider.scheme, mode ? { returnUrl, mode } : { returnUrl });\n if (result.success) {\n await this.router.navigateByUrl(returnUrl);\n return;\n }\n this.showExternalError(result.error);\n if (result.error === 'popup_blocked') this.blockedProvider.set(provider);\n }\n\n /** Retries the provider whose popup was blocked as a full-page redirect in this tab. */\n continueInThisTab(): Promise<void> {\n const provider = this.blockedProvider();\n return provider ? this.signInWith(provider, 'redirect') : Promise.resolve();\n }\n}\n\nexport default SparkExternalLoginButtonsComponent;\n","@if (loaded()) {\n <!-- D9: the external sign-in's outcome, from the popup or from a redirect's ?sparkExternalLogin.\n Shown even with no providers left to offer: the failure is still news. -->\n @if (externalError()) {\n <bs-alert [type]=\"externalErrorIsNotice() ? colors.info : colors.danger\" class=\"mb-2 d-block spark-external-error\">\n {{ externalError() | t }}\n @if (blockedProvider()) {\n <div class=\"mt-2\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary spark-continue-in-tab\" (click)=\"continueInThisTab()\">\n {{ 'auth.continueInThisTab' | t }}\n </button>\n </div>\n }\n </bs-alert>\n }\n\n @for (provider of providers(); track provider.scheme) {\n @if (providerTemplate(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: contextFor(provider)\" />\n } @else {\n <button type=\"button\" class=\"btn btn-outline-primary w-100 mb-2 spark-provider-button\"\n [disabled]=\"externalPending()\" (click)=\"signInWith(provider)\">\n @if (provider.iconClass) {\n <i [class]=\"provider.iconClass\" class=\"me-2\"></i>\n }\n {{ provider.displayName }}\n </button>\n }\n }\n\n <!-- While a popup is open: externalLoginPending, not the promise (D2). No Cancel button: a\n closed popup re-enables the buttons by itself, and a late result still signs the user in. -->\n @if (externalPending()) {\n <div class=\"text-center mb-2 spark-external-pending\"><bs-spinner /></div>\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,196 @@
1
+ import * as i0 from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, inject, signal } from "@angular/core";
3
+ import * as i1 from "@angular/forms";
4
+ import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
5
+ import { RouterLink } from "@angular/router";
6
+ import { Color } from "@mintplayer/ng-bootstrap";
7
+ import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
8
+ import { BsCardComponent, BsCardHeaderComponent } from "@mintplayer/ng-bootstrap/card";
9
+ import { BsFormComponent, BsFormControlDirective } from "@mintplayer/ng-bootstrap/form";
10
+ import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
11
+ import { SPARK_AUTH_ROUTE_PATHS } from "@mintplayer/ng-spark/auth/models";
12
+ import { SparkAuthService, SparkAuthTranslationService } from "@mintplayer/ng-spark/auth/core";
13
+ import { TranslateKeyPipe } from "@mintplayer/ng-spark/auth/pipes";
14
+ var SparkForgotPasswordComponent = class SparkForgotPasswordComponent {
15
+ fb = inject(FormBuilder);
16
+ authService = inject(SparkAuthService);
17
+ translation = inject(SparkAuthTranslationService);
18
+ routePaths = inject(SPARK_AUTH_ROUTE_PATHS);
19
+ colors = Color;
20
+ loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
21
+ errorMessage = signal("", ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
22
+ successMessage = signal("", ...ngDevMode ? [{ debugName: "successMessage" }] : /* istanbul ignore next */ []);
23
+ form = this.fb.group({ email: ["", [Validators.required, Validators.email]] });
24
+ async onSubmit() {
25
+ if (this.form.invalid) {
26
+ this.form.markAllAsTouched();
27
+ return;
28
+ }
29
+ this.loading.set(true);
30
+ this.errorMessage.set("");
31
+ this.successMessage.set("");
32
+ const { email } = this.form.value;
33
+ try {
34
+ await this.authService.forgotPassword(email);
35
+ } catch {}
36
+ this.successMessage.set(this.translation.t("auth.forgotPasswordSuccess"));
37
+ this.loading.set(false);
38
+ }
39
+ static ɵfac = i0.ɵɵngDeclareFactory({
40
+ minVersion: "12.0.0",
41
+ version: "22.2.0",
42
+ ngImport: i0,
43
+ type: SparkForgotPasswordComponent,
44
+ deps: [],
45
+ target: i0.ɵɵFactoryTarget.Component
46
+ });
47
+ static ɵcmp = i0.ɵɵngDeclareComponent({
48
+ minVersion: "17.0.0",
49
+ version: "22.2.0",
50
+ type: SparkForgotPasswordComponent,
51
+ isStandalone: true,
52
+ selector: "spark-forgot-password",
53
+ ngImport: i0,
54
+ 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.forgotPasswordTitle' | t }}</h3>\n </bs-card-header>\n <div class=\"p-4\">\n @if (successMessage()) {\n <bs-alert [type]=\"colors.success\" class=\"mb-3\">{{ successMessage() }}</bs-alert>\n }\n\n @if (errorMessage()) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">{{ errorMessage() }}</bs-alert>\n }\n\n @if (!successMessage()) {\n <p class=\"text-muted mb-3\">\n {{ 'auth.forgotPasswordDescription' | t }}\n </p>\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 </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.sendResetLink' | t }}\n </button>\n </form>\n </bs-form>\n }\n\n <div class=\"mt-3 text-center\">\n <a [routerLink]=\"routePaths.login\">{{ 'auth.backToLogin' | t }}</a>\n </div>\n </div>\n </bs-card>\n</div>\n",
55
+ dependencies: [
56
+ {
57
+ kind: "ngmodule",
58
+ type: ReactiveFormsModule
59
+ },
60
+ {
61
+ kind: "directive",
62
+ type: i1.ɵNgNoValidate,
63
+ selector: "form:not([ngNoForm]):not([ngNativeValidate])"
64
+ },
65
+ {
66
+ kind: "directive",
67
+ type: i1.DefaultValueAccessor,
68
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
69
+ },
70
+ {
71
+ kind: "directive",
72
+ type: i1.NgControlStatus,
73
+ selector: "[formControlName],[ngModel],[formControl]"
74
+ },
75
+ {
76
+ kind: "directive",
77
+ type: i1.NgControlStatusGroup,
78
+ selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
79
+ },
80
+ {
81
+ kind: "directive",
82
+ type: i1.FormGroupDirective,
83
+ selector: "[formGroup]",
84
+ inputs: ["formGroup"],
85
+ outputs: ["ngSubmit"],
86
+ exportAs: ["ngForm"]
87
+ },
88
+ {
89
+ kind: "directive",
90
+ type: i1.FormControlName,
91
+ selector: "[formControlName]",
92
+ inputs: [
93
+ "formControlName",
94
+ "disabled",
95
+ "ngModel"
96
+ ],
97
+ outputs: ["ngModelChange"]
98
+ },
99
+ {
100
+ kind: "directive",
101
+ type: RouterLink,
102
+ selector: "[routerLink]",
103
+ inputs: [
104
+ "target",
105
+ "queryParams",
106
+ "fragment",
107
+ "queryParamsHandling",
108
+ "state",
109
+ "info",
110
+ "relativeTo",
111
+ "preserveFragment",
112
+ "skipLocationChange",
113
+ "replaceUrl",
114
+ "browserUrl",
115
+ "routerLink"
116
+ ]
117
+ },
118
+ {
119
+ kind: "component",
120
+ type: BsAlertComponent,
121
+ selector: "bs-alert",
122
+ inputs: [
123
+ "type",
124
+ "announce",
125
+ "isVisible"
126
+ ],
127
+ outputs: ["isVisibleChange", "afterOpenedOrClosed"]
128
+ },
129
+ {
130
+ kind: "component",
131
+ type: BsCardComponent,
132
+ selector: "bs-card",
133
+ inputs: ["color", "outline"]
134
+ },
135
+ {
136
+ kind: "component",
137
+ type: BsCardHeaderComponent,
138
+ selector: "bs-card-header",
139
+ inputs: ["color", "navStyle"]
140
+ },
141
+ {
142
+ kind: "component",
143
+ type: BsFormComponent,
144
+ selector: "bs-form",
145
+ inputs: ["action", "method"],
146
+ outputs: ["submitted"]
147
+ },
148
+ {
149
+ kind: "directive",
150
+ type: BsFormControlDirective,
151
+ selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)"
152
+ },
153
+ {
154
+ kind: "component",
155
+ type: BsSpinnerComponent,
156
+ selector: "bs-spinner",
157
+ inputs: ["type", "color"]
158
+ },
159
+ {
160
+ kind: "pipe",
161
+ type: TranslateKeyPipe,
162
+ name: "t"
163
+ }
164
+ ],
165
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
166
+ });
167
+ };
168
+ i0.ɵɵngDeclareClassMetadata({
169
+ minVersion: "12.0.0",
170
+ version: "22.2.0",
171
+ ngImport: i0,
172
+ type: SparkForgotPasswordComponent,
173
+ decorators: [{
174
+ type: Component,
175
+ args: [{
176
+ selector: "spark-forgot-password",
177
+ standalone: true,
178
+ changeDetection: ChangeDetectionStrategy.OnPush,
179
+ imports: [
180
+ ReactiveFormsModule,
181
+ RouterLink,
182
+ BsAlertComponent,
183
+ BsCardComponent,
184
+ BsCardHeaderComponent,
185
+ BsFormComponent,
186
+ BsFormControlDirective,
187
+ BsSpinnerComponent,
188
+ TranslateKeyPipe
189
+ ],
190
+ 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.forgotPasswordTitle' | t }}</h3>\n </bs-card-header>\n <div class=\"p-4\">\n @if (successMessage()) {\n <bs-alert [type]=\"colors.success\" class=\"mb-3\">{{ successMessage() }}</bs-alert>\n }\n\n @if (errorMessage()) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">{{ errorMessage() }}</bs-alert>\n }\n\n @if (!successMessage()) {\n <p class=\"text-muted mb-3\">\n {{ 'auth.forgotPasswordDescription' | t }}\n </p>\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 </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.sendResetLink' | t }}\n </button>\n </form>\n </bs-form>\n }\n\n <div class=\"mt-3 text-center\">\n <a [routerLink]=\"routePaths.login\">{{ 'auth.backToLogin' | t }}</a>\n </div>\n </div>\n </bs-card>\n</div>\n"
191
+ }]
192
+ }]
193
+ });
194
+ export { SparkForgotPasswordComponent };
195
+
196
+ //# sourceMappingURL=mintplayer-ng-spark-auth-forgot-password.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mintplayer-ng-spark-auth-forgot-password.mjs","names":[],"sources":["../../auth/forgot-password/src/spark-forgot-password.component.ts","../../auth/forgot-password/src/spark-forgot-password.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';\nimport { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';\nimport { RouterLink } from '@angular/router';\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\n@Component({\n selector: 'spark-forgot-password',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [ReactiveFormsModule, RouterLink, BsAlertComponent, BsCardComponent, BsCardHeaderComponent, BsFormComponent, BsFormControlDirective, BsSpinnerComponent, TranslateKeyPipe],\n templateUrl: './spark-forgot-password.component.html',\n})\nexport class SparkForgotPasswordComponent {\n private readonly fb = inject(FormBuilder);\n private readonly authService = inject(SparkAuthService);\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 readonly successMessage = signal('');\n\n readonly form = this.fb.group({\n email: ['', [Validators.required, Validators.email]],\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 this.successMessage.set('');\n\n const { email } = this.form.value;\n\n try {\n await this.authService.forgotPassword(email!);\n } catch {\n // Don't reveal whether the email exists\n }\n this.successMessage.set(this.translation.t('auth.forgotPasswordSuccess'));\n this.loading.set(false);\n }\n}\n\nexport default SparkForgotPasswordComponent;\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.forgotPasswordTitle' | t }}</h3>\n </bs-card-header>\n <div class=\"p-4\">\n @if (successMessage()) {\n <bs-alert [type]=\"colors.success\" class=\"mb-3\">{{ successMessage() }}</bs-alert>\n }\n\n @if (errorMessage()) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">{{ errorMessage() }}</bs-alert>\n }\n\n @if (!successMessage()) {\n <p class=\"text-muted mb-3\">\n {{ 'auth.forgotPasswordDescription' | t }}\n </p>\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 </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.sendResetLink' | t }}\n </button>\n </form>\n </bs-form>\n }\n\n <div class=\"mt-3 text-center\">\n <a [routerLink]=\"routePaths.login\">{{ 'auth.backToLogin' | t }}</a>\n </div>\n </div>\n </bs-card>\n</div>\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { inject } from "@angular/core";
2
+ import { Router } from "@angular/router";
3
+ import { SPARK_AUTH_CONFIG, resolveSignInUrl } from "@mintplayer/ng-spark/auth/models";
4
+ import { SparkAuthService } from "@mintplayer/ng-spark/auth/core";
5
+ const sparkAuthGuard = async (route, state) => {
6
+ const authService = inject(SparkAuthService);
7
+ const router = inject(Router);
8
+ const config = inject(SPARK_AUTH_CONFIG);
9
+ if (authService.isAuthenticated()) return true;
10
+ if ((await authService.checkAuth())?.isAuthenticated) return true;
11
+ return router.createUrlTree([resolveSignInUrl(config, router)], { queryParams: { returnUrl: state.url } });
12
+ };
13
+ const sparkAuthenticatedGuard = sparkAuthGuard;
14
+ export { sparkAuthGuard, sparkAuthenticatedGuard };
15
+
16
+ //# sourceMappingURL=mintplayer-ng-spark-auth-guards.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mintplayer-ng-spark-auth-guards.mjs","names":[],"sources":["../../auth/guards/src/spark-auth.guard.ts"],"sourcesContent":["import { inject } from '@angular/core';\nimport { CanActivateFn, Router } from '@angular/router';\nimport { SPARK_AUTH_CONFIG, resolveSignInUrl } from '@mintplayer/ng-spark/auth/models';\nimport { SparkAuthService } from '@mintplayer/ng-spark/auth/core';\n\n/**\n * Lets a signed-in user through and sends everyone else to the sign-in page with a `returnUrl`.\n *\n * `SparkAuthService` reads `/me` asynchronously at start-up, so on a hard reload the user is not\n * known to be signed in yet when the router runs this guard. When the session is not known, the\n * guard waits for the session check before deciding, so a signed-in user is never bounced to the\n * sign-in page. A session that is already known passes without a round trip.\n */\nexport const sparkAuthGuard: CanActivateFn = async (route, state) => {\n const authService = inject(SparkAuthService);\n const router = inject(Router);\n const config = inject(SPARK_AUTH_CONFIG);\n\n if (authService.isAuthenticated()) return true;\n const user = await authService.checkAuth();\n if (user?.isAuthenticated) return true;\n\n return router.createUrlTree([resolveSignInUrl(config, router)], {\n queryParams: { returnUrl: state.url },\n });\n};\n\n/**\n * The same guard as {@link sparkAuthGuard}. It existed separately while `sparkAuthGuard` decided\n * before the session check had finished; both now wait for it.\n */\nexport const sparkAuthenticatedGuard: CanActivateFn = sparkAuthGuard;\n"],"mappings":";;;;AAaA,MAAa,iBAAgC,OAAO,OAAO,UAAS;CAClE,MAAM,cAAc,OAAO,gBAAgB;CAC3C,MAAM,SAAS,OAAO,MAAM;CAC5B,MAAM,SAAS,OAAO,iBAAiB;CAEvC,IAAI,YAAY,gBAAe,GAAI,OAAO;CAE1C,KAAI,MADe,YAAY,UAAS,EACpC,EAAM,iBAAiB,OAAO;CAElC,OAAO,OAAO,cAAc,CAAC,iBAAiB,QAAQ,MAAM,CAAC,GAAG,EAC9D,aAAa,EAAE,WAAW,MAAM,IAAG,EACpC,CAAA;AACH;AAMA,MAAa,0BAAyC"}
@@ -0,0 +1,18 @@
1
+ import { inject } from "@angular/core";
2
+ import { Router } from "@angular/router";
3
+ import { tap } from "rxjs";
4
+ import { SPARK_AUTH_CONFIG, resolveSignInUrl, sanitizeReturnUrl } from "@mintplayer/ng-spark/auth/models";
5
+ const sparkAuthInterceptor = (req, next) => {
6
+ const config = inject(SPARK_AUTH_CONFIG);
7
+ const router = inject(Router);
8
+ return next(req).pipe(tap({ error: (error) => {
9
+ if (error.status === 401 && !req.url.startsWith(config.apiBasePath)) {
10
+ const pathOnly = router.url.split("?")[0].split("#")[0];
11
+ const returnUrl = sanitizeReturnUrl(pathOnly, config.defaultRedirectUrl);
12
+ router.navigate([resolveSignInUrl(config, router)], { queryParams: { returnUrl } });
13
+ }
14
+ } }));
15
+ };
16
+ export { sparkAuthInterceptor };
17
+
18
+ //# sourceMappingURL=mintplayer-ng-spark-auth-interceptors.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mintplayer-ng-spark-auth-interceptors.mjs","names":[],"sources":["../../auth/interceptors/src/spark-auth.interceptor.ts"],"sourcesContent":["import { inject } from '@angular/core';\nimport { HttpInterceptorFn } from '@angular/common/http';\nimport { Router } from '@angular/router';\nimport { tap } from 'rxjs';\nimport { SPARK_AUTH_CONFIG, resolveSignInUrl, sanitizeReturnUrl } from '@mintplayer/ng-spark/auth/models';\n\nexport const sparkAuthInterceptor: HttpInterceptorFn = (req, next) => {\n const config = inject(SPARK_AUTH_CONFIG);\n const router = inject(Router);\n\n return next(req).pipe(\n tap({\n error: (error) => {\n if (\n error.status === 401 &&\n !req.url.startsWith(config.apiBasePath)\n ) {\n // R2-L4: drop query string and fragment from the captured URL before\n // putting it on the login URL. The unmodified router.url leaks\n // in-app draft text / document IDs / search queries into the next\n // access log entry for /login. Also pass through the same sanitizer\n // the login component uses so a path with embedded // can't reflect\n // back as an open redirect after authentication.\n const pathOnly = router.url.split('?')[0].split('#')[0];\n const returnUrl = sanitizeReturnUrl(pathOnly, config.defaultRedirectUrl);\n router.navigate([resolveSignInUrl(config, router)], {\n queryParams: { returnUrl },\n });\n }\n },\n }),\n );\n};\n"],"mappings":";;;;AAMA,MAAa,wBAA2C,KAAK,SAAQ;CACnE,MAAM,SAAS,OAAO,iBAAiB;CACvC,MAAM,SAAS,OAAO,MAAM;CAE5B,OAAO,KAAK,GAAG,CAAC,CAAC,KACf,IAAI,EACF,QAAQ,UAAS;EACf,IACE,MAAM,WAAW,OACjB,CAAC,IAAI,IAAI,WAAW,OAAO,WAAW,GACtC;GAOA,MAAM,WAAW,OAAO,IAAI,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;GACrD,MAAM,YAAY,kBAAkB,UAAU,OAAO,kBAAkB;GACvE,OAAO,SAAS,CAAC,iBAAiB,QAAQ,MAAM,CAAC,GAAG,EAClD,aAAa,EAAE,UAAS,EACzB,CAAA;EACH;CACF,EACD,CAAA,CAAC;AAEN"}