@franzzemen/brokenstock-shell 0.5.0 → 0.6.1
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.
|
@@ -349,6 +349,18 @@ class AuthenticationService {
|
|
|
349
349
|
...(ngDevMode ? [{ debugName: "userSession" }] : /* istanbul ignore next */ []));
|
|
350
350
|
this.conflictSessions = signal(undefined, /* @ts-ignore */
|
|
351
351
|
...(ngDevMode ? [{ debugName: "conflictSessions" }] : /* istanbul ignore next */ []));
|
|
352
|
+
/**
|
|
353
|
+
* Whether this app lets you sign in ALONGSIDE the sessions listed in
|
|
354
|
+
* `conflictSessions` (multiple-devices D3).
|
|
355
|
+
*
|
|
356
|
+
* ⚠ READ FROM THE SERVER'S REFUSAL, NEVER DECIDED HERE. This component is
|
|
357
|
+
* shared by the trading app and both admin consoles, which have different
|
|
358
|
+
* rules. Deciding it locally would put the policy in two places, and the one
|
|
359
|
+
* a reader would find is not the one that enforces it — an admin screen would
|
|
360
|
+
* happily offer a button the server refuses.
|
|
361
|
+
*/
|
|
362
|
+
this.additionalSignInAllowed = signal(false, /* @ts-ignore */
|
|
363
|
+
...(ngDevMode ? [{ debugName: "additionalSignInAllowed" }] : /* istanbul ignore next */ []));
|
|
352
364
|
this.pendingCredentials = signal(undefined, /* @ts-ignore */
|
|
353
365
|
...(ngDevMode ? [{ debugName: "pendingCredentials" }] : /* istanbul ignore next */ []));
|
|
354
366
|
this.#refreshing = null;
|
|
@@ -439,6 +451,7 @@ class AuthenticationService {
|
|
|
439
451
|
}
|
|
440
452
|
clearConflict() {
|
|
441
453
|
this.conflictSessions.set(undefined);
|
|
454
|
+
this.additionalSignInAllowed.set(false);
|
|
442
455
|
this.pendingCredentials.set(undefined);
|
|
443
456
|
}
|
|
444
457
|
/**
|
|
@@ -477,7 +490,17 @@ class AuthenticationService {
|
|
|
477
490
|
},
|
|
478
491
|
});
|
|
479
492
|
}
|
|
480
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Signs in, and — on a second attempt — says what to do about the sessions
|
|
495
|
+
* already held (multiple-devices D2/D6).
|
|
496
|
+
*
|
|
497
|
+
* `force` and `additional` are different answers, not synonyms: `force` ends
|
|
498
|
+
* every session in this app, `additional` leaves them all running.
|
|
499
|
+
* `endSessionIds` ends the ones the user picked and leaves the rest. None of
|
|
500
|
+
* the three appears on a first attempt — they exist because the server
|
|
501
|
+
* refused once and said what was live.
|
|
502
|
+
*/
|
|
503
|
+
login(credentials, force = false, options = {}) {
|
|
481
504
|
const validation = validateCredentials(credentials);
|
|
482
505
|
if (validation !== true) {
|
|
483
506
|
return Promise.reject('Invalid credentials');
|
|
@@ -486,7 +509,15 @@ class AuthenticationService {
|
|
|
486
509
|
this.#thinkingService.startThinking();
|
|
487
510
|
return new Promise((resolve, reject) => {
|
|
488
511
|
this.authenticating.set(true);
|
|
489
|
-
|
|
512
|
+
// Fields are added only when they carry a decision. The route's body
|
|
513
|
+
// schema is strict, and sending `additional: false` on a first attempt
|
|
514
|
+
// would be a statement where silence is meant.
|
|
515
|
+
const body = {
|
|
516
|
+
...credentials,
|
|
517
|
+
...(force ? { force: true } : {}),
|
|
518
|
+
...(options.additional ? { additional: true } : {}),
|
|
519
|
+
...(options.endSessionIds?.length ? { endSessionIds: options.endSessionIds } : {}),
|
|
520
|
+
};
|
|
490
521
|
const subscription = this.#httpClient
|
|
491
522
|
.post(this.#authUrls.login, body, {
|
|
492
523
|
headers: {
|
|
@@ -527,6 +558,10 @@ class AuthenticationService {
|
|
|
527
558
|
try {
|
|
528
559
|
if (err.status === 409) {
|
|
529
560
|
this.conflictSessions.set(err.error?.activeSessions ?? []);
|
|
561
|
+
// Absent means false: an older back end that does not send this
|
|
562
|
+
// does not permit signing in alongside, so the screen must not
|
|
563
|
+
// offer it.
|
|
564
|
+
this.additionalSignInAllowed.set(err.error?.additionalSignInAllowed === true);
|
|
530
565
|
this.pendingCredentials.set(credentials);
|
|
531
566
|
reject('conflict');
|
|
532
567
|
}
|
|
@@ -3218,9 +3253,19 @@ class Login {
|
|
|
3218
3253
|
this.submitted = false;
|
|
3219
3254
|
this.hide = signal(true, /* @ts-ignore */
|
|
3220
3255
|
...(ngDevMode ? [{ debugName: "hide" }] : /* istanbul ignore next */ []));
|
|
3221
|
-
this.takeover = signal(true, /* @ts-ignore */
|
|
3222
|
-
...(ngDevMode ? [{ debugName: "takeover" }] : /* istanbul ignore next */ []));
|
|
3223
3256
|
this.errors = [];
|
|
3257
|
+
/**
|
|
3258
|
+
* Session identifiers the user has ticked for ending (multiple-devices D7).
|
|
3259
|
+
*
|
|
3260
|
+
* ⚠ THE TAKEOVER CHECKBOX THAT USED TO LIVE ON THIS FORM IS GONE, and its
|
|
3261
|
+
* removal is the point rather than a tidy-up. It defaulted to ON and sat on
|
|
3262
|
+
* the FIRST form, before anything was known about what was live — so once a
|
|
3263
|
+
* user deliberately holds sessions on two machines, one absent-minded submit
|
|
3264
|
+
* ended the session on the machine in the other room. The choice belongs
|
|
3265
|
+
* here, on the screen that lists what is actually running.
|
|
3266
|
+
*/
|
|
3267
|
+
this.selected = signal(new Set(), /* @ts-ignore */
|
|
3268
|
+
...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
3224
3269
|
this.USERNAME_REGEX = USERNAME_REGEX;
|
|
3225
3270
|
this.PASSWORD_REGEX = PASSWORD_REGEX;
|
|
3226
3271
|
this.appName = inject(SHELL_APP_NAME, { optional: true }) ?? 'Application';
|
|
@@ -3264,7 +3309,10 @@ class Login {
|
|
|
3264
3309
|
this.submitted = true;
|
|
3265
3310
|
const result = validateCredentials(this.model);
|
|
3266
3311
|
if (result === true) {
|
|
3267
|
-
|
|
3312
|
+
// A first attempt makes no claim about other sessions. If any exist the
|
|
3313
|
+
// server refuses, and the conflict panel below is where the decision is
|
|
3314
|
+
// made — with the sessions on screen.
|
|
3315
|
+
this.#doLogin(false);
|
|
3268
3316
|
}
|
|
3269
3317
|
else {
|
|
3270
3318
|
for (let ndx = 0; ndx < result.length; ndx++) {
|
|
@@ -3276,17 +3324,66 @@ class Login {
|
|
|
3276
3324
|
this.submitted = false;
|
|
3277
3325
|
}
|
|
3278
3326
|
}
|
|
3327
|
+
/** Whether individual sessions can be ticked: only when the server named them. */
|
|
3328
|
+
canEndIndividually() {
|
|
3329
|
+
return (this.authenticationService.conflictSessions() ?? []).some(s => !!s.sessionId);
|
|
3330
|
+
}
|
|
3331
|
+
isSelected(session) {
|
|
3332
|
+
return !!session.sessionId && this.selected().has(session.sessionId);
|
|
3333
|
+
}
|
|
3334
|
+
toggleSelected(session, checked) {
|
|
3335
|
+
if (!session.sessionId)
|
|
3336
|
+
return;
|
|
3337
|
+
const next = new Set(this.selected());
|
|
3338
|
+
if (checked)
|
|
3339
|
+
next.add(session.sessionId);
|
|
3340
|
+
else
|
|
3341
|
+
next.delete(session.sessionId);
|
|
3342
|
+
this.selected.set(next);
|
|
3343
|
+
}
|
|
3344
|
+
selectedCount() {
|
|
3345
|
+
return this.selected().size;
|
|
3346
|
+
}
|
|
3347
|
+
/**
|
|
3348
|
+
* What to call a session on screen.
|
|
3349
|
+
*
|
|
3350
|
+
* Falls back to the application name, which is what this screen showed before
|
|
3351
|
+
* devices were recorded, and then to something plainly non-committal. It never
|
|
3352
|
+
* invents a device: a session that predates the column genuinely has none, and
|
|
3353
|
+
* guessing would make two machines look like one.
|
|
3354
|
+
*/
|
|
3355
|
+
deviceLabel(session) {
|
|
3356
|
+
return session.deviceDescription ?? session.appContext ?? 'Another session';
|
|
3357
|
+
}
|
|
3279
3358
|
onTakeOver() {
|
|
3280
3359
|
this.errors = [];
|
|
3281
|
-
this
|
|
3360
|
+
this.selected.set(new Set());
|
|
3361
|
+
// ⚠ This used to hand the terminal `login --force`, with no username or
|
|
3362
|
+
// password on it — the terminal's login command reads its credentials from
|
|
3363
|
+
// the words typed after it, found none, and answered "Invalid syntax". The
|
|
3364
|
+
// credentials the user already typed are on this form; sign in with them,
|
|
3365
|
+
// the same way the other two buttons here do.
|
|
3366
|
+
this.#doLogin(true);
|
|
3367
|
+
}
|
|
3368
|
+
onSignInAsWell() {
|
|
3369
|
+
this.errors = [];
|
|
3370
|
+
this.selected.set(new Set());
|
|
3371
|
+
this.#doLogin(false, { additional: true });
|
|
3372
|
+
}
|
|
3373
|
+
onEndSelected() {
|
|
3374
|
+
this.errors = [];
|
|
3375
|
+
const ids = [...this.selected()];
|
|
3376
|
+
this.selected.set(new Set());
|
|
3377
|
+
this.#doLogin(false, { endSessionIds: ids });
|
|
3282
3378
|
}
|
|
3283
3379
|
onCancelConflict() {
|
|
3284
3380
|
this.authenticationService.clearConflict();
|
|
3381
|
+
this.selected.set(new Set());
|
|
3285
3382
|
this.submitted = false;
|
|
3286
3383
|
}
|
|
3287
|
-
#doLogin(force) {
|
|
3384
|
+
#doLogin(force, choice = {}) {
|
|
3288
3385
|
this.authenticationService
|
|
3289
|
-
.login(this.model, force)
|
|
3386
|
+
.login(this.model, force, choice)
|
|
3290
3387
|
.then(() => {
|
|
3291
3388
|
this.#commandService.echoCommand(`login ${this.model.username}`);
|
|
3292
3389
|
this.router.navigateByUrl(this.#resolvePostLoginUrl()).then(() => {
|
|
@@ -3301,7 +3398,7 @@ class Login {
|
|
|
3301
3398
|
});
|
|
3302
3399
|
}
|
|
3303
3400
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: Login, deps: [{ token: AuthenticationService }, { token: i2$1.Router }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3304
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: Login, isStandalone: true, selector: "shell-login", ngImport: i0, template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <
|
|
3401
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: Login, isStandalone: true, selector: "shell-login", ngImport: i0, template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n Login\n </button>\n </form>\n @if (errors.length > 0) {\n <div class=\"login-errors\">\n @for (error of errors; track error.ndx) {\n <div class=\"login-error text-loss\">{{ error.message }}</div>\n }\n </div>\n }\n @if (authenticationService.conflictSessions(); as sessions) {\n <div class=\"login-conflict\">\n <p class=\"text-secondary-info\">\n You are already signed in {{ sessions.length === 1 ? 'somewhere else' : 'on other devices' }}:\n </p>\n @for (session of sessions; track session.sessionId ?? $index) {\n <div class=\"login-conflict-session\">\n @if (canEndIndividually()) {\n <mat-checkbox\n [checked]=\"isSelected(session)\"\n [disabled]=\"!session.sessionId\"\n (change)=\"toggleSelected(session, $event.checked)\"\n [attr.aria-label]=\"'End ' + deviceLabel(session)\"\n ></mat-checkbox>\n }\n <span class=\"text-secondary-info\">\n <strong>{{ deviceLabel(session) }}</strong>\n <!-- signedInAtEpoch is when the password was typed. startEpoch is\n when the row was written, which renewal resets \u2014 so it is shown\n only when there is nothing better, and labelled for what it is. -->\n @if (session.signedInAtEpoch) {\n \u2014 since {{ session.signedInAtEpoch | date:'short' }}\n } @else {\n \u2014 last active {{ session.startEpoch | date:'short' }}\n }\n </span>\n </div>\n }\n <div class=\"login-conflict-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"onCancelConflict()\">Cancel</button>\n @if (selectedCount() > 0) {\n <button mat-stroked-button type=\"button\" (click)=\"onEndSelected()\">\n End {{ selectedCount() === 1 ? 'that one' : selectedCount() + ' of them' }} and sign in\n </button>\n }\n <button mat-stroked-button type=\"button\" (click)=\"onTakeOver()\">\n {{ sessions.length === 1 ? 'End it and sign in' : 'End them all and sign in' }}\n </button>\n @if (authenticationService.additionalSignInAllowed()) {\n <button mat-flat-button type=\"button\" (click)=\"onSignInAsWell()\">Sign in here as well</button>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".login-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center}.login-header{margin-bottom:32px}.login-header h2{font-size:1.5rem;font-weight:300;margin-bottom:8px}.login-form{display:flex;flex-direction:column;align-items:center;gap:8px;width:320px}.login-field{width:100%;text-align:left}.login-takeover{align-self:flex-start;margin-top:4px}.login-button{margin-top:8px;width:100%}.login-errors{margin-top:16px;max-width:320px}.login-conflict{margin-top:16px;max-width:320px;padding:12px 16px;border-radius:4px;background-color:var(--color-surface-light);font-size:.85rem;text-align:left}.login-conflict p{margin:0 0 8px}.login-conflict-session{display:flex;align-items:center;gap:8px;margin:4px 0}.login-conflict-actions{flex-wrap:wrap;display:flex;gap:8px;margin-top:12px;justify-content:flex-end}.login-error{padding:8px 12px;margin-bottom:4px;font-size:.85rem;border-radius:4px;background-color:var(--color-surface-light)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.DefaultValueAccessor, 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]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i5.CdkAutofill, selector: "[cdkAutofill]", outputs: ["cdkAutofill"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i7.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RegexValidatorDirective, selector: "[regexValidator]", inputs: ["regex"] }, { kind: "ngmodule", type: TextFieldModule }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
3305
3402
|
}
|
|
3306
3403
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: Login, decorators: [{
|
|
3307
3404
|
type: Component,
|
|
@@ -3317,7 +3414,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
3317
3414
|
RegexValidatorDirective,
|
|
3318
3415
|
TextFieldModule,
|
|
3319
3416
|
DatePipe,
|
|
3320
|
-
], template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <
|
|
3417
|
+
], template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n Login\n </button>\n </form>\n @if (errors.length > 0) {\n <div class=\"login-errors\">\n @for (error of errors; track error.ndx) {\n <div class=\"login-error text-loss\">{{ error.message }}</div>\n }\n </div>\n }\n @if (authenticationService.conflictSessions(); as sessions) {\n <div class=\"login-conflict\">\n <p class=\"text-secondary-info\">\n You are already signed in {{ sessions.length === 1 ? 'somewhere else' : 'on other devices' }}:\n </p>\n @for (session of sessions; track session.sessionId ?? $index) {\n <div class=\"login-conflict-session\">\n @if (canEndIndividually()) {\n <mat-checkbox\n [checked]=\"isSelected(session)\"\n [disabled]=\"!session.sessionId\"\n (change)=\"toggleSelected(session, $event.checked)\"\n [attr.aria-label]=\"'End ' + deviceLabel(session)\"\n ></mat-checkbox>\n }\n <span class=\"text-secondary-info\">\n <strong>{{ deviceLabel(session) }}</strong>\n <!-- signedInAtEpoch is when the password was typed. startEpoch is\n when the row was written, which renewal resets \u2014 so it is shown\n only when there is nothing better, and labelled for what it is. -->\n @if (session.signedInAtEpoch) {\n \u2014 since {{ session.signedInAtEpoch | date:'short' }}\n } @else {\n \u2014 last active {{ session.startEpoch | date:'short' }}\n }\n </span>\n </div>\n }\n <div class=\"login-conflict-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"onCancelConflict()\">Cancel</button>\n @if (selectedCount() > 0) {\n <button mat-stroked-button type=\"button\" (click)=\"onEndSelected()\">\n End {{ selectedCount() === 1 ? 'that one' : selectedCount() + ' of them' }} and sign in\n </button>\n }\n <button mat-stroked-button type=\"button\" (click)=\"onTakeOver()\">\n {{ sessions.length === 1 ? 'End it and sign in' : 'End them all and sign in' }}\n </button>\n @if (authenticationService.additionalSignInAllowed()) {\n <button mat-flat-button type=\"button\" (click)=\"onSignInAsWell()\">Sign in here as well</button>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".login-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center}.login-header{margin-bottom:32px}.login-header h2{font-size:1.5rem;font-weight:300;margin-bottom:8px}.login-form{display:flex;flex-direction:column;align-items:center;gap:8px;width:320px}.login-field{width:100%;text-align:left}.login-takeover{align-self:flex-start;margin-top:4px}.login-button{margin-top:8px;width:100%}.login-errors{margin-top:16px;max-width:320px}.login-conflict{margin-top:16px;max-width:320px;padding:12px 16px;border-radius:4px;background-color:var(--color-surface-light);font-size:.85rem;text-align:left}.login-conflict p{margin:0 0 8px}.login-conflict-session{display:flex;align-items:center;gap:8px;margin:4px 0}.login-conflict-actions{flex-wrap:wrap;display:flex;gap:8px;margin-top:12px;justify-content:flex-end}.login-error{padding:8px 12px;margin-bottom:4px;font-size:.85rem;border-radius:4px;background-color:var(--color-surface-light)}\n"] }]
|
|
3321
3418
|
}], ctorParameters: () => [{ type: AuthenticationService }, { type: i2$1.Router }] });
|
|
3322
3419
|
|
|
3323
3420
|
class ConfirmDialog {
|