@mintplayer/ng-spark 22.29.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.
- package/README.md +3 -3
- package/auth/README.md +216 -0
- package/fesm2022/mintplayer-ng-spark-auth-account.mjs +2023 -0
- package/fesm2022/mintplayer-ng-spark-auth-account.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs +96 -0
- package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-confirm-email.mjs +200 -0
- package/fesm2022/mintplayer-ng-spark-auth-confirm-email.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-core.mjs +452 -0
- package/fesm2022/mintplayer-ng-spark-auth-core.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-external-login.mjs +201 -0
- package/fesm2022/mintplayer-ng-spark-auth-external-login.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-forgot-password.mjs +196 -0
- package/fesm2022/mintplayer-ng-spark-auth-forgot-password.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-guards.mjs +16 -0
- package/fesm2022/mintplayer-ng-spark-auth-guards.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-interceptors.mjs +18 -0
- package/fesm2022/mintplayer-ng-spark-auth-interceptors.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-login.mjs +282 -0
- package/fesm2022/mintplayer-ng-spark-auth-login.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-models.mjs +114 -0
- package/fesm2022/mintplayer-ng-spark-auth-models.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-pipes.mjs +43 -0
- package/fesm2022/mintplayer-ng-spark-auth-pipes.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-register.mjs +216 -0
- package/fesm2022/mintplayer-ng-spark-auth-register.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-reset-password.mjs +223 -0
- package/fesm2022/mintplayer-ng-spark-auth-reset-password.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-routes.mjs +194 -0
- package/fesm2022/mintplayer-ng-spark-auth-routes.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-sign-in.mjs +210 -0
- package/fesm2022/mintplayer-ng-spark-auth-sign-in.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth-two-factor.mjs +218 -0
- package/fesm2022/mintplayer-ng-spark-auth-two-factor.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-auth.mjs +48 -0
- package/fesm2022/mintplayer-ng-spark-auth.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs +368 -2
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-grid.mjs +22 -9
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-identity-provider-connected-applications.mjs +389 -0
- package/fesm2022/mintplayer-ng-spark-identity-provider-connected-applications.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-identity-provider-core.mjs +204 -0
- package/fesm2022/mintplayer-ng-spark-identity-provider-core.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-identity-provider-developers.mjs +486 -0
- package/fesm2022/mintplayer-ng-spark-identity-provider-developers.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-identity-provider-management.mjs +350 -0
- package/fesm2022/mintplayer-ng-spark-identity-provider-management.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-identity-provider.mjs +66 -0
- package/fesm2022/mintplayer-ng-spark-identity-provider.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-models.mjs +2 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +13 -3
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +6 -3
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-pwa.mjs +162 -0
- package/fesm2022/mintplayer-ng-spark-pwa.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +5 -2
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs +109 -4
- package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +36 -6
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-services.mjs +48 -6
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
- package/package.json +101 -3
- package/types/mintplayer-ng-spark-auth-account.d.ts +255 -0
- package/types/mintplayer-ng-spark-auth-auth-bar.d.ts +22 -0
- package/types/mintplayer-ng-spark-auth-confirm-email.d.ts +21 -0
- package/types/mintplayer-ng-spark-auth-core.d.ts +197 -0
- package/types/mintplayer-ng-spark-auth-external-login.d.ts +132 -0
- package/types/mintplayer-ng-spark-auth-forgot-password.d.ts +18 -0
- package/types/mintplayer-ng-spark-auth-guards.d.ts +15 -0
- package/types/mintplayer-ng-spark-auth-interceptors.d.ts +2 -0
- package/types/mintplayer-ng-spark-auth-login.d.ts +46 -0
- package/types/mintplayer-ng-spark-auth-models.d.ts +489 -0
- package/types/mintplayer-ng-spark-auth-pipes.d.ts +8 -0
- package/types/mintplayer-ng-spark-auth-register.d.ts +21 -0
- package/types/mintplayer-ng-spark-auth-reset-password.d.ts +25 -0
- package/types/mintplayer-ng-spark-auth-routes.d.ts +154 -0
- package/types/mintplayer-ng-spark-auth-sign-in.d.ts +84 -0
- package/types/mintplayer-ng-spark-auth-two-factor.d.ts +23 -0
- package/types/mintplayer-ng-spark-auth.d.ts +24 -0
- package/types/mintplayer-ng-spark-client-operations.d.ts +148 -3
- package/types/mintplayer-ng-spark-grid.d.ts +19 -1
- package/types/mintplayer-ng-spark-identity-provider-connected-applications.d.ts +35 -0
- package/types/mintplayer-ng-spark-identity-provider-core.d.ts +144 -0
- package/types/mintplayer-ng-spark-identity-provider-developers.d.ts +55 -0
- package/types/mintplayer-ng-spark-identity-provider-management.d.ts +33 -0
- package/types/mintplayer-ng-spark-identity-provider.d.ts +64 -0
- package/types/mintplayer-ng-spark-models.d.ts +28 -0
- package/types/mintplayer-ng-spark-po-detail.d.ts +8 -0
- package/types/mintplayer-ng-spark-po-form.d.ts +2 -0
- package/types/mintplayer-ng-spark-pwa.d.ts +89 -0
- package/types/mintplayer-ng-spark-query-list.d.ts +2 -0
- package/types/mintplayer-ng-spark-renderers.d.ts +47 -0
- package/types/mintplayer-ng-spark-retry-action-modal.d.ts +1 -0
- package/types/mintplayer-ng-spark-services.d.ts +37 -1
- package/types/mintplayer-ng-spark-shell.d.ts +2 -2
- package/types/mintplayer-ng-spark.d.ts +4 -4
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, inject, signal } from "@angular/core";
|
|
3
|
+
import * as i1 from "@angular/forms";
|
|
4
|
+
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
|
5
|
+
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
|
|
6
|
+
import { HttpErrorResponse } from "@angular/common/http";
|
|
7
|
+
import { Color } from "@mintplayer/ng-bootstrap";
|
|
8
|
+
import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
|
|
9
|
+
import { BsCardComponent, BsCardHeaderComponent } from "@mintplayer/ng-bootstrap/card";
|
|
10
|
+
import { BsFormComponent, BsFormControlDirective } from "@mintplayer/ng-bootstrap/form";
|
|
11
|
+
import { BsCheckboxComponent } from "@mintplayer/ng-bootstrap/checkbox";
|
|
12
|
+
import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
|
|
13
|
+
import { SPARK_AUTH_CONFIG, SPARK_AUTH_ROUTE_PATHS, passkeysSupported, sanitizeReturnUrl } from "@mintplayer/ng-spark/auth/models";
|
|
14
|
+
import { SparkAuthService, SparkAuthTranslationService } from "@mintplayer/ng-spark/auth/core";
|
|
15
|
+
import { SparkExternalLoginButtonsComponent } from "@mintplayer/ng-spark/auth/external-login";
|
|
16
|
+
import { TranslateKeyPipe } from "@mintplayer/ng-spark/auth/pipes";
|
|
17
|
+
var SparkLoginComponent = class SparkLoginComponent {
|
|
18
|
+
fb = inject(FormBuilder);
|
|
19
|
+
authService = inject(SparkAuthService);
|
|
20
|
+
router = inject(Router);
|
|
21
|
+
route = inject(ActivatedRoute);
|
|
22
|
+
config = inject(SPARK_AUTH_CONFIG);
|
|
23
|
+
translation = inject(SparkAuthTranslationService);
|
|
24
|
+
routePaths = inject(SPARK_AUTH_ROUTE_PATHS);
|
|
25
|
+
colors = Color;
|
|
26
|
+
loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
|
|
27
|
+
errorMessage = signal("", ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
|
|
28
|
+
passkeysAvailable = signal(false, ...ngDevMode ? [{ debugName: "passkeysAvailable" }] : /* istanbul ignore next */ []);
|
|
29
|
+
passkeyBusy = signal(false, ...ngDevMode ? [{ debugName: "passkeyBusy" }] : /* istanbul ignore next */ []);
|
|
30
|
+
signInIdentifiers = signal(["email", "userName"], ...ngDevMode ? [{ debugName: "signInIdentifiers" }] : /* istanbul ignore next */ []);
|
|
31
|
+
identifierField = computed(() => {
|
|
32
|
+
const kinds = this.signInIdentifiers();
|
|
33
|
+
const email = kinds.includes("email");
|
|
34
|
+
const userName = kinds.includes("userName");
|
|
35
|
+
if (email && !userName) return {
|
|
36
|
+
label: "auth.email",
|
|
37
|
+
type: "email",
|
|
38
|
+
autocomplete: "email"
|
|
39
|
+
};
|
|
40
|
+
if (userName && !email) return {
|
|
41
|
+
label: "auth.userName",
|
|
42
|
+
type: "text",
|
|
43
|
+
autocomplete: "username"
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
label: "auth.emailOrUserName",
|
|
47
|
+
type: "text",
|
|
48
|
+
autocomplete: "username"
|
|
49
|
+
};
|
|
50
|
+
}, ...ngDevMode ? [{ debugName: "identifierField" }] : /* istanbul ignore next */ []);
|
|
51
|
+
constructor() {
|
|
52
|
+
this.loadCapabilities();
|
|
53
|
+
}
|
|
54
|
+
async loadCapabilities() {
|
|
55
|
+
try {
|
|
56
|
+
const capabilities = await this.authService.capabilities();
|
|
57
|
+
this.passkeysAvailable.set(capabilities.passkeys === true && passkeysSupported());
|
|
58
|
+
if (capabilities.signInIdentifiers?.length) this.signInIdentifiers.set(capabilities.signInIdentifiers);
|
|
59
|
+
} catch {
|
|
60
|
+
this.passkeysAvailable.set(false);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async signInWithPasskey() {
|
|
64
|
+
this.errorMessage.set("");
|
|
65
|
+
this.passkeyBusy.set(true);
|
|
66
|
+
try {
|
|
67
|
+
const returnUrl = sanitizeReturnUrl(this.route.snapshot.queryParamMap.get("returnUrl"), this.config.defaultRedirectUrl);
|
|
68
|
+
const result = await this.authService.signInWithPasskey();
|
|
69
|
+
if (result.success) {
|
|
70
|
+
await this.router.navigateByUrl(returnUrl);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (result.error === "cancelled") return;
|
|
74
|
+
this.errorMessage.set(this.translation.t(result.error === "locked_out" ? "auth.lockedOut" : "auth.passkeyFailed"));
|
|
75
|
+
} finally {
|
|
76
|
+
this.passkeyBusy.set(false);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
form = this.fb.group({
|
|
80
|
+
email: ["", Validators.required],
|
|
81
|
+
password: ["", Validators.required],
|
|
82
|
+
rememberMe: [false]
|
|
83
|
+
});
|
|
84
|
+
async onSubmit() {
|
|
85
|
+
if (this.form.invalid) return;
|
|
86
|
+
this.loading.set(true);
|
|
87
|
+
this.errorMessage.set("");
|
|
88
|
+
const { email, password, rememberMe } = this.form.value;
|
|
89
|
+
try {
|
|
90
|
+
await this.authService.login(email, password, rememberMe === true);
|
|
91
|
+
const returnUrl = sanitizeReturnUrl(this.route.snapshot.queryParamMap.get("returnUrl"), this.config.defaultRedirectUrl);
|
|
92
|
+
this.router.navigateByUrl(returnUrl);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err instanceof HttpErrorResponse && err.status === 401 && err.error?.detail === "RequiresTwoFactor") {
|
|
95
|
+
const returnUrl = sanitizeReturnUrl(this.route.snapshot.queryParamMap.get("returnUrl"), this.config.defaultRedirectUrl);
|
|
96
|
+
this.router.navigate([this.routePaths.twoFactor], { queryParams: { returnUrl } });
|
|
97
|
+
} else this.errorMessage.set(this.translation.t("auth.invalidCredentials"));
|
|
98
|
+
} finally {
|
|
99
|
+
this.loading.set(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
103
|
+
minVersion: "12.0.0",
|
|
104
|
+
version: "22.2.0",
|
|
105
|
+
ngImport: i0,
|
|
106
|
+
type: SparkLoginComponent,
|
|
107
|
+
deps: [],
|
|
108
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
109
|
+
});
|
|
110
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
111
|
+
minVersion: "17.0.0",
|
|
112
|
+
version: "22.2.0",
|
|
113
|
+
type: SparkLoginComponent,
|
|
114
|
+
isStandalone: true,
|
|
115
|
+
selector: "spark-login",
|
|
116
|
+
ngImport: i0,
|
|
117
|
+
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.login' | 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 <!-- The same provider buttons as the sign-in landing page. They render nothing when the server\n reports no provider, and so does the divider, which only separates them from the form. -->\n <spark-external-login-buttons #externalLogin />\n @if (externalLogin.hasProviders()) {\n <div class=\"text-center text-muted my-2 spark-login-or\">{{ 'auth.or' | t }}</div>\n }\n\n <bs-form>\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">{{ identifierField().label | t }}</label>\n <input\n [type]=\"identifierField().type\"\n id=\"email\"\n formControlName=\"email\"\n [attr.autocomplete]=\"identifierField().autocomplete\"\n />\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=\"current-password\"\n />\n </div>\n\n <div class=\"mb-3\">\n <bs-checkbox [type]=\"'checkbox'\" formControlName=\"rememberMe\" [name]=\"'rememberMe'\">{{ 'auth.rememberMe' | t }}</bs-checkbox>\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.login' | t }}\n </button>\n </form>\n </bs-form>\n\n <!-- Passkeys are independent of withLocalLogin()/withExternalLogin(), so the button has to be\n reachable from whichever sign-in page the app mounted. Without it here, an app using only\n local login could enroll a passkey and never be able to use it. -->\n @if (passkeysAvailable()) {\n <div class=\"text-center text-muted my-2\">{{ 'auth.or' | t }}</div>\n <button type=\"button\" class=\"btn btn-outline-secondary w-100\"\n [disabled]=\"passkeyBusy()\" (click)=\"signInWithPasskey()\">\n <i class=\"bi bi-fingerprint me-2\"></i>{{ 'auth.signInWithPasskey' | t }}\n </button>\n }\n\n <!-- Registration is a separate opt-in feature (withRegistration()), so it may not be mounted\n even though this page is. A [routerLink] bound to undefined navigates to the current\n route instead of failing, which reads as a dead link. -->\n @if (routePaths.register) {\n <div class=\"mt-3 text-center\">\n <a [routerLink]=\"routePaths.register\">{{ 'auth.createAccount' | t }}</a>\n </div>\n }\n <div class=\"mt-2 text-center\">\n <a [routerLink]=\"routePaths.forgotPassword\">{{ 'auth.forgotPassword' | t }}</a>\n </div>\n </div>\n </bs-card>\n</div>\n",
|
|
118
|
+
dependencies: [
|
|
119
|
+
{
|
|
120
|
+
kind: "ngmodule",
|
|
121
|
+
type: ReactiveFormsModule
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
kind: "directive",
|
|
125
|
+
type: i1.ɵNgNoValidate,
|
|
126
|
+
selector: "form:not([ngNoForm]):not([ngNativeValidate])"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
kind: "directive",
|
|
130
|
+
type: i1.DefaultValueAccessor,
|
|
131
|
+
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]"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
kind: "directive",
|
|
135
|
+
type: i1.NgControlStatus,
|
|
136
|
+
selector: "[formControlName],[ngModel],[formControl]"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
kind: "directive",
|
|
140
|
+
type: i1.NgControlStatusGroup,
|
|
141
|
+
selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
kind: "directive",
|
|
145
|
+
type: i1.FormGroupDirective,
|
|
146
|
+
selector: "[formGroup]",
|
|
147
|
+
inputs: ["formGroup"],
|
|
148
|
+
outputs: ["ngSubmit"],
|
|
149
|
+
exportAs: ["ngForm"]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
kind: "directive",
|
|
153
|
+
type: i1.FormControlName,
|
|
154
|
+
selector: "[formControlName]",
|
|
155
|
+
inputs: [
|
|
156
|
+
"formControlName",
|
|
157
|
+
"disabled",
|
|
158
|
+
"ngModel"
|
|
159
|
+
],
|
|
160
|
+
outputs: ["ngModelChange"]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
kind: "directive",
|
|
164
|
+
type: RouterLink,
|
|
165
|
+
selector: "[routerLink]",
|
|
166
|
+
inputs: [
|
|
167
|
+
"target",
|
|
168
|
+
"queryParams",
|
|
169
|
+
"fragment",
|
|
170
|
+
"queryParamsHandling",
|
|
171
|
+
"state",
|
|
172
|
+
"info",
|
|
173
|
+
"relativeTo",
|
|
174
|
+
"preserveFragment",
|
|
175
|
+
"skipLocationChange",
|
|
176
|
+
"replaceUrl",
|
|
177
|
+
"browserUrl",
|
|
178
|
+
"routerLink"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
kind: "component",
|
|
183
|
+
type: BsAlertComponent,
|
|
184
|
+
selector: "bs-alert",
|
|
185
|
+
inputs: [
|
|
186
|
+
"type",
|
|
187
|
+
"announce",
|
|
188
|
+
"isVisible"
|
|
189
|
+
],
|
|
190
|
+
outputs: ["isVisibleChange", "afterOpenedOrClosed"]
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
kind: "component",
|
|
194
|
+
type: BsCardComponent,
|
|
195
|
+
selector: "bs-card",
|
|
196
|
+
inputs: ["color", "outline"]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
kind: "component",
|
|
200
|
+
type: BsCardHeaderComponent,
|
|
201
|
+
selector: "bs-card-header",
|
|
202
|
+
inputs: ["color", "navStyle"]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
kind: "component",
|
|
206
|
+
type: BsFormComponent,
|
|
207
|
+
selector: "bs-form",
|
|
208
|
+
inputs: ["action", "method"],
|
|
209
|
+
outputs: ["submitted"]
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
kind: "directive",
|
|
213
|
+
type: BsFormControlDirective,
|
|
214
|
+
selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
kind: "component",
|
|
218
|
+
type: BsCheckboxComponent,
|
|
219
|
+
selector: "bs-checkbox",
|
|
220
|
+
inputs: [
|
|
221
|
+
"type",
|
|
222
|
+
"isToggled",
|
|
223
|
+
"indeterminate",
|
|
224
|
+
"name",
|
|
225
|
+
"value",
|
|
226
|
+
"group"
|
|
227
|
+
],
|
|
228
|
+
outputs: ["isToggledChange", "indeterminateChange"]
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
kind: "component",
|
|
232
|
+
type: BsSpinnerComponent,
|
|
233
|
+
selector: "bs-spinner",
|
|
234
|
+
inputs: ["type", "color"]
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
kind: "component",
|
|
238
|
+
type: SparkExternalLoginButtonsComponent,
|
|
239
|
+
selector: "spark-external-login-buttons",
|
|
240
|
+
inputs: ["returnUrl", "providerTemplate"]
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
kind: "pipe",
|
|
244
|
+
type: TranslateKeyPipe,
|
|
245
|
+
name: "t"
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
252
|
+
minVersion: "12.0.0",
|
|
253
|
+
version: "22.2.0",
|
|
254
|
+
ngImport: i0,
|
|
255
|
+
type: SparkLoginComponent,
|
|
256
|
+
decorators: [{
|
|
257
|
+
type: Component,
|
|
258
|
+
args: [{
|
|
259
|
+
selector: "spark-login",
|
|
260
|
+
standalone: true,
|
|
261
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
262
|
+
imports: [
|
|
263
|
+
ReactiveFormsModule,
|
|
264
|
+
RouterLink,
|
|
265
|
+
BsAlertComponent,
|
|
266
|
+
BsCardComponent,
|
|
267
|
+
BsCardHeaderComponent,
|
|
268
|
+
BsFormComponent,
|
|
269
|
+
BsFormControlDirective,
|
|
270
|
+
BsCheckboxComponent,
|
|
271
|
+
BsSpinnerComponent,
|
|
272
|
+
SparkExternalLoginButtonsComponent,
|
|
273
|
+
TranslateKeyPipe
|
|
274
|
+
],
|
|
275
|
+
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.login' | 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 <!-- The same provider buttons as the sign-in landing page. They render nothing when the server\n reports no provider, and so does the divider, which only separates them from the form. -->\n <spark-external-login-buttons #externalLogin />\n @if (externalLogin.hasProviders()) {\n <div class=\"text-center text-muted my-2 spark-login-or\">{{ 'auth.or' | t }}</div>\n }\n\n <bs-form>\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">{{ identifierField().label | t }}</label>\n <input\n [type]=\"identifierField().type\"\n id=\"email\"\n formControlName=\"email\"\n [attr.autocomplete]=\"identifierField().autocomplete\"\n />\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=\"current-password\"\n />\n </div>\n\n <div class=\"mb-3\">\n <bs-checkbox [type]=\"'checkbox'\" formControlName=\"rememberMe\" [name]=\"'rememberMe'\">{{ 'auth.rememberMe' | t }}</bs-checkbox>\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.login' | t }}\n </button>\n </form>\n </bs-form>\n\n <!-- Passkeys are independent of withLocalLogin()/withExternalLogin(), so the button has to be\n reachable from whichever sign-in page the app mounted. Without it here, an app using only\n local login could enroll a passkey and never be able to use it. -->\n @if (passkeysAvailable()) {\n <div class=\"text-center text-muted my-2\">{{ 'auth.or' | t }}</div>\n <button type=\"button\" class=\"btn btn-outline-secondary w-100\"\n [disabled]=\"passkeyBusy()\" (click)=\"signInWithPasskey()\">\n <i class=\"bi bi-fingerprint me-2\"></i>{{ 'auth.signInWithPasskey' | t }}\n </button>\n }\n\n <!-- Registration is a separate opt-in feature (withRegistration()), so it may not be mounted\n even though this page is. A [routerLink] bound to undefined navigates to the current\n route instead of failing, which reads as a dead link. -->\n @if (routePaths.register) {\n <div class=\"mt-3 text-center\">\n <a [routerLink]=\"routePaths.register\">{{ 'auth.createAccount' | t }}</a>\n </div>\n }\n <div class=\"mt-2 text-center\">\n <a [routerLink]=\"routePaths.forgotPassword\">{{ 'auth.forgotPassword' | t }}</a>\n </div>\n </div>\n </bs-card>\n</div>\n"
|
|
276
|
+
}]
|
|
277
|
+
}],
|
|
278
|
+
ctorParameters: () => []
|
|
279
|
+
});
|
|
280
|
+
export { SparkLoginComponent };
|
|
281
|
+
|
|
282
|
+
//# sourceMappingURL=mintplayer-ng-spark-auth-login.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-auth-login.mjs","names":[],"sources":["../../auth/login/src/spark-login.component.ts","../../auth/login/src/spark-login.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';\nimport { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';\nimport { RouterLink, ActivatedRoute, 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 { BsCheckboxComponent } from '@mintplayer/ng-bootstrap/checkbox';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { SPARK_AUTH_CONFIG, SPARK_AUTH_ROUTE_PATHS, sanitizeReturnUrl, passkeysSupported, SparkSignInIdentifier } from '@mintplayer/ng-spark/auth/models';\nimport { SparkAuthService, SparkAuthTranslationService } from '@mintplayer/ng-spark/auth/core';\nimport { SparkExternalLoginButtonsComponent } from '@mintplayer/ng-spark/auth/external-login';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/auth/pipes';\n\n@Component({\n selector: 'spark-login',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [ReactiveFormsModule, RouterLink, BsAlertComponent, BsCardComponent, BsCardHeaderComponent, BsFormComponent, BsFormControlDirective, BsCheckboxComponent, BsSpinnerComponent, SparkExternalLoginButtonsComponent, TranslateKeyPipe],\n templateUrl: './spark-login.component.html',\n})\nexport class SparkLoginComponent {\n private readonly fb = inject(FormBuilder);\n private readonly authService = inject(SparkAuthService);\n private readonly router = inject(Router);\n private readonly route = inject(ActivatedRoute);\n private readonly config = inject(SPARK_AUTH_CONFIG);\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 /**\n * The passkey button belongs here as well as on the sign-in landing page.\n *\n * ⚠️ Without it an application using `withLocalLogin()` can *enroll* a passkey and then have no way\n * to use it, because the landing page it would otherwise appear on is only mounted by\n * `withExternalLogin()`. Passkeys are independent of both features, so the button has to be\n * reachable from whichever sign-in page the application actually mounted.\n */\n readonly passkeysAvailable = signal(false);\n readonly passkeyBusy = signal(false);\n\n /**\n * What the server's `SignInIdentifiers` accepts, which shapes the identifier field's label, input\n * type and autocomplete. Both until the capabilities arrive, and when the server does not say.\n */\n private readonly signInIdentifiers = signal<SparkSignInIdentifier[]>(['email', 'userName']);\n readonly identifierField = computed(() => {\n const kinds = this.signInIdentifiers();\n const email = kinds.includes('email');\n const userName = kinds.includes('userName');\n if (email && !userName) return { label: 'auth.email', type: 'email', autocomplete: 'email' };\n if (userName && !email) return { label: 'auth.userName', type: 'text', autocomplete: 'username' };\n return { label: 'auth.emailOrUserName', type: 'text', autocomplete: 'username' };\n });\n\n constructor() {\n void this.loadCapabilities();\n }\n\n private async loadCapabilities(): Promise<void> {\n try {\n // <spark-external-login-buttons> asks at the same moment; the service makes that one request.\n const capabilities = await this.authService.capabilities();\n this.passkeysAvailable.set(capabilities.passkeys === true && passkeysSupported());\n if (capabilities.signInIdentifiers?.length) this.signInIdentifiers.set(capabilities.signInIdentifiers);\n } catch {\n // A capability lookup that fails must not break password sign-in, which is this page's job.\n this.passkeysAvailable.set(false);\n }\n }\n\n /** Same shape as {@link onSubmit}'s success path — sign in, then leave the page. */\n async signInWithPasskey(): Promise<void> {\n this.errorMessage.set('');\n this.passkeyBusy.set(true);\n try {\n const returnUrl = sanitizeReturnUrl(\n this.route.snapshot.queryParamMap.get('returnUrl'), this.config.defaultRedirectUrl);\n const result = await this.authService.signInWithPasskey();\n\n if (result.success) {\n await this.router.navigateByUrl(returnUrl);\n return;\n }\n\n // Dismissing the prompt is \"not now\", not a failure.\n if (result.error === 'cancelled') return;\n\n this.errorMessage.set(this.translation.t(\n result.error === 'locked_out' ? 'auth.lockedOut' : 'auth.passkeyFailed'));\n } finally {\n this.passkeyBusy.set(false);\n }\n }\n\n readonly form = this.fb.group({\n email: ['', Validators.required],\n password: ['', Validators.required],\n rememberMe: [false],\n });\n\n async onSubmit(): Promise<void> {\n if (this.form.invalid) return;\n\n this.loading.set(true);\n this.errorMessage.set('');\n\n const { email, password, rememberMe } = this.form.value;\n\n try {\n await this.authService.login(email!, password!, rememberMe === true);\n // R2-H9: validate returnUrl against the local-path rule. navigateByUrl\n // accepts some external shapes (protocol-relative '//attacker'); reject\n // and fall back to the default to close the open-redirect.\n const returnUrl = sanitizeReturnUrl(\n this.route.snapshot.queryParamMap.get('returnUrl'),\n this.config.defaultRedirectUrl);\n this.router.navigateByUrl(returnUrl);\n } catch (err: any) {\n if (err instanceof HttpErrorResponse && err.status === 401 && err.error?.detail === 'RequiresTwoFactor') {\n // Forward the same sanitized returnUrl to the 2FA step so the user\n // lands on the intended in-app page after completing the challenge.\n const returnUrl = sanitizeReturnUrl(\n this.route.snapshot.queryParamMap.get('returnUrl'),\n this.config.defaultRedirectUrl);\n this.router.navigate([this.routePaths.twoFactor], {\n queryParams: { returnUrl },\n });\n } else {\n this.errorMessage.set(this.translation.t('auth.invalidCredentials'));\n }\n } finally {\n this.loading.set(false);\n }\n }\n}\n\nexport default SparkLoginComponent;\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.login' | 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 <!-- The same provider buttons as the sign-in landing page. They render nothing when the server\n reports no provider, and so does the divider, which only separates them from the form. -->\n <spark-external-login-buttons #externalLogin />\n @if (externalLogin.hasProviders()) {\n <div class=\"text-center text-muted my-2 spark-login-or\">{{ 'auth.or' | t }}</div>\n }\n\n <bs-form>\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">{{ identifierField().label | t }}</label>\n <input\n [type]=\"identifierField().type\"\n id=\"email\"\n formControlName=\"email\"\n [attr.autocomplete]=\"identifierField().autocomplete\"\n />\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=\"current-password\"\n />\n </div>\n\n <div class=\"mb-3\">\n <bs-checkbox [type]=\"'checkbox'\" formControlName=\"rememberMe\" [name]=\"'rememberMe'\">{{ 'auth.rememberMe' | t }}</bs-checkbox>\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.login' | t }}\n </button>\n </form>\n </bs-form>\n\n <!-- Passkeys are independent of withLocalLogin()/withExternalLogin(), so the button has to be\n reachable from whichever sign-in page the app mounted. Without it here, an app using only\n local login could enroll a passkey and never be able to use it. -->\n @if (passkeysAvailable()) {\n <div class=\"text-center text-muted my-2\">{{ 'auth.or' | t }}</div>\n <button type=\"button\" class=\"btn btn-outline-secondary w-100\"\n [disabled]=\"passkeyBusy()\" (click)=\"signInWithPasskey()\">\n <i class=\"bi bi-fingerprint me-2\"></i>{{ 'auth.signInWithPasskey' | t }}\n </button>\n }\n\n <!-- Registration is a separate opt-in feature (withRegistration()), so it may not be mounted\n even though this page is. A [routerLink] bound to undefined navigates to the current\n route instead of failing, which reads as a dead link. -->\n @if (routePaths.register) {\n <div class=\"mt-3 text-center\">\n <a [routerLink]=\"routePaths.register\">{{ 'auth.createAccount' | t }}</a>\n </div>\n }\n <div class=\"mt-2 text-center\">\n <a [routerLink]=\"routePaths.forgotPassword\">{{ 'auth.forgotPassword' | t }}</a>\n </div>\n </div>\n </bs-card>\n</div>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { InjectionToken, isDevMode } from "@angular/core";
|
|
2
|
+
const SPARK_AUTH_CONFIG = new InjectionToken("SPARK_AUTH_CONFIG");
|
|
3
|
+
const defaultSparkAuthConfig = {
|
|
4
|
+
apiBasePath: "/spark/auth",
|
|
5
|
+
defaultRedirectUrl: "/",
|
|
6
|
+
loginUrl: "/login",
|
|
7
|
+
externalLoginMode: "auto"
|
|
8
|
+
};
|
|
9
|
+
const SPARK_ACCOUNT_OVERVIEW_LINKS = new InjectionToken("SPARK_ACCOUNT_OVERVIEW_LINKS");
|
|
10
|
+
const SPARK_AUTH_ROUTE_PATHS = new InjectionToken("SPARK_AUTH_ROUTE_PATHS");
|
|
11
|
+
function findSparkAuthRoutePaths(routes) {
|
|
12
|
+
for (const route of routes ?? []) {
|
|
13
|
+
for (const provider of route.providers ?? []) {
|
|
14
|
+
const p = provider;
|
|
15
|
+
if (p?.provide === SPARK_AUTH_ROUTE_PATHS && p.useValue) return p.useValue;
|
|
16
|
+
}
|
|
17
|
+
const nested = findSparkAuthRoutePaths(route.children);
|
|
18
|
+
if (nested) return nested;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const SPARK_EXTERNAL_PROVIDERS = new InjectionToken("SPARK_EXTERNAL_PROVIDERS");
|
|
23
|
+
function isSafeReturnUrl(value) {
|
|
24
|
+
if (!value) return false;
|
|
25
|
+
if (/[\r\n]/.test(value)) return false;
|
|
26
|
+
if (!value.startsWith("/")) return false;
|
|
27
|
+
if (value.length >= 2 && (value[1] === "/" || value[1] === "\\")) return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
function sanitizeReturnUrl(value, defaultUrl) {
|
|
31
|
+
return isSafeReturnUrl(value) ? value : defaultUrl;
|
|
32
|
+
}
|
|
33
|
+
function sparkIsStandaloneDisplay() {
|
|
34
|
+
if (typeof window === "undefined") return false;
|
|
35
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(display-mode: standalone)")?.matches === true || window.navigator?.standalone === true;
|
|
36
|
+
}
|
|
37
|
+
function resolveSparkExternalLoginMode(setting) {
|
|
38
|
+
if (setting === "popup" || setting === "redirect") return setting;
|
|
39
|
+
return sparkIsStandaloneDisplay() ? "redirect" : "popup";
|
|
40
|
+
}
|
|
41
|
+
const SPARK_EXTERNAL_LOGIN_ERRORS = [
|
|
42
|
+
"no_login_info",
|
|
43
|
+
"email_not_verified",
|
|
44
|
+
"account_creation_failed",
|
|
45
|
+
"email_already_registered",
|
|
46
|
+
"sign_in_to_link",
|
|
47
|
+
"link_confirmation_sent",
|
|
48
|
+
"login_already_associated",
|
|
49
|
+
"link_failed",
|
|
50
|
+
"remote_failure",
|
|
51
|
+
"invalid_nonce",
|
|
52
|
+
"popup_blocked",
|
|
53
|
+
"popup_closed"
|
|
54
|
+
];
|
|
55
|
+
const SPARK_EXTERNAL_LOGIN_MESSAGE_TYPE = "spark:external-login";
|
|
56
|
+
const SPARK_EXTERNAL_LOGIN_ACK_TYPE = "spark:external-login-ack";
|
|
57
|
+
const SPARK_EXTERNAL_LOGIN_CHANNEL = "spark:external-login";
|
|
58
|
+
const SPARK_EXTERNAL_LOGIN_QUERY_PARAM = "sparkExternalLogin";
|
|
59
|
+
const SPARK_EXTERNAL_LOGIN_TIMEOUT_MS = 6e5;
|
|
60
|
+
function sparkExternalLoginStorageKey(nonce) {
|
|
61
|
+
return `spark:external-login:${nonce}`;
|
|
62
|
+
}
|
|
63
|
+
function sparkExternalLoginDoneKey(nonce) {
|
|
64
|
+
return `spark:external-login-done:${nonce}`;
|
|
65
|
+
}
|
|
66
|
+
function sparkExternalLoginNonce() {
|
|
67
|
+
const bytes = /* @__PURE__ */ new Uint8Array(24);
|
|
68
|
+
crypto.getRandomValues(bytes);
|
|
69
|
+
let binary = "";
|
|
70
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
71
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_");
|
|
72
|
+
}
|
|
73
|
+
function sparkPasskeyError(error) {
|
|
74
|
+
if (error instanceof DOMException) {
|
|
75
|
+
if (error.name === "AbortError") return "cancelled";
|
|
76
|
+
if (error.name === "NotAllowedError") return "no_credential";
|
|
77
|
+
return "failed";
|
|
78
|
+
}
|
|
79
|
+
if (error?.error?.error === "locked_out") return "locked_out";
|
|
80
|
+
return "failed";
|
|
81
|
+
}
|
|
82
|
+
function passkeysSupported() {
|
|
83
|
+
return typeof window !== "undefined" && window.isSecureContext === true && typeof PublicKeyCredential !== "undefined" && typeof navigator?.credentials?.create === "function" && typeof navigator?.credentials?.get === "function" && typeof PublicKeyCredential.parseCreationOptionsFromJSON === "function" && typeof PublicKeyCredential.parseRequestOptionsFromJSON === "function";
|
|
84
|
+
}
|
|
85
|
+
function resolveSignInUrl(config, router) {
|
|
86
|
+
if (isDevMode() && !warned && !routeExists(router, config.loginUrl)) {
|
|
87
|
+
warned = true;
|
|
88
|
+
console.warn(`[ng-spark-auth] loginUrl is "${config.loginUrl}", but no route matches it, so redirecting an unauthenticated user will land on a blank page. If this app runs with local credentials disabled, point loginUrl at its sign-in route (for example provideSparkAuth({ loginUrl: '/sign-in' })).`);
|
|
89
|
+
}
|
|
90
|
+
return config.loginUrl;
|
|
91
|
+
}
|
|
92
|
+
let warned = false;
|
|
93
|
+
function routeExists(router, url) {
|
|
94
|
+
const target = url.replace(/^\/+/, "").split("?")[0].split("#")[0];
|
|
95
|
+
return matches(router.config, target);
|
|
96
|
+
}
|
|
97
|
+
function matches(routes, target, prefix = "") {
|
|
98
|
+
return routes.some((route) => {
|
|
99
|
+
const path = [prefix, route.path ?? ""].filter(Boolean).join("/");
|
|
100
|
+
if (path === target) return true;
|
|
101
|
+
return route.children ? matches(route.children, target, path) : false;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const SPARK_ACCOUNT_PROFILE_FIELDS = new InjectionToken("SPARK_ACCOUNT_PROFILE_FIELDS");
|
|
105
|
+
function provideSparkAccountProfileFields(...fields) {
|
|
106
|
+
return fields.map((field) => ({
|
|
107
|
+
provide: SPARK_ACCOUNT_PROFILE_FIELDS,
|
|
108
|
+
useValue: field,
|
|
109
|
+
multi: true
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
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, sparkExternalLoginDoneKey, sparkExternalLoginNonce, sparkExternalLoginStorageKey, sparkIsStandaloneDisplay, sparkPasskeyError };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=mintplayer-ng-spark-auth-models.mjs.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { Pipe, inject } from "@angular/core";
|
|
3
|
+
import { SparkAuthTranslationService } from "@mintplayer/ng-spark/auth/core";
|
|
4
|
+
var TranslateKeyPipe = class TranslateKeyPipe {
|
|
5
|
+
translation = inject(SparkAuthTranslationService);
|
|
6
|
+
transform(key) {
|
|
7
|
+
return this.translation.t(key);
|
|
8
|
+
}
|
|
9
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
10
|
+
minVersion: "12.0.0",
|
|
11
|
+
version: "22.2.0",
|
|
12
|
+
ngImport: i0,
|
|
13
|
+
type: TranslateKeyPipe,
|
|
14
|
+
deps: [],
|
|
15
|
+
target: i0.ɵɵFactoryTarget.Pipe
|
|
16
|
+
});
|
|
17
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({
|
|
18
|
+
minVersion: "14.0.0",
|
|
19
|
+
version: "22.2.0",
|
|
20
|
+
ngImport: i0,
|
|
21
|
+
type: TranslateKeyPipe,
|
|
22
|
+
isStandalone: true,
|
|
23
|
+
name: "t",
|
|
24
|
+
pure: false
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
28
|
+
minVersion: "12.0.0",
|
|
29
|
+
version: "22.2.0",
|
|
30
|
+
ngImport: i0,
|
|
31
|
+
type: TranslateKeyPipe,
|
|
32
|
+
decorators: [{
|
|
33
|
+
type: Pipe,
|
|
34
|
+
args: [{
|
|
35
|
+
name: "t",
|
|
36
|
+
pure: false,
|
|
37
|
+
standalone: true
|
|
38
|
+
}]
|
|
39
|
+
}]
|
|
40
|
+
});
|
|
41
|
+
export { TranslateKeyPipe };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=mintplayer-ng-spark-auth-pipes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-auth-pipes.mjs","names":[],"sources":["../../auth/pipes/src/translate-key.pipe.ts"],"sourcesContent":["import { Pipe, PipeTransform, inject } from '@angular/core';\nimport { SparkAuthTranslationService } from '@mintplayer/ng-spark/auth/core';\n\n@Pipe({ name: 't', pure: false, standalone: true })\nexport class TranslateKeyPipe implements PipeTransform {\n private readonly translation = inject(SparkAuthTranslationService);\n\n transform(key: string): string {\n return this.translation.t(key);\n }\n}\n"],"mappings":";;;AAIA,IAAa,mBAAb,MAAa,iBAAgB;CACV,cAAc,OAAO,2BAA2B;CAEjE,UAAU,KAAW;EACnB,OAAO,KAAK,YAAY,EAAE,GAAG;CAC/B;CALW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAgB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAhB,OAAA,QAAA,GAAA,gBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAgB,cAAA;EAAA,MAAA;EAAA,MAAA;CAAA,CAAA;;AAAhB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAgB,YAAA,CAAA;EAD5B,MAAA;EAAK,MAAA,CAAA;GAAE,MAAM;GAAK,MAAM;GAAO,YAAY;EAAI,CAAE"}
|