@atlasauth/angular 0.8.0 → 0.10.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/CHANGELOG.md +23 -0
- package/dist/esm2022/lib/appearance.mjs +1 -1
- package/dist/esm2022/lib/atlas.service.mjs +40 -3
- package/dist/esm2022/lib/atlas.ui.mjs +7 -1
- package/dist/esm2022/lib/components/attempt-flow.component.mjs +419 -11
- package/dist/esm2022/lib/components/flow-controller.mjs +132 -2
- package/dist/esm2022/lib/components/impersonation-banner.component.mjs +75 -0
- package/dist/esm2022/lib/components/organization-audit-log.component.mjs +115 -0
- package/dist/esm2022/lib/components/organization-audit-log.mjs +40 -0
- package/dist/esm2022/lib/components/organization-profile.component.mjs +602 -27
- package/dist/esm2022/lib/components/policy-host.mjs +254 -0
- package/dist/esm2022/lib/components/recovery-codes.component.mjs +122 -0
- package/dist/esm2022/lib/components/sign-in.component.mjs +47 -4
- package/dist/esm2022/lib/components/sign-up-form.component.mjs +136 -10
- package/dist/esm2022/lib/components/sign-up.component.mjs +86 -8
- package/dist/esm2022/lib/i18n.mjs +61 -1
- package/dist/esm2022/lib/provide-atlas.mjs +4 -2
- package/dist/esm2022/lib/types.mjs +1 -1
- package/dist/esm2022/lib/version.mjs +2 -2
- package/dist/esm2022/public-api.mjs +6 -1
- package/dist/fesm2022/atlasauth-angular.mjs +2129 -95
- package/dist/fesm2022/atlasauth-angular.mjs.map +1 -1
- package/dist/lib/appearance.d.ts +1 -1
- package/dist/lib/atlas.service.d.ts +23 -1
- package/dist/lib/atlas.ui.d.ts +3 -1
- package/dist/lib/components/attempt-flow.component.d.ts +61 -2
- package/dist/lib/components/flow-controller.d.ts +73 -1
- package/dist/lib/components/impersonation-banner.component.d.ts +27 -0
- package/dist/lib/components/organization-audit-log.component.d.ts +34 -0
- package/dist/lib/components/organization-audit-log.d.ts +25 -0
- package/dist/lib/components/organization-profile.component.d.ts +55 -6
- package/dist/lib/components/policy-host.d.ts +88 -0
- package/dist/lib/components/recovery-codes.component.d.ts +26 -0
- package/dist/lib/components/sign-in.component.d.ts +2 -0
- package/dist/lib/components/sign-up-form.component.d.ts +22 -4
- package/dist/lib/components/sign-up.component.d.ts +13 -0
- package/dist/lib/provide-atlas.d.ts +3 -1
- package/dist/lib/types.d.ts +6 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/public-api.d.ts +5 -0
- package/package.json +3 -3
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, PLATFORM_ID, computed, Injectable, TemplateRef, ViewContainerRef, Directive, Input, ElementRef, Renderer2, EventEmitter, Output, ChangeDetectionStrategy, Component,
|
|
2
|
+
import { InjectionToken, signal, inject, PLATFORM_ID, computed, Injectable, TemplateRef, ViewContainerRef, Directive, Input, ElementRef, Renderer2, EventEmitter, Output, ChangeDetectionStrategy, Component, ChangeDetectorRef, effect, untracked, DestroyRef, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER, NgModule } from '@angular/core';
|
|
3
3
|
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
4
4
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
5
5
|
import { BehaviorSubject, Subject, combineLatest, firstValueFrom } from 'rxjs';
|
|
6
6
|
import { map, distinctUntilChanged, filter, take } from 'rxjs/operators';
|
|
7
|
-
import { TokenCache, SignOutNotifier, resolveFrontendApi, withDevBrowser, catalogLocale, configureReferralCapture, FapiClient, SignOutGuard, localizedErrorMessage, getTemplateToken, getProviderToken, signOutReasonFromClient, readRedirectResult, completeOAuthPopupLanding, mapOAuthPopupPayload, stripRedirectParams, closeOAuthPopupAfterSignIn, SDK_HEADER, sdkHeaderValue, reportSdkAdvisory, readExpiry, msUntilRefresh, nextStep, createCaptchaController, atlasCaptchaLabels, flowFromPending, initialFlow, fetchInstanceSettings, offeredFirstFactors, prepareFactor, signInCaptchaOptions, flowNeedsCaptcha, planFirstFactor, advanceWithCaptcha, createSignUp, shouldKeepPolling, pollAttempt, signUp } from '@atlasauth/js';
|
|
7
|
+
import { createOrganizationAuditLog, POLICY_STRINGS, TokenCache, SignOutNotifier, resolveFrontendApi, withDevBrowser, catalogLocale, configureReferralCapture, FapiClient, SignOutGuard, localizedErrorMessage, reauthenticate, getTemplateToken, getProviderToken, signOutReasonFromClient, readRedirectResult, completeOAuthPopupLanding, mapOAuthPopupPayload, stripRedirectParams, closeOAuthPopupAfterSignIn, SDK_HEADER, sdkHeaderValue, reportSdkAdvisory, readExpiry, msUntilRefresh, policyTranslator, injectPolicyStyles, mountPolicyClickwrap, mountPolicyConsent, controllerForPending, mountPolicyAcceptStep, joinTitles, formatPolicyDate, PolicyGateController, groupSetupKey, enrollmentSubmitKey, nextStep, awaitingSignUpLink, canDownloadRecoveryCodes, copyRecoveryCodes, downloadRecoveryCodes, createCaptchaController, atlasCaptchaLabels, PolicyAcceptanceController, flowFromPending, initialFlow, createMfaEnrollment, mfaChoicesFrom, fetchInstanceSettings, fetchLastSignInMethod, offeredFirstFactors, prepareFactor, signInCaptchaOptions, flowNeedsCaptcha, planFirstFactor, advanceWithCaptcha, loadPolicies, createSignUp, prepareSecondFactorCode, resendSignUpVerification, createPasskeySignUp, finishPasskeySignUp, pendingRecoveryCodes, acknowledgeRecoveryCodes, shouldKeepPolling, pollAttempt, lastUsedMethodLabel, signUp, confirmPolicies, passkeysSupported, awaitingPasskey, OrganizationProfileController, describeAuditAction, actorFromClaims } from '@atlasauth/js';
|
|
8
8
|
export { FapiClient, SIGN_OUT_EVENT, SignOutError, advance, flowFromPending, getProviderToken, initialFlow, onAtlasSignOut, pollAttempt, prepareFactor, requestForStep, shouldKeepPolling } from '@atlasauth/js';
|
|
9
9
|
import { evaluate } from '@atlasauth/authz';
|
|
10
10
|
export { evaluate, hasFromClaims } from '@atlasauth/authz';
|
|
@@ -19,6 +19,43 @@ import { Router } from '@angular/router';
|
|
|
19
19
|
*/
|
|
20
20
|
const ATLAS_OPTIONS = new InjectionToken('ATLAS_OPTIONS');
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The active organization's audit trail, for its admins — the engine behind
|
|
24
|
+
* `<atlas-organization-audit-log>` and `AtlasService.organizationAuditLog()`
|
|
25
|
+
* (the Angular peer of React's `useOrganizationAuditLog`). `state` is a signal
|
|
26
|
+
* of `{ entries, loading, forbidden, hasMore }`, one page at a time, newest
|
|
27
|
+
* first. It follows the active organization until {@link destroy}.
|
|
28
|
+
*/
|
|
29
|
+
class AtlasOrganizationAuditLog {
|
|
30
|
+
_state;
|
|
31
|
+
log;
|
|
32
|
+
unsubscribe;
|
|
33
|
+
organizationSub;
|
|
34
|
+
state;
|
|
35
|
+
constructor(client, organization$, options = {}) {
|
|
36
|
+
this.log = createOrganizationAuditLog(client, options);
|
|
37
|
+
this._state = signal(this.log.getState());
|
|
38
|
+
this.state = this._state.asReadonly();
|
|
39
|
+
this.unsubscribe = this.log.subscribe((next) => this._state.set(next));
|
|
40
|
+
this.organizationSub = organization$
|
|
41
|
+
.pipe(map((membership) => membership?.organization.id ?? null), distinctUntilChanged())
|
|
42
|
+
.subscribe((id) => void this.log.load(id));
|
|
43
|
+
}
|
|
44
|
+
/** Append the next page, when there is one. */
|
|
45
|
+
loadMore() {
|
|
46
|
+
return this.log.loadMore();
|
|
47
|
+
}
|
|
48
|
+
/** Re-read from the newest entry. */
|
|
49
|
+
reload() {
|
|
50
|
+
return this.log.reload();
|
|
51
|
+
}
|
|
52
|
+
/** Stop following the active organization. */
|
|
53
|
+
destroy() {
|
|
54
|
+
this.organizationSub.unsubscribe();
|
|
55
|
+
this.unsubscribe();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
22
59
|
/**
|
|
23
60
|
* §10.3 theming.
|
|
24
61
|
*
|
|
@@ -163,6 +200,7 @@ const EN_US = {
|
|
|
163
200
|
'signIn.submit': 'Continue',
|
|
164
201
|
'signIn.forgotPassword': 'Forgot password?',
|
|
165
202
|
'signIn.noAccount': 'No account?',
|
|
203
|
+
'signIn.lastUsed': 'Last used',
|
|
166
204
|
'signIn.signUpLink': 'Sign up',
|
|
167
205
|
'signIn.orDivider': 'or',
|
|
168
206
|
'signIn.socialButton': 'Continue with {provider}',
|
|
@@ -170,6 +208,8 @@ const EN_US = {
|
|
|
170
208
|
'signIn.useEmailCode': 'Email me a code',
|
|
171
209
|
'signIn.codeSentTo': 'We sent a code to {email}. Enter it below.',
|
|
172
210
|
'signIn.resendCode': 'Resend code',
|
|
211
|
+
'signIn.sending': 'Sending…',
|
|
212
|
+
'signIn.emailCodeOption': 'Email me a code instead',
|
|
173
213
|
'signIn.magicLinkSent': 'Check your email for a sign-in link.',
|
|
174
214
|
'signIn.signedIn': 'You\'re signed in.',
|
|
175
215
|
'signIn.sessionNotKept': 'You signed in, but this browser didn\'t keep the session. If it blocks third-party cookies, allow cookies for this site and try again.',
|
|
@@ -189,6 +229,13 @@ const EN_US = {
|
|
|
189
229
|
'signUp.and': 'and',
|
|
190
230
|
// = EMAIL_UNAVAILABLE_MESSAGE from @atlasauth/js.
|
|
191
231
|
'signUp.emailUnavailable': "This app can't send email right now. Try another sign-in method or contact the app owner.",
|
|
232
|
+
'signUp.passkeyOption': 'Sign up with a passkey instead',
|
|
233
|
+
'signUp.passwordOption': 'Sign up with a password instead',
|
|
234
|
+
'signUp.passkeyIntro': "No password needed: you'll sign in with a passkey saved on this device or in your password manager.",
|
|
235
|
+
'signUp.passkeyPrompt': 'Create a passkey to finish setting up your account.',
|
|
236
|
+
'signUp.passkeyCreate': 'Create passkey',
|
|
237
|
+
'signUp.passkeyCancelled': "The passkey wasn't created. Try again when you're ready.",
|
|
238
|
+
'signUp.linkSentTo': 'We sent a confirmation link to {email}. Open it on any device; this page continues on its own.',
|
|
192
239
|
'captcha.prompt': 'Complete the security check to continue.',
|
|
193
240
|
'captcha.required': 'Please complete the security check, then try again.',
|
|
194
241
|
'captcha.failed': "The security check didn't pass. Please try again.",
|
|
@@ -206,6 +253,24 @@ const EN_US = {
|
|
|
206
253
|
'mfa.usePasskey': 'Use a passkey instead',
|
|
207
254
|
'mfa.useBackupCode': 'Use a recovery code',
|
|
208
255
|
'mfa.submit': 'Verify',
|
|
256
|
+
'mfa.enrollChoose': 'Set up a second step for signing in.',
|
|
257
|
+
'mfa.enrollTotpOption': 'Use an authenticator app',
|
|
258
|
+
'mfa.enrollSmsOption': 'Use text messages',
|
|
259
|
+
'mfa.enrollScan': 'Scan this QR code with your authenticator app, or enter the setup key.',
|
|
260
|
+
'mfa.enrollQrAlt': 'QR code for your authenticator app',
|
|
261
|
+
'mfa.enrollSetupKey': 'Setup key',
|
|
262
|
+
'mfa.enrollPreparing': 'Setting up…',
|
|
263
|
+
'mfa.phoneLabel': 'Mobile number',
|
|
264
|
+
'mfa.phoneHint': 'Include the country code, for example +14155550101.',
|
|
265
|
+
'mfa.sendCode': 'Text me a code',
|
|
266
|
+
'mfa.codeTextedTo': 'Enter the code we texted to {phone}.',
|
|
267
|
+
'mfa.smsCodeOption': 'Text me a code instead',
|
|
268
|
+
'mfa.recoveryCodesTitle': 'Save your recovery codes',
|
|
269
|
+
'mfa.recoveryCodesHelp': "Each code works once. If you can't use your second step, sign in with one of these. Keep them somewhere safe: they won't be shown again.",
|
|
270
|
+
'mfa.recoveryCodesCopy': 'Copy codes',
|
|
271
|
+
'mfa.recoveryCodesCopied': 'Copied.',
|
|
272
|
+
'mfa.recoveryCodesDownload': 'Download as .txt',
|
|
273
|
+
'mfa.recoveryCodesSaved': "I've saved my codes",
|
|
209
274
|
'reset.title': 'Reset your password',
|
|
210
275
|
'reset.submit': 'Send reset code',
|
|
211
276
|
'reset.newPasswordLabel': 'New password',
|
|
@@ -233,6 +298,37 @@ const EN_US = {
|
|
|
233
298
|
'organization.members': 'Members',
|
|
234
299
|
'organization.invite': 'Invite member',
|
|
235
300
|
'organization.roleLabel': 'Role',
|
|
301
|
+
'organization.invitations': 'Invitations',
|
|
302
|
+
'organization.settings': 'Settings',
|
|
303
|
+
'organization.emailLabel': 'Email address',
|
|
304
|
+
'organization.sendInvite': 'Send invitation',
|
|
305
|
+
'organization.noInvitations': 'No pending invitations.',
|
|
306
|
+
'organization.remove': 'Remove',
|
|
307
|
+
'organization.revoke': 'Revoke',
|
|
308
|
+
'organization.resend': 'Resend',
|
|
309
|
+
'organization.you': '(you)',
|
|
310
|
+
'organization.leave': 'Leave organization',
|
|
311
|
+
'organization.leaveConfirm': 'Leave {name}? You will lose access until you are invited again.',
|
|
312
|
+
'organization.delete': 'Delete organization',
|
|
313
|
+
'organization.deleteConfirm': 'Delete {name} for every member? This cannot be undone.',
|
|
314
|
+
'organization.confirm': 'Confirm',
|
|
315
|
+
'organization.cancel': 'Cancel',
|
|
316
|
+
'organization.unnamedMember': 'Unnamed member',
|
|
317
|
+
'organization.passwordPrompt': 'Enter your password to confirm.',
|
|
318
|
+
'organization.roles': 'Roles',
|
|
319
|
+
'organization.roleName': 'Role name',
|
|
320
|
+
'organization.roleKey': 'Role key (for example org:auditor)',
|
|
321
|
+
'organization.rolePermissions': 'Permissions',
|
|
322
|
+
'organization.createRole': 'Create role',
|
|
323
|
+
'organization.saveRole': 'Save role',
|
|
324
|
+
'organization.editRole': 'Edit',
|
|
325
|
+
'organization.deleteRole': 'Delete',
|
|
326
|
+
'organization.noCustomRoles': 'This organization has no roles of its own yet.',
|
|
327
|
+
'impersonation.banner': 'You are signed in as {name} on someone else\'s behalf.',
|
|
328
|
+
'impersonation.returnToAdmin': 'Return to admin',
|
|
329
|
+
'impersonation.end': 'End impersonation',
|
|
330
|
+
// Policy acceptance (terms, privacy, any policy).
|
|
331
|
+
...POLICY_STRINGS,
|
|
236
332
|
'error.generic': 'Something went wrong. Please try again.',
|
|
237
333
|
'error.network': 'We could not reach the server. Check your connection.',
|
|
238
334
|
// Server errors, by the stable `reason` the Frontend API sends. Shown instead
|
|
@@ -293,7 +389,7 @@ function withFallback(locale) {
|
|
|
293
389
|
}
|
|
294
390
|
|
|
295
391
|
/** This package's version, sent as `X-Atlas-SDK` (kept in step with package.json by a test). */
|
|
296
|
-
const ATLAS_ANGULAR_VERSION = '0.
|
|
392
|
+
const ATLAS_ANGULAR_VERSION = '0.9.0';
|
|
297
393
|
const ATLAS_ANGULAR_SDK = { name: '@atlasauth/angular', version: ATLAS_ANGULAR_VERSION };
|
|
298
394
|
|
|
299
395
|
/**
|
|
@@ -507,6 +603,23 @@ class AtlasService {
|
|
|
507
603
|
return null;
|
|
508
604
|
}
|
|
509
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* The active organization's audit trail, for its admins: `state` is a signal
|
|
608
|
+
* of `{ entries, loading, forbidden, hasMore }`, one page at a time with
|
|
609
|
+
* `loadMore()`. `action` narrows it (exact, or a prefix ending in `.`). It
|
|
610
|
+
* follows the active organization; call `destroy()` when done. The peer of
|
|
611
|
+
* React's `useOrganizationAuditLog`.
|
|
612
|
+
*/
|
|
613
|
+
organizationAuditLog(options = {}) {
|
|
614
|
+
return new AtlasOrganizationAuditLog(this.createFapiClient(), this.organization$, options);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Re-authenticate the signed-in user with their password (a step-up), for an
|
|
618
|
+
* action the server answered with STEP_UP_REQUIRED. Rejects when refused.
|
|
619
|
+
*/
|
|
620
|
+
reauthenticate(password) {
|
|
621
|
+
return reauthenticate({ strategy: 'password', password }, { api: this.baseUrl, publishableKey: this.publishableKey, fetchImpl: this.doFetch, locale: this.requestLocale });
|
|
622
|
+
}
|
|
510
623
|
/** A fresh {@link FapiClient} for driving a sign-in/sign-up flow. */
|
|
511
624
|
createFapiClient() {
|
|
512
625
|
return new FapiClient({
|
|
@@ -574,6 +687,25 @@ class AtlasService {
|
|
|
574
687
|
await this.reload();
|
|
575
688
|
return this.guard.settle(attempt, options);
|
|
576
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* End the active impersonation ("return to admin"): the impersonator's own
|
|
692
|
+
* session on this browser becomes active again with no sign-in
|
|
693
|
+
* (`resumed: true`); with none, another account signed in here is adopted,
|
|
694
|
+
* or this browser is signed out.
|
|
695
|
+
*/
|
|
696
|
+
async endImpersonation() {
|
|
697
|
+
const outcome = await this.guard.endImpersonation(this.cache.peek()?.sessionId ?? this._session.value?.id ?? null);
|
|
698
|
+
if (!outcome.ended)
|
|
699
|
+
return outcome;
|
|
700
|
+
if (!outcome.activeSessionId) {
|
|
701
|
+
this.clearLocalState('user');
|
|
702
|
+
return outcome;
|
|
703
|
+
}
|
|
704
|
+
if (outcome.jwt)
|
|
705
|
+
this.applyToken(outcome.jwt, outcome.activeSessionId);
|
|
706
|
+
await this.reload();
|
|
707
|
+
return outcome;
|
|
708
|
+
}
|
|
577
709
|
/**
|
|
578
710
|
* Switch the active organization (or `null` for the personal account). The
|
|
579
711
|
* new token carries the new org claims, so the app's permission checks update
|
|
@@ -674,7 +806,7 @@ class AtlasService {
|
|
|
674
806
|
readPendingAttempt() {
|
|
675
807
|
const result = readRedirectResult(window.location.search);
|
|
676
808
|
return result?.status && !result.ticket
|
|
677
|
-
? { id: result.attemptId, status: result.status }
|
|
809
|
+
? { id: result.attemptId, status: result.status, ...(result.policyToken ? { policyToken: result.policyToken } : {}) }
|
|
678
810
|
: null;
|
|
679
811
|
}
|
|
680
812
|
async call(path, init) {
|
|
@@ -1069,6 +1201,254 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1069
1201
|
args: ['atlasCaptchaSlot']
|
|
1070
1202
|
}] } });
|
|
1071
1203
|
|
|
1204
|
+
/** The policy UI's translator (the localization bundle, else the English policy strings) for the current Atlas service. */
|
|
1205
|
+
function policyUi(atlas) {
|
|
1206
|
+
return {
|
|
1207
|
+
t: policyTranslator((key, vars) => translate(atlas.localization, key, vars)),
|
|
1208
|
+
classes: {
|
|
1209
|
+
link: 'atlas-link',
|
|
1210
|
+
text: 'atlas-text',
|
|
1211
|
+
primaryButton: classFor('formButtonPrimary', atlas.appearance, 'atlas-button'),
|
|
1212
|
+
error: classFor('formFieldError', atlas.appearance, 'atlas-error'),
|
|
1213
|
+
},
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* `[atlasPolicies]="controller"` — draws the instance's policies (terms, privacy,
|
|
1218
|
+
* …) into the host element: `kind="consent"` (default) is the checkbox block with
|
|
1219
|
+
* its in-page viewer, `kind="clickwrap"` the "By continuing you agree to …" line.
|
|
1220
|
+
* The state is the controller's (`AtlasFlowController.policies`); the DOM comes
|
|
1221
|
+
* from `@atlasauth/js`, shared with every SDK.
|
|
1222
|
+
*/
|
|
1223
|
+
class AtlasPoliciesDirective {
|
|
1224
|
+
host = inject(ElementRef);
|
|
1225
|
+
atlas = inject(AtlasService);
|
|
1226
|
+
mounted = null;
|
|
1227
|
+
ready = false;
|
|
1228
|
+
controller = null;
|
|
1229
|
+
atlasPolicyKind = 'consent';
|
|
1230
|
+
ngOnInit() {
|
|
1231
|
+
this.ready = true;
|
|
1232
|
+
this.draw();
|
|
1233
|
+
}
|
|
1234
|
+
ngOnChanges(changes) {
|
|
1235
|
+
if (this.ready && (changes['controller'] || changes['atlasPolicyKind']))
|
|
1236
|
+
this.draw();
|
|
1237
|
+
}
|
|
1238
|
+
ngOnDestroy() {
|
|
1239
|
+
this.mounted?.destroy();
|
|
1240
|
+
}
|
|
1241
|
+
draw() {
|
|
1242
|
+
this.mounted?.destroy();
|
|
1243
|
+
this.mounted = null;
|
|
1244
|
+
if (!this.controller)
|
|
1245
|
+
return;
|
|
1246
|
+
const el = this.host.nativeElement;
|
|
1247
|
+
injectPolicyStyles(el.ownerDocument);
|
|
1248
|
+
const ui = policyUi(this.atlas);
|
|
1249
|
+
this.mounted =
|
|
1250
|
+
this.atlasPolicyKind === 'clickwrap'
|
|
1251
|
+
? mountPolicyClickwrap(el, this.controller, ui)
|
|
1252
|
+
: mountPolicyConsent(el, this.controller, ui);
|
|
1253
|
+
}
|
|
1254
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasPoliciesDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1255
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: AtlasPoliciesDirective, isStandalone: true, selector: "[atlasPolicies]", inputs: { controller: ["atlasPolicies", "controller"], atlasPolicyKind: "atlasPolicyKind" }, usesOnChanges: true, ngImport: i0 });
|
|
1256
|
+
}
|
|
1257
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasPoliciesDirective, decorators: [{
|
|
1258
|
+
type: Directive,
|
|
1259
|
+
args: [{ selector: '[atlasPolicies]', standalone: true }]
|
|
1260
|
+
}], propDecorators: { controller: [{
|
|
1261
|
+
type: Input,
|
|
1262
|
+
args: ['atlasPolicies']
|
|
1263
|
+
}], atlasPolicyKind: [{
|
|
1264
|
+
type: Input
|
|
1265
|
+
}] } });
|
|
1266
|
+
/**
|
|
1267
|
+
* `<atlas-policy-accept-step>` — the sign-in step for a `needs_policy_acceptance`
|
|
1268
|
+
* attempt: scroll-mode policies in their unskippable dialog, the rest as
|
|
1269
|
+
* checkboxes, then "Accept and continue". Emits `submitFlow` with the
|
|
1270
|
+
* `policy_acceptances` JSON for the flow, and `declined` with the policy title
|
|
1271
|
+
* when the user declines (cancel the attempt).
|
|
1272
|
+
*/
|
|
1273
|
+
class PolicyAcceptStepComponent {
|
|
1274
|
+
host = inject(ElementRef);
|
|
1275
|
+
atlas = inject(AtlasService);
|
|
1276
|
+
step = null;
|
|
1277
|
+
alive = true;
|
|
1278
|
+
attempt = null;
|
|
1279
|
+
busy = false;
|
|
1280
|
+
submitFlow = new EventEmitter();
|
|
1281
|
+
declined = new EventEmitter();
|
|
1282
|
+
loading = signal(true);
|
|
1283
|
+
loadingText = policyUi(this.atlas).t('policy.loading');
|
|
1284
|
+
ngOnInit() {
|
|
1285
|
+
const client = this.atlas.createFapiClient();
|
|
1286
|
+
void controllerForPending(client, this.attempt, { locale: this.atlas.locale ?? undefined }).then((controller) => {
|
|
1287
|
+
if (!this.alive)
|
|
1288
|
+
return;
|
|
1289
|
+
this.loading.set(false);
|
|
1290
|
+
const el = this.host.nativeElement.querySelector('.atlas-policy-step-host');
|
|
1291
|
+
if (!el)
|
|
1292
|
+
return;
|
|
1293
|
+
injectPolicyStyles(el.ownerDocument);
|
|
1294
|
+
this.step = mountPolicyAcceptStep(el, controller, {
|
|
1295
|
+
...policyUi(this.atlas),
|
|
1296
|
+
onSubmit: (acceptances) => this.submitFlow.emit({ policy_acceptances: JSON.stringify(acceptances) }),
|
|
1297
|
+
onDecline: (policy) => this.declined.emit(policy.title),
|
|
1298
|
+
});
|
|
1299
|
+
this.step.setBusy(this.busy);
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
ngOnChanges(changes) {
|
|
1303
|
+
if (changes['busy'])
|
|
1304
|
+
this.step?.setBusy(this.busy);
|
|
1305
|
+
}
|
|
1306
|
+
ngOnDestroy() {
|
|
1307
|
+
this.alive = false;
|
|
1308
|
+
this.step?.destroy();
|
|
1309
|
+
}
|
|
1310
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PolicyAcceptStepComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1311
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: PolicyAcceptStepComponent, isStandalone: true, selector: "atlas-policy-accept-step", inputs: { attempt: "attempt", busy: "busy" }, outputs: { submitFlow: "submitFlow", declined: "declined" }, usesOnChanges: true, ngImport: i0, template: `@if (loading()) {<p class="atlas-text">{{ loadingText }}</p>}<div class="atlas-policy-step-host"></div>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1312
|
+
}
|
|
1313
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PolicyAcceptStepComponent, decorators: [{
|
|
1314
|
+
type: Component,
|
|
1315
|
+
args: [{
|
|
1316
|
+
selector: 'atlas-policy-accept-step',
|
|
1317
|
+
standalone: true,
|
|
1318
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1319
|
+
template: `@if (loading()) {<p class="atlas-text">{{ loadingText }}</p>}<div class="atlas-policy-step-host"></div>`,
|
|
1320
|
+
}]
|
|
1321
|
+
}], propDecorators: { attempt: [{
|
|
1322
|
+
type: Input
|
|
1323
|
+
}], busy: [{
|
|
1324
|
+
type: Input
|
|
1325
|
+
}], submitFlow: [{
|
|
1326
|
+
type: Output
|
|
1327
|
+
}], declined: [{
|
|
1328
|
+
type: Output
|
|
1329
|
+
}] } });
|
|
1330
|
+
/**
|
|
1331
|
+
* `<atlas-policy-gate>` — in-app re-acceptance. For a signed-in user, a banner
|
|
1332
|
+
* when a policy needs re-acceptance; "Review and accept" opens the unskippable
|
|
1333
|
+
* dialog / checkboxes and posts `/v1/client/me/policies/accept`. Projects its
|
|
1334
|
+
* content always.
|
|
1335
|
+
*/
|
|
1336
|
+
class PolicyGateComponent {
|
|
1337
|
+
host = inject(ElementRef);
|
|
1338
|
+
atlas = inject(AtlasService);
|
|
1339
|
+
ui = policyUi(this.atlas);
|
|
1340
|
+
gate;
|
|
1341
|
+
off = null;
|
|
1342
|
+
step = null;
|
|
1343
|
+
version = signal(0);
|
|
1344
|
+
accepted = new EventEmitter();
|
|
1345
|
+
reviewing = signal(false);
|
|
1346
|
+
error = signal('');
|
|
1347
|
+
title = this.ui.t('policy.bannerTitle');
|
|
1348
|
+
reviewText = this.ui.t('policy.bannerReview');
|
|
1349
|
+
linkClass = this.ui.classes.link ?? '';
|
|
1350
|
+
errorClass = this.ui.classes.error ?? '';
|
|
1351
|
+
bannerClass = 'atlas-policy-banner';
|
|
1352
|
+
cardClass = classFor('card', this.atlas.appearance, 'atlas-card');
|
|
1353
|
+
bannerText() {
|
|
1354
|
+
this.version();
|
|
1355
|
+
if (!this.gate || this.gate.pending.length === 0 || this.reviewing())
|
|
1356
|
+
return null;
|
|
1357
|
+
const t = this.ui.t;
|
|
1358
|
+
const grace = this.gate.graceEndsAt;
|
|
1359
|
+
return (t('policy.bannerBody', { policies: joinTitles(this.gate.pending.map((p) => p.title), t('policy.and')) }) +
|
|
1360
|
+
(grace ? ` ${t('policy.bannerGrace', { date: formatPolicyDate(grace) })}` : ''));
|
|
1361
|
+
}
|
|
1362
|
+
ngOnInit() {
|
|
1363
|
+
this.gate = new PolicyGateController(this.atlas.createFapiClient(), this.atlas.locale ?? undefined);
|
|
1364
|
+
this.off = this.gate.subscribe(() => this.version.update((n) => n + 1));
|
|
1365
|
+
if (this.atlas.status() === 'signed_in')
|
|
1366
|
+
void this.gate.load();
|
|
1367
|
+
}
|
|
1368
|
+
async begin() {
|
|
1369
|
+
const controller = await this.gate.begin();
|
|
1370
|
+
this.reviewing.set(true);
|
|
1371
|
+
await Promise.resolve();
|
|
1372
|
+
const el = this.host.nativeElement.querySelector('.atlas-policy-gate-host');
|
|
1373
|
+
if (!el)
|
|
1374
|
+
return;
|
|
1375
|
+
injectPolicyStyles(el.ownerDocument);
|
|
1376
|
+
this.step = mountPolicyAcceptStep(el, controller, {
|
|
1377
|
+
...this.ui,
|
|
1378
|
+
onSubmit: async () => {
|
|
1379
|
+
this.step?.setBusy(true);
|
|
1380
|
+
const res = await this.gate.accept(controller);
|
|
1381
|
+
this.step?.setBusy(false);
|
|
1382
|
+
if (!res.ok) {
|
|
1383
|
+
this.error.set(res.errors[0]?.message ?? this.ui.t('policy.requiredGeneric'));
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
this.close();
|
|
1387
|
+
this.accepted.emit();
|
|
1388
|
+
},
|
|
1389
|
+
onDecline: () => this.close(),
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
close() {
|
|
1393
|
+
this.step?.destroy();
|
|
1394
|
+
this.step = null;
|
|
1395
|
+
this.reviewing.set(false);
|
|
1396
|
+
}
|
|
1397
|
+
ngOnDestroy() {
|
|
1398
|
+
this.off?.();
|
|
1399
|
+
this.step?.destroy();
|
|
1400
|
+
}
|
|
1401
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PolicyGateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1402
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: PolicyGateComponent, isStandalone: true, selector: "atlas-policy-gate", outputs: { accepted: "accepted" }, ngImport: i0, template: `
|
|
1403
|
+
@if (bannerText(); as text) {
|
|
1404
|
+
<div [class]="bannerClass" role="status" data-atlas-policy-banner="">
|
|
1405
|
+
<span
|
|
1406
|
+
><strong>{{ title }}. </strong>{{ text }}</span
|
|
1407
|
+
>
|
|
1408
|
+
<button type="button" [class]="linkClass" (click)="begin()">{{ reviewText }}</button>
|
|
1409
|
+
</div>
|
|
1410
|
+
}
|
|
1411
|
+
@if (reviewing()) {
|
|
1412
|
+
<div [class]="cardClass" data-atlas-policy-gate="">
|
|
1413
|
+
<div class="atlas-policy-gate-host"></div>
|
|
1414
|
+
@if (error()) {
|
|
1415
|
+
<p [class]="errorClass" role="alert">{{ error() }}</p>
|
|
1416
|
+
}
|
|
1417
|
+
</div>
|
|
1418
|
+
}
|
|
1419
|
+
<ng-content></ng-content>
|
|
1420
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1421
|
+
}
|
|
1422
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PolicyGateComponent, decorators: [{
|
|
1423
|
+
type: Component,
|
|
1424
|
+
args: [{
|
|
1425
|
+
selector: 'atlas-policy-gate',
|
|
1426
|
+
standalone: true,
|
|
1427
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1428
|
+
template: `
|
|
1429
|
+
@if (bannerText(); as text) {
|
|
1430
|
+
<div [class]="bannerClass" role="status" data-atlas-policy-banner="">
|
|
1431
|
+
<span
|
|
1432
|
+
><strong>{{ title }}. </strong>{{ text }}</span
|
|
1433
|
+
>
|
|
1434
|
+
<button type="button" [class]="linkClass" (click)="begin()">{{ reviewText }}</button>
|
|
1435
|
+
</div>
|
|
1436
|
+
}
|
|
1437
|
+
@if (reviewing()) {
|
|
1438
|
+
<div [class]="cardClass" data-atlas-policy-gate="">
|
|
1439
|
+
<div class="atlas-policy-gate-host"></div>
|
|
1440
|
+
@if (error()) {
|
|
1441
|
+
<p [class]="errorClass" role="alert">{{ error() }}</p>
|
|
1442
|
+
}
|
|
1443
|
+
</div>
|
|
1444
|
+
}
|
|
1445
|
+
<ng-content></ng-content>
|
|
1446
|
+
`,
|
|
1447
|
+
}]
|
|
1448
|
+
}], propDecorators: { accepted: [{
|
|
1449
|
+
type: Output
|
|
1450
|
+
}] } });
|
|
1451
|
+
|
|
1072
1452
|
/**
|
|
1073
1453
|
* The server-driven flow shell, shared by `<atlas-sign-in>` and
|
|
1074
1454
|
* `<atlas-sign-up>`. The Angular peer of React's internal `AttemptFlow`.
|
|
@@ -1105,6 +1485,40 @@ class AttemptFlowComponent {
|
|
|
1105
1485
|
submitFlow = new EventEmitter();
|
|
1106
1486
|
/** Emits when the user picks a first factor the server must send first (an emailed code). */
|
|
1107
1487
|
chooseFactor = new EventEmitter();
|
|
1488
|
+
/**
|
|
1489
|
+
* Offer "Email me a code instead" at the second factor (the instance's
|
|
1490
|
+
* `account.emailSecondFactor`); `emailSecondFactor` asks for it and
|
|
1491
|
+
* `emailedSecondFactor` reports how it went.
|
|
1492
|
+
*/
|
|
1493
|
+
offerEmailSecondFactor = false;
|
|
1494
|
+
/** `AtlasFlowController.emailSecondFactor()`: in flight, where it went, or why not. */
|
|
1495
|
+
emailedSecondFactor = null;
|
|
1496
|
+
emailSecondFactor = new EventEmitter();
|
|
1497
|
+
/**
|
|
1498
|
+
* Offer "Text me a code instead" at the second factor (where the app offers
|
|
1499
|
+
* texted codes, `AtlasFlowController.mfaChoices().sms`); `smsSecondFactor`
|
|
1500
|
+
* asks for it and `textedSecondFactor` reports how it went.
|
|
1501
|
+
*/
|
|
1502
|
+
offerSmsSecondFactor = false;
|
|
1503
|
+
/** `AtlasFlowController.smsSecondFactor()`: in flight, where it went, or why not. */
|
|
1504
|
+
textedSecondFactor = null;
|
|
1505
|
+
smsSecondFactor = new EventEmitter();
|
|
1506
|
+
/**
|
|
1507
|
+
* The MFA enrolment step (`AtlasFlowController.enrollment` and
|
|
1508
|
+
* `enrollmentState()`): the authenticator app's QR code and setup key, or a
|
|
1509
|
+
* texted code. Null: the step asks for two authenticator codes only.
|
|
1510
|
+
*/
|
|
1511
|
+
enrollment = null;
|
|
1512
|
+
enrollmentState = null;
|
|
1513
|
+
/**
|
|
1514
|
+
* The instance's `account.emailVerification`. On `link` a sign-up's
|
|
1515
|
+
* verification step has nothing to type: it says to open the emailed link
|
|
1516
|
+
* while the flow polls, and offers a resend (`resend`).
|
|
1517
|
+
*/
|
|
1518
|
+
emailVerification = 'code';
|
|
1519
|
+
resend = new EventEmitter();
|
|
1520
|
+
/** The user declined a policy the sign-in was waiting on (`needs_policy_acceptance`): cancel the attempt. */
|
|
1521
|
+
policyDeclined = new EventEmitter();
|
|
1108
1522
|
values = {};
|
|
1109
1523
|
t(key, vars) {
|
|
1110
1524
|
return translate(this.atlas.localization, key, vars);
|
|
@@ -1123,8 +1537,45 @@ class AttemptFlowComponent {
|
|
|
1123
1537
|
}
|
|
1124
1538
|
onSubmit(event) {
|
|
1125
1539
|
event.preventDefault();
|
|
1540
|
+
// Enrolment: text the code to the typed number, or confirm the factor.
|
|
1541
|
+
if (this.enrolling() && this.enrollment) {
|
|
1542
|
+
void this.enrollment.submit(this.values).then((next) => {
|
|
1543
|
+
if (next)
|
|
1544
|
+
this.submitFlow.emit(next);
|
|
1545
|
+
});
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1126
1548
|
this.submitFlow.emit(this.values);
|
|
1127
1549
|
}
|
|
1550
|
+
/** The shared enrolment step is drawn (a controller was passed and the flow is enrolling). */
|
|
1551
|
+
enrolling() {
|
|
1552
|
+
return this.step().kind === 'enroll_second_factor' && !!this.enrollment && !!this.enrollmentState;
|
|
1553
|
+
}
|
|
1554
|
+
chooseMethod(method) {
|
|
1555
|
+
this.values = {};
|
|
1556
|
+
void this.enrollment?.choose(method);
|
|
1557
|
+
}
|
|
1558
|
+
resendEnrollment() {
|
|
1559
|
+
void this.enrollment?.resend();
|
|
1560
|
+
}
|
|
1561
|
+
otherMethods(state) {
|
|
1562
|
+
return state.options.filter((m) => m !== state.method);
|
|
1563
|
+
}
|
|
1564
|
+
setupKey(secret) {
|
|
1565
|
+
return groupSetupKey(secret);
|
|
1566
|
+
}
|
|
1567
|
+
/** The submit button's label key (enrolment: "Text me a code" before the number is sent). */
|
|
1568
|
+
submitKey() {
|
|
1569
|
+
return (this.enrolling() && this.enrollmentState && enrollmentSubmitKey(this.enrollmentState)) || 'signIn.submit';
|
|
1570
|
+
}
|
|
1571
|
+
showSmsSecondFactor() {
|
|
1572
|
+
return this.offerSmsSecondFactor && this.step().kind === 'collect_second_factor';
|
|
1573
|
+
}
|
|
1574
|
+
/** Where the texted second-factor code went (masked), once it was sent. */
|
|
1575
|
+
smsSecondFactorSentTo() {
|
|
1576
|
+
const sent = this.textedSecondFactor?.sentTo;
|
|
1577
|
+
return typeof sent === 'string' ? sent || ' ' : null;
|
|
1578
|
+
}
|
|
1128
1579
|
formErrors() {
|
|
1129
1580
|
return this.errors.filter((error) => !error.param);
|
|
1130
1581
|
}
|
|
@@ -1145,6 +1596,17 @@ class AttemptFlowComponent {
|
|
|
1145
1596
|
return [];
|
|
1146
1597
|
return this.firstFactors ?? step.strategies;
|
|
1147
1598
|
}
|
|
1599
|
+
linkOnly() {
|
|
1600
|
+
return awaitingSignUpLink(this.attempt, this.emailVerification);
|
|
1601
|
+
}
|
|
1602
|
+
showEmailSecondFactor() {
|
|
1603
|
+
return this.offerEmailSecondFactor && this.step().kind === 'collect_second_factor';
|
|
1604
|
+
}
|
|
1605
|
+
/** Where the emailed second-factor code went (masked), once it was sent. */
|
|
1606
|
+
emailSecondFactorSentTo() {
|
|
1607
|
+
const sent = this.emailedSecondFactor?.sentTo;
|
|
1608
|
+
return typeof sent === 'string' ? sent || ' ' : null;
|
|
1609
|
+
}
|
|
1148
1610
|
showEmailCodeOption() {
|
|
1149
1611
|
// Before the code is sent: the choice (or the retry of a failed automatic
|
|
1150
1612
|
// send). After: "Resend code".
|
|
@@ -1156,13 +1618,22 @@ class AttemptFlowComponent {
|
|
|
1156
1618
|
return null;
|
|
1157
1619
|
return this.t('signIn.codeSentTo', { email: this.attempt?.identifier ?? '' });
|
|
1158
1620
|
}
|
|
1621
|
+
/** The attempt is `needs_policy_acceptance`. */
|
|
1622
|
+
awaitingPolicies() {
|
|
1623
|
+
return this.step().kind === 'accept_policies';
|
|
1624
|
+
}
|
|
1159
1625
|
showSubmit() {
|
|
1160
1626
|
const kind = this.step().kind;
|
|
1161
1627
|
// Nothing to submit on a first-factor step that only offers an emailed code
|
|
1162
1628
|
// until that code is sent.
|
|
1163
1629
|
if (kind === 'collect_first_factor')
|
|
1164
1630
|
return this.fields().length > 0;
|
|
1165
|
-
|
|
1631
|
+
if (this.linkOnly())
|
|
1632
|
+
return false;
|
|
1633
|
+
// Enrolment: nothing to submit while choosing a method or fetching the setup.
|
|
1634
|
+
if (this.enrolling() && this.enrollmentState && !enrollmentSubmitKey(this.enrollmentState))
|
|
1635
|
+
return false;
|
|
1636
|
+
return kind !== 'done' && kind !== 'await_oauth' && kind !== 'accept_policies';
|
|
1166
1637
|
}
|
|
1167
1638
|
fields() {
|
|
1168
1639
|
const step = this.step();
|
|
@@ -1183,6 +1654,9 @@ class AttemptFlowComponent {
|
|
|
1183
1654
|
case 'collect_second_factor':
|
|
1184
1655
|
return [{ kind: 'input', name: 'code', labelKey: 'mfa.codeLabel', type: 'text' }];
|
|
1185
1656
|
case 'enroll_second_factor':
|
|
1657
|
+
// The shared enrolment step draws its own fields (above).
|
|
1658
|
+
if (this.enrolling())
|
|
1659
|
+
return [];
|
|
1186
1660
|
// Two consecutive codes (§5.6): a phone with a fast clock fails here
|
|
1187
1661
|
// rather than on every sign-in afterwards.
|
|
1188
1662
|
return [
|
|
@@ -1190,9 +1664,12 @@ class AttemptFlowComponent {
|
|
|
1190
1664
|
{ kind: 'input', name: 'secondCode', labelKey: 'mfa.enrollSecondCodeLabel', type: 'text' },
|
|
1191
1665
|
];
|
|
1192
1666
|
case 'collect_email_code':
|
|
1193
|
-
return [{ kind: 'input', name: 'code', labelKey: 'signIn.emailCodeLabel', type: 'text' }];
|
|
1667
|
+
return this.linkOnly() ? [] : [{ kind: 'input', name: 'code', labelKey: 'signIn.emailCodeLabel', type: 'text' }];
|
|
1194
1668
|
case 'collect_new_password':
|
|
1195
1669
|
return [{ kind: 'input', name: 'password', labelKey: 'reset.newPasswordLabel', type: 'password' }];
|
|
1670
|
+
case 'accept_policies':
|
|
1671
|
+
// Drawn by <atlas-policy-accept-step> above.
|
|
1672
|
+
return [];
|
|
1196
1673
|
case 'collect_captcha':
|
|
1197
1674
|
// §5 the server withheld the attempt until a captcha clears. Solving the
|
|
1198
1675
|
// widget resubmits the identifier on its own; the button retries.
|
|
@@ -1216,7 +1693,7 @@ class AttemptFlowComponent {
|
|
|
1216
1693
|
}
|
|
1217
1694
|
}
|
|
1218
1695
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1219
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: AttemptFlowComponent, isStandalone: true, selector: "atlas-attempt-flow", inputs: { attempt: "attempt", busy: "busy", errors: "errors", sessionKept: "sessionKept", titleKey: "titleKey", captcha: "captcha", firstFactors: "firstFactors", preparedFactor: "preparedFactor" }, outputs: { submitFlow: "submitFlow", chooseFactor: "chooseFactor" }, ngImport: i0, template: `
|
|
1696
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: AttemptFlowComponent, isStandalone: true, selector: "atlas-attempt-flow", inputs: { attempt: "attempt", busy: "busy", errors: "errors", sessionKept: "sessionKept", titleKey: "titleKey", captcha: "captcha", firstFactors: "firstFactors", preparedFactor: "preparedFactor", offerEmailSecondFactor: "offerEmailSecondFactor", emailedSecondFactor: "emailedSecondFactor", offerSmsSecondFactor: "offerSmsSecondFactor", textedSecondFactor: "textedSecondFactor", enrollment: "enrollment", enrollmentState: "enrollmentState", emailVerification: "emailVerification" }, outputs: { submitFlow: "submitFlow", chooseFactor: "chooseFactor", emailSecondFactor: "emailSecondFactor", smsSecondFactor: "smsSecondFactor", resend: "resend", policyDeclined: "policyDeclined" }, ngImport: i0, template: `
|
|
1220
1697
|
<div [class]="cls('card', 'atlas-card')">
|
|
1221
1698
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
1222
1699
|
|
|
@@ -1228,6 +1705,98 @@ class AttemptFlowComponent {
|
|
|
1228
1705
|
@if (codeSentText(); as sentText) {
|
|
1229
1706
|
<p class="atlas-text" data-atlas-code-sent="">{{ sentText }}</p>
|
|
1230
1707
|
}
|
|
1708
|
+
@if (linkOnly()) {
|
|
1709
|
+
<!-- Nothing to type: the flow polls until the emailed link is opened (anywhere). -->
|
|
1710
|
+
<p class="atlas-text" data-atlas-link-sent="" aria-live="polite">
|
|
1711
|
+
{{ t('signUp.linkSentTo', { email: attempt?.identifier ?? '' }) }}
|
|
1712
|
+
</p>
|
|
1713
|
+
<button
|
|
1714
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1715
|
+
type="button"
|
|
1716
|
+
data-atlas-resend=""
|
|
1717
|
+
[disabled]="busy"
|
|
1718
|
+
(click)="resend.emit()"
|
|
1719
|
+
>
|
|
1720
|
+
{{ t('signIn.resendLink') }}
|
|
1721
|
+
</button>
|
|
1722
|
+
}
|
|
1723
|
+
@if (enrolling() && enrollmentState; as enrol) {
|
|
1724
|
+
@if (enrol.method === null) {
|
|
1725
|
+
<p class="atlas-text">{{ t('mfa.enrollChoose') }}</p>
|
|
1726
|
+
@for (m of enrol.options; track m) {
|
|
1727
|
+
<button
|
|
1728
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1729
|
+
type="button"
|
|
1730
|
+
[attr.data-atlas-mfa-method]="m"
|
|
1731
|
+
[disabled]="busy || enrol.busy"
|
|
1732
|
+
(click)="chooseMethod(m)"
|
|
1733
|
+
>
|
|
1734
|
+
{{ t(m === 'totp' ? 'mfa.enrollTotpOption' : 'mfa.enrollSmsOption') }}
|
|
1735
|
+
</button>
|
|
1736
|
+
}
|
|
1737
|
+
} @else if (enrol.method === 'totp') {
|
|
1738
|
+
@if (enrol.enrollment; as setup) {
|
|
1739
|
+
<p class="atlas-text">{{ t('mfa.enrollScan') }}</p>
|
|
1740
|
+
@if (setup.qrCode) {
|
|
1741
|
+
<img class="atlas-mfa-qr" data-atlas="mfa-qr" [src]="setup.qrCode" [alt]="t('mfa.enrollQrAlt')" width="180" height="180" />
|
|
1742
|
+
}
|
|
1743
|
+
@if (setup.secret) {
|
|
1744
|
+
<p class="atlas-text" data-atlas="mfa-setup-key">{{ t('mfa.enrollSetupKey') }}: <code>{{ setupKey(setup.secret) }}</code></p>
|
|
1745
|
+
}
|
|
1746
|
+
<label class="atlas-field">
|
|
1747
|
+
<span>{{ t('mfa.enrollFirstCodeLabel') }}</span>
|
|
1748
|
+
<input [class]="cls('formFieldInput', 'atlas-input')" type="text" name="code" autocomplete="one-time-code" inputmode="numeric" [value]="values['code'] ?? ''" (input)="setValue('code', $any($event.target).value)" />
|
|
1749
|
+
</label>
|
|
1750
|
+
@if (enrol.totpCodes >= 2) {
|
|
1751
|
+
<label class="atlas-field">
|
|
1752
|
+
<span>{{ t('mfa.enrollSecondCodeLabel') }}</span>
|
|
1753
|
+
<input [class]="cls('formFieldInput', 'atlas-input')" type="text" name="secondCode" autocomplete="one-time-code" inputmode="numeric" [value]="values['secondCode'] ?? ''" (input)="setValue('secondCode', $any($event.target).value)" />
|
|
1754
|
+
</label>
|
|
1755
|
+
}
|
|
1756
|
+
} @else {
|
|
1757
|
+
<p class="atlas-text" aria-live="polite">{{ enrol.busy ? t('mfa.enrollPreparing') : '' }}</p>
|
|
1758
|
+
}
|
|
1759
|
+
} @else {
|
|
1760
|
+
@if (enrol.enrollment; as texted) {
|
|
1761
|
+
<p class="atlas-text" data-atlas="mfa-code-texted" aria-live="polite">{{ t('mfa.codeTextedTo', { phone: texted.phone ?? '' }) }}</p>
|
|
1762
|
+
<label class="atlas-field">
|
|
1763
|
+
<span>{{ t('mfa.codeLabel') }}</span>
|
|
1764
|
+
<input [class]="cls('formFieldInput', 'atlas-input')" type="text" name="code" autocomplete="one-time-code" inputmode="numeric" [value]="values['code'] ?? ''" (input)="setValue('code', $any($event.target).value)" />
|
|
1765
|
+
</label>
|
|
1766
|
+
<button
|
|
1767
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1768
|
+
type="button"
|
|
1769
|
+
data-atlas-resend=""
|
|
1770
|
+
[disabled]="busy || enrol.busy"
|
|
1771
|
+
(click)="resendEnrollment()"
|
|
1772
|
+
>
|
|
1773
|
+
{{ t('signIn.resendCode') }}
|
|
1774
|
+
</button>
|
|
1775
|
+
} @else {
|
|
1776
|
+
<label class="atlas-field">
|
|
1777
|
+
<span>{{ t('mfa.phoneLabel') }}</span>
|
|
1778
|
+
<input [class]="cls('formFieldInput', 'atlas-input')" type="tel" name="phone" autocomplete="tel" [value]="values['phone'] ?? ''" (input)="setValue('phone', $any($event.target).value)" />
|
|
1779
|
+
</label>
|
|
1780
|
+
<p class="atlas-text">{{ t('mfa.phoneHint') }}</p>
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
@for (error of enrol.errors; track $index) {
|
|
1784
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error.message }}</p>
|
|
1785
|
+
}
|
|
1786
|
+
@if (enrol.method !== null && enrol.options.length > 1) {
|
|
1787
|
+
@for (other of otherMethods(enrol); track other) {
|
|
1788
|
+
<button
|
|
1789
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1790
|
+
type="button"
|
|
1791
|
+
[attr.data-atlas-mfa-method]="other"
|
|
1792
|
+
[disabled]="busy || enrol.busy"
|
|
1793
|
+
(click)="chooseMethod(other)"
|
|
1794
|
+
>
|
|
1795
|
+
{{ t(other === 'totp' ? 'mfa.enrollTotpOption' : 'mfa.enrollSmsOption') }}
|
|
1796
|
+
</button>
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1231
1800
|
@for (f of fields(); track $index) {
|
|
1232
1801
|
@if (f.kind === 'input') {
|
|
1233
1802
|
<label class="atlas-field">
|
|
@@ -1250,16 +1819,68 @@ class AttemptFlowComponent {
|
|
|
1250
1819
|
}
|
|
1251
1820
|
}
|
|
1252
1821
|
|
|
1822
|
+
@if (awaitingPolicies()) {
|
|
1823
|
+
<!-- A required policy changed: scroll-mode ones in their dialog, the rest as checkboxes. -->
|
|
1824
|
+
<atlas-policy-accept-step
|
|
1825
|
+
[attempt]="attempt"
|
|
1826
|
+
[busy]="busy"
|
|
1827
|
+
(submitFlow)="submitFlow.emit($event)"
|
|
1828
|
+
(declined)="policyDeclined.emit($event)"
|
|
1829
|
+
></atlas-policy-accept-step>
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1253
1832
|
@if (showCaptcha()) {
|
|
1254
1833
|
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1255
1834
|
}
|
|
1256
1835
|
|
|
1257
1836
|
@if (showSubmit()) {
|
|
1258
|
-
<button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy" type="submit">
|
|
1259
|
-
{{ t(
|
|
1837
|
+
<button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy || (enrolling() && !!enrollmentState?.busy)" type="submit">
|
|
1838
|
+
{{ t(submitKey()) }}
|
|
1260
1839
|
</button>
|
|
1261
1840
|
}
|
|
1262
1841
|
|
|
1842
|
+
@if (showSmsSecondFactor()) {
|
|
1843
|
+
@if (smsSecondFactorSentTo(); as sentTo) {
|
|
1844
|
+
<p class="atlas-text" data-atlas="sms-second-factor-sent" aria-live="polite">
|
|
1845
|
+
{{ t('mfa.codeTextedTo', { phone: sentTo }) }}
|
|
1846
|
+
</p>
|
|
1847
|
+
} @else {
|
|
1848
|
+
<button
|
|
1849
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1850
|
+
type="button"
|
|
1851
|
+
data-atlas="sms-second-factor"
|
|
1852
|
+
[disabled]="busy || !!textedSecondFactor?.busy"
|
|
1853
|
+
(click)="smsSecondFactor.emit()"
|
|
1854
|
+
>
|
|
1855
|
+
{{ textedSecondFactor?.busy ? t('signIn.sending') : t('mfa.smsCodeOption') }}
|
|
1856
|
+
</button>
|
|
1857
|
+
@if (textedSecondFactor?.error; as message) {
|
|
1858
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ message }}</p>
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
@if (showEmailSecondFactor()) {
|
|
1864
|
+
@if (emailSecondFactorSentTo(); as sentTo) {
|
|
1865
|
+
<p class="atlas-text" data-atlas="email-second-factor-sent" aria-live="polite">
|
|
1866
|
+
{{ t('signIn.codeSentTo', { email: sentTo }) }}
|
|
1867
|
+
</p>
|
|
1868
|
+
} @else {
|
|
1869
|
+
<button
|
|
1870
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1871
|
+
type="button"
|
|
1872
|
+
data-atlas="email-second-factor"
|
|
1873
|
+
[disabled]="busy || !!emailedSecondFactor?.busy"
|
|
1874
|
+
(click)="emailSecondFactor.emit()"
|
|
1875
|
+
>
|
|
1876
|
+
{{ emailedSecondFactor?.busy ? t('signIn.sending') : t('signIn.emailCodeOption') }}
|
|
1877
|
+
</button>
|
|
1878
|
+
@if (emailedSecondFactor?.error; as message) {
|
|
1879
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ message }}</p>
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1263
1884
|
@if (showEmailCodeOption()) {
|
|
1264
1885
|
<button
|
|
1265
1886
|
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
@@ -1274,7 +1895,7 @@ class AttemptFlowComponent {
|
|
|
1274
1895
|
}
|
|
1275
1896
|
</form>
|
|
1276
1897
|
</div>
|
|
1277
|
-
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1898
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }, { kind: "component", type: PolicyAcceptStepComponent, selector: "atlas-policy-accept-step", inputs: ["attempt", "busy"], outputs: ["submitFlow", "declined"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1278
1899
|
}
|
|
1279
1900
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, decorators: [{
|
|
1280
1901
|
type: Component,
|
|
@@ -1282,7 +1903,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1282
1903
|
selector: 'atlas-attempt-flow',
|
|
1283
1904
|
standalone: true,
|
|
1284
1905
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1285
|
-
imports: [AtlasCaptchaSlotDirective],
|
|
1906
|
+
imports: [AtlasCaptchaSlotDirective, PolicyAcceptStepComponent],
|
|
1286
1907
|
template: `
|
|
1287
1908
|
<div [class]="cls('card', 'atlas-card')">
|
|
1288
1909
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
@@ -1295,6 +1916,98 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1295
1916
|
@if (codeSentText(); as sentText) {
|
|
1296
1917
|
<p class="atlas-text" data-atlas-code-sent="">{{ sentText }}</p>
|
|
1297
1918
|
}
|
|
1919
|
+
@if (linkOnly()) {
|
|
1920
|
+
<!-- Nothing to type: the flow polls until the emailed link is opened (anywhere). -->
|
|
1921
|
+
<p class="atlas-text" data-atlas-link-sent="" aria-live="polite">
|
|
1922
|
+
{{ t('signUp.linkSentTo', { email: attempt?.identifier ?? '' }) }}
|
|
1923
|
+
</p>
|
|
1924
|
+
<button
|
|
1925
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1926
|
+
type="button"
|
|
1927
|
+
data-atlas-resend=""
|
|
1928
|
+
[disabled]="busy"
|
|
1929
|
+
(click)="resend.emit()"
|
|
1930
|
+
>
|
|
1931
|
+
{{ t('signIn.resendLink') }}
|
|
1932
|
+
</button>
|
|
1933
|
+
}
|
|
1934
|
+
@if (enrolling() && enrollmentState; as enrol) {
|
|
1935
|
+
@if (enrol.method === null) {
|
|
1936
|
+
<p class="atlas-text">{{ t('mfa.enrollChoose') }}</p>
|
|
1937
|
+
@for (m of enrol.options; track m) {
|
|
1938
|
+
<button
|
|
1939
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1940
|
+
type="button"
|
|
1941
|
+
[attr.data-atlas-mfa-method]="m"
|
|
1942
|
+
[disabled]="busy || enrol.busy"
|
|
1943
|
+
(click)="chooseMethod(m)"
|
|
1944
|
+
>
|
|
1945
|
+
{{ t(m === 'totp' ? 'mfa.enrollTotpOption' : 'mfa.enrollSmsOption') }}
|
|
1946
|
+
</button>
|
|
1947
|
+
}
|
|
1948
|
+
} @else if (enrol.method === 'totp') {
|
|
1949
|
+
@if (enrol.enrollment; as setup) {
|
|
1950
|
+
<p class="atlas-text">{{ t('mfa.enrollScan') }}</p>
|
|
1951
|
+
@if (setup.qrCode) {
|
|
1952
|
+
<img class="atlas-mfa-qr" data-atlas="mfa-qr" [src]="setup.qrCode" [alt]="t('mfa.enrollQrAlt')" width="180" height="180" />
|
|
1953
|
+
}
|
|
1954
|
+
@if (setup.secret) {
|
|
1955
|
+
<p class="atlas-text" data-atlas="mfa-setup-key">{{ t('mfa.enrollSetupKey') }}: <code>{{ setupKey(setup.secret) }}</code></p>
|
|
1956
|
+
}
|
|
1957
|
+
<label class="atlas-field">
|
|
1958
|
+
<span>{{ t('mfa.enrollFirstCodeLabel') }}</span>
|
|
1959
|
+
<input [class]="cls('formFieldInput', 'atlas-input')" type="text" name="code" autocomplete="one-time-code" inputmode="numeric" [value]="values['code'] ?? ''" (input)="setValue('code', $any($event.target).value)" />
|
|
1960
|
+
</label>
|
|
1961
|
+
@if (enrol.totpCodes >= 2) {
|
|
1962
|
+
<label class="atlas-field">
|
|
1963
|
+
<span>{{ t('mfa.enrollSecondCodeLabel') }}</span>
|
|
1964
|
+
<input [class]="cls('formFieldInput', 'atlas-input')" type="text" name="secondCode" autocomplete="one-time-code" inputmode="numeric" [value]="values['secondCode'] ?? ''" (input)="setValue('secondCode', $any($event.target).value)" />
|
|
1965
|
+
</label>
|
|
1966
|
+
}
|
|
1967
|
+
} @else {
|
|
1968
|
+
<p class="atlas-text" aria-live="polite">{{ enrol.busy ? t('mfa.enrollPreparing') : '' }}</p>
|
|
1969
|
+
}
|
|
1970
|
+
} @else {
|
|
1971
|
+
@if (enrol.enrollment; as texted) {
|
|
1972
|
+
<p class="atlas-text" data-atlas="mfa-code-texted" aria-live="polite">{{ t('mfa.codeTextedTo', { phone: texted.phone ?? '' }) }}</p>
|
|
1973
|
+
<label class="atlas-field">
|
|
1974
|
+
<span>{{ t('mfa.codeLabel') }}</span>
|
|
1975
|
+
<input [class]="cls('formFieldInput', 'atlas-input')" type="text" name="code" autocomplete="one-time-code" inputmode="numeric" [value]="values['code'] ?? ''" (input)="setValue('code', $any($event.target).value)" />
|
|
1976
|
+
</label>
|
|
1977
|
+
<button
|
|
1978
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1979
|
+
type="button"
|
|
1980
|
+
data-atlas-resend=""
|
|
1981
|
+
[disabled]="busy || enrol.busy"
|
|
1982
|
+
(click)="resendEnrollment()"
|
|
1983
|
+
>
|
|
1984
|
+
{{ t('signIn.resendCode') }}
|
|
1985
|
+
</button>
|
|
1986
|
+
} @else {
|
|
1987
|
+
<label class="atlas-field">
|
|
1988
|
+
<span>{{ t('mfa.phoneLabel') }}</span>
|
|
1989
|
+
<input [class]="cls('formFieldInput', 'atlas-input')" type="tel" name="phone" autocomplete="tel" [value]="values['phone'] ?? ''" (input)="setValue('phone', $any($event.target).value)" />
|
|
1990
|
+
</label>
|
|
1991
|
+
<p class="atlas-text">{{ t('mfa.phoneHint') }}</p>
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
@for (error of enrol.errors; track $index) {
|
|
1995
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error.message }}</p>
|
|
1996
|
+
}
|
|
1997
|
+
@if (enrol.method !== null && enrol.options.length > 1) {
|
|
1998
|
+
@for (other of otherMethods(enrol); track other) {
|
|
1999
|
+
<button
|
|
2000
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
2001
|
+
type="button"
|
|
2002
|
+
[attr.data-atlas-mfa-method]="other"
|
|
2003
|
+
[disabled]="busy || enrol.busy"
|
|
2004
|
+
(click)="chooseMethod(other)"
|
|
2005
|
+
>
|
|
2006
|
+
{{ t(other === 'totp' ? 'mfa.enrollTotpOption' : 'mfa.enrollSmsOption') }}
|
|
2007
|
+
</button>
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
1298
2011
|
@for (f of fields(); track $index) {
|
|
1299
2012
|
@if (f.kind === 'input') {
|
|
1300
2013
|
<label class="atlas-field">
|
|
@@ -1317,16 +2030,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1317
2030
|
}
|
|
1318
2031
|
}
|
|
1319
2032
|
|
|
2033
|
+
@if (awaitingPolicies()) {
|
|
2034
|
+
<!-- A required policy changed: scroll-mode ones in their dialog, the rest as checkboxes. -->
|
|
2035
|
+
<atlas-policy-accept-step
|
|
2036
|
+
[attempt]="attempt"
|
|
2037
|
+
[busy]="busy"
|
|
2038
|
+
(submitFlow)="submitFlow.emit($event)"
|
|
2039
|
+
(declined)="policyDeclined.emit($event)"
|
|
2040
|
+
></atlas-policy-accept-step>
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1320
2043
|
@if (showCaptcha()) {
|
|
1321
2044
|
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1322
2045
|
}
|
|
1323
2046
|
|
|
1324
2047
|
@if (showSubmit()) {
|
|
1325
|
-
<button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy" type="submit">
|
|
1326
|
-
{{ t(
|
|
2048
|
+
<button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy || (enrolling() && !!enrollmentState?.busy)" type="submit">
|
|
2049
|
+
{{ t(submitKey()) }}
|
|
1327
2050
|
</button>
|
|
1328
2051
|
}
|
|
1329
2052
|
|
|
2053
|
+
@if (showSmsSecondFactor()) {
|
|
2054
|
+
@if (smsSecondFactorSentTo(); as sentTo) {
|
|
2055
|
+
<p class="atlas-text" data-atlas="sms-second-factor-sent" aria-live="polite">
|
|
2056
|
+
{{ t('mfa.codeTextedTo', { phone: sentTo }) }}
|
|
2057
|
+
</p>
|
|
2058
|
+
} @else {
|
|
2059
|
+
<button
|
|
2060
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
2061
|
+
type="button"
|
|
2062
|
+
data-atlas="sms-second-factor"
|
|
2063
|
+
[disabled]="busy || !!textedSecondFactor?.busy"
|
|
2064
|
+
(click)="smsSecondFactor.emit()"
|
|
2065
|
+
>
|
|
2066
|
+
{{ textedSecondFactor?.busy ? t('signIn.sending') : t('mfa.smsCodeOption') }}
|
|
2067
|
+
</button>
|
|
2068
|
+
@if (textedSecondFactor?.error; as message) {
|
|
2069
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ message }}</p>
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
@if (showEmailSecondFactor()) {
|
|
2075
|
+
@if (emailSecondFactorSentTo(); as sentTo) {
|
|
2076
|
+
<p class="atlas-text" data-atlas="email-second-factor-sent" aria-live="polite">
|
|
2077
|
+
{{ t('signIn.codeSentTo', { email: sentTo }) }}
|
|
2078
|
+
</p>
|
|
2079
|
+
} @else {
|
|
2080
|
+
<button
|
|
2081
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
2082
|
+
type="button"
|
|
2083
|
+
data-atlas="email-second-factor"
|
|
2084
|
+
[disabled]="busy || !!emailedSecondFactor?.busy"
|
|
2085
|
+
(click)="emailSecondFactor.emit()"
|
|
2086
|
+
>
|
|
2087
|
+
{{ emailedSecondFactor?.busy ? t('signIn.sending') : t('signIn.emailCodeOption') }}
|
|
2088
|
+
</button>
|
|
2089
|
+
@if (emailedSecondFactor?.error; as message) {
|
|
2090
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ message }}</p>
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
|
|
1330
2095
|
@if (showEmailCodeOption()) {
|
|
1331
2096
|
<button
|
|
1332
2097
|
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
@@ -1363,6 +2128,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1363
2128
|
type: Output
|
|
1364
2129
|
}], chooseFactor: [{
|
|
1365
2130
|
type: Output
|
|
2131
|
+
}], offerEmailSecondFactor: [{
|
|
2132
|
+
type: Input
|
|
2133
|
+
}], emailedSecondFactor: [{
|
|
2134
|
+
type: Input
|
|
2135
|
+
}], emailSecondFactor: [{
|
|
2136
|
+
type: Output
|
|
2137
|
+
}], offerSmsSecondFactor: [{
|
|
2138
|
+
type: Input
|
|
2139
|
+
}], textedSecondFactor: [{
|
|
2140
|
+
type: Input
|
|
2141
|
+
}], smsSecondFactor: [{
|
|
2142
|
+
type: Output
|
|
2143
|
+
}], enrollment: [{
|
|
2144
|
+
type: Input
|
|
2145
|
+
}], enrollmentState: [{
|
|
2146
|
+
type: Input
|
|
2147
|
+
}], emailVerification: [{
|
|
2148
|
+
type: Input
|
|
2149
|
+
}], resend: [{
|
|
2150
|
+
type: Output
|
|
2151
|
+
}], policyDeclined: [{
|
|
2152
|
+
type: Output
|
|
1366
2153
|
}] } });
|
|
1367
2154
|
/** A read-only message row: `labelKey` holds the i18n key of the text to show. */
|
|
1368
2155
|
function msg(labelKey) {
|
|
@@ -1370,49 +2157,190 @@ function msg(labelKey) {
|
|
|
1370
2157
|
}
|
|
1371
2158
|
|
|
1372
2159
|
/**
|
|
1373
|
-
* The
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
* server via `advance`, and this controller only posts whatever the current step
|
|
1378
|
-
* needs and re-renders the returned status. State lives in a signal so templates
|
|
1379
|
-
* update whether the app runs zoned or zoneless.
|
|
1380
|
-
*
|
|
1381
|
-
* `<atlas-sign-up>` starts the attempt with {@link create}
|
|
1382
|
-
* (`POST /v1/client/sign_ups`); the emailed-code step then runs through the same
|
|
1383
|
-
* {@link submit}. Both flows draw the instance's captcha (from
|
|
1384
|
-
* {@link loadSettings}) when it is required.
|
|
2160
|
+
* The recovery codes a mid-sign-in MFA enrolment came with, shown once before
|
|
2161
|
+
* the sign-in finishes: copy them, download them as a .txt file, then
|
|
2162
|
+
* "I've saved my codes" (`saved`) exchanges the ticket. The codes live only in
|
|
2163
|
+
* the flow controller's memory and are forgotten once saved.
|
|
1385
2164
|
*/
|
|
1386
|
-
class
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
2165
|
+
class RecoveryCodesComponent {
|
|
2166
|
+
atlas = inject(AtlasService);
|
|
2167
|
+
cdr = inject(ChangeDetectorRef);
|
|
2168
|
+
codes = [];
|
|
2169
|
+
busy = false;
|
|
2170
|
+
saved = new EventEmitter();
|
|
2171
|
+
canCopy = typeof navigator !== 'undefined' && !!navigator.clipboard?.writeText;
|
|
2172
|
+
canDownload = canDownloadRecoveryCodes();
|
|
2173
|
+
copied = false;
|
|
2174
|
+
ngOnChanges() {
|
|
2175
|
+
this.copied = false;
|
|
2176
|
+
}
|
|
2177
|
+
t(key) {
|
|
2178
|
+
return translate(this.atlas.localization, key);
|
|
2179
|
+
}
|
|
2180
|
+
cls(key, base) {
|
|
2181
|
+
return classFor(key, this.atlas.appearance, base);
|
|
2182
|
+
}
|
|
2183
|
+
copy() {
|
|
2184
|
+
void copyRecoveryCodes(this.codes).then((ok) => {
|
|
2185
|
+
this.copied = ok;
|
|
2186
|
+
this.cdr.markForCheck();
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
download() {
|
|
2190
|
+
downloadRecoveryCodes(this.codes);
|
|
2191
|
+
}
|
|
2192
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecoveryCodesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2193
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: RecoveryCodesComponent, isStandalone: true, selector: "atlas-recovery-codes", inputs: { codes: "codes", busy: "busy" }, outputs: { saved: "saved" }, usesOnChanges: true, ngImport: i0, template: `
|
|
2194
|
+
<div [class]="cls('card', 'atlas-card')" data-atlas="recovery-codes">
|
|
2195
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('mfa.recoveryCodesTitle') }}</h1>
|
|
2196
|
+
<p class="atlas-text">{{ t('mfa.recoveryCodesHelp') }}</p>
|
|
2197
|
+
<ul
|
|
2198
|
+
class="atlas-recovery-codes"
|
|
2199
|
+
data-atlas="recovery-codes-list"
|
|
2200
|
+
style="list-style:none;padding:0;margin:8px 0 12px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center"
|
|
2201
|
+
>
|
|
2202
|
+
@for (code of codes; track code) {
|
|
2203
|
+
<li><code>{{ code }}</code></li>
|
|
2204
|
+
}
|
|
2205
|
+
</ul>
|
|
2206
|
+
<div class="atlas-alternatives">
|
|
2207
|
+
@if (canCopy) {
|
|
2208
|
+
<button [class]="cls('formButtonSecondary', 'atlas-button-secondary')" type="button" data-atlas="recovery-codes-copy" (click)="copy()">
|
|
2209
|
+
{{ t('mfa.recoveryCodesCopy') }}
|
|
2210
|
+
</button>
|
|
2211
|
+
}
|
|
2212
|
+
@if (canDownload) {
|
|
2213
|
+
<button [class]="cls('formButtonSecondary', 'atlas-button-secondary')" type="button" data-atlas="recovery-codes-download" (click)="download()">
|
|
2214
|
+
{{ t('mfa.recoveryCodesDownload') }}
|
|
2215
|
+
</button>
|
|
2216
|
+
}
|
|
2217
|
+
</div>
|
|
2218
|
+
@if (copied) {
|
|
2219
|
+
<p class="atlas-text" aria-live="polite" data-atlas="recovery-codes-copied">{{ t('mfa.recoveryCodesCopied') }}</p>
|
|
2220
|
+
}
|
|
2221
|
+
<button [class]="cls('formButtonPrimary', 'atlas-button')" type="button" data-atlas="recovery-codes-saved" [disabled]="busy" (click)="saved.emit()">
|
|
2222
|
+
{{ t('mfa.recoveryCodesSaved') }}
|
|
2223
|
+
</button>
|
|
2224
|
+
</div>
|
|
2225
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2226
|
+
}
|
|
2227
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecoveryCodesComponent, decorators: [{
|
|
2228
|
+
type: Component,
|
|
2229
|
+
args: [{
|
|
2230
|
+
selector: 'atlas-recovery-codes',
|
|
2231
|
+
standalone: true,
|
|
2232
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2233
|
+
template: `
|
|
2234
|
+
<div [class]="cls('card', 'atlas-card')" data-atlas="recovery-codes">
|
|
2235
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('mfa.recoveryCodesTitle') }}</h1>
|
|
2236
|
+
<p class="atlas-text">{{ t('mfa.recoveryCodesHelp') }}</p>
|
|
2237
|
+
<ul
|
|
2238
|
+
class="atlas-recovery-codes"
|
|
2239
|
+
data-atlas="recovery-codes-list"
|
|
2240
|
+
style="list-style:none;padding:0;margin:8px 0 12px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center"
|
|
2241
|
+
>
|
|
2242
|
+
@for (code of codes; track code) {
|
|
2243
|
+
<li><code>{{ code }}</code></li>
|
|
2244
|
+
}
|
|
2245
|
+
</ul>
|
|
2246
|
+
<div class="atlas-alternatives">
|
|
2247
|
+
@if (canCopy) {
|
|
2248
|
+
<button [class]="cls('formButtonSecondary', 'atlas-button-secondary')" type="button" data-atlas="recovery-codes-copy" (click)="copy()">
|
|
2249
|
+
{{ t('mfa.recoveryCodesCopy') }}
|
|
2250
|
+
</button>
|
|
2251
|
+
}
|
|
2252
|
+
@if (canDownload) {
|
|
2253
|
+
<button [class]="cls('formButtonSecondary', 'atlas-button-secondary')" type="button" data-atlas="recovery-codes-download" (click)="download()">
|
|
2254
|
+
{{ t('mfa.recoveryCodesDownload') }}
|
|
2255
|
+
</button>
|
|
2256
|
+
}
|
|
2257
|
+
</div>
|
|
2258
|
+
@if (copied) {
|
|
2259
|
+
<p class="atlas-text" aria-live="polite" data-atlas="recovery-codes-copied">{{ t('mfa.recoveryCodesCopied') }}</p>
|
|
2260
|
+
}
|
|
2261
|
+
<button [class]="cls('formButtonPrimary', 'atlas-button')" type="button" data-atlas="recovery-codes-saved" [disabled]="busy" (click)="saved.emit()">
|
|
2262
|
+
{{ t('mfa.recoveryCodesSaved') }}
|
|
2263
|
+
</button>
|
|
2264
|
+
</div>
|
|
2265
|
+
`,
|
|
2266
|
+
}]
|
|
2267
|
+
}], propDecorators: { codes: [{
|
|
2268
|
+
type: Input
|
|
2269
|
+
}], busy: [{
|
|
2270
|
+
type: Input
|
|
2271
|
+
}], saved: [{
|
|
2272
|
+
type: Output
|
|
2273
|
+
}] } });
|
|
2274
|
+
|
|
2275
|
+
/**
|
|
2276
|
+
* The server-driven sign-in / sign-up engine, shared by `<atlas-sign-in>` and
|
|
2277
|
+
* `<atlas-sign-up>` — the Angular counterpart of React's `useFlow` hook.
|
|
2278
|
+
*
|
|
2279
|
+
* It owns no decision about what comes next: every status comes back from the
|
|
2280
|
+
* server via `advance`, and this controller only posts whatever the current step
|
|
2281
|
+
* needs and re-renders the returned status. State lives in a signal so templates
|
|
2282
|
+
* update whether the app runs zoned or zoneless.
|
|
2283
|
+
*
|
|
2284
|
+
* `<atlas-sign-up>` starts the attempt with {@link create}
|
|
2285
|
+
* (`POST /v1/client/sign_ups`); the emailed-code step then runs through the same
|
|
2286
|
+
* {@link submit}. Both flows draw the instance's captcha (from
|
|
2287
|
+
* {@link loadSettings}) when it is required.
|
|
2288
|
+
*/
|
|
2289
|
+
class AtlasFlowController {
|
|
2290
|
+
client;
|
|
2291
|
+
reload;
|
|
2292
|
+
t;
|
|
2293
|
+
state;
|
|
2294
|
+
/**
|
|
2295
|
+
* The instance's public sign-in / sign-up settings (captcha, sign-up fields,
|
|
2296
|
+
* legal consent) from `GET /v1/appearance`; null until {@link loadSettings}
|
|
2297
|
+
* resolves, or when the server could not be reached.
|
|
2298
|
+
*/
|
|
2299
|
+
settings = signal(null);
|
|
2300
|
+
/**
|
|
2301
|
+
* The method this browser last signed in with (`password`, `oauth_github`, …)
|
|
2302
|
+
* for the "Last used" hint; null when none, or the instance turned it off.
|
|
2303
|
+
*/
|
|
2304
|
+
lastMethod = signal(null);
|
|
2305
|
+
/** The sign-in captcha (identifier step / `needs_captcha` challenge). */
|
|
2306
|
+
captcha = createCaptchaController(undefined, {
|
|
2307
|
+
// The Atlas captcha's words follow the form's catalog (read when drawn).
|
|
2308
|
+
atlasLabels: () => atlasCaptchaLabels(this.t),
|
|
2309
|
+
});
|
|
2310
|
+
/** The sign-up captcha (the create-account step). */
|
|
2311
|
+
signUpCaptcha = createCaptchaController(undefined, {
|
|
2312
|
+
atlasLabels: () => atlasCaptchaLabels(this.t),
|
|
2313
|
+
});
|
|
2314
|
+
/** Whether {@link captcha} currently draws a widget (for templates). */
|
|
2315
|
+
captchaDrawn = signal(false);
|
|
2316
|
+
/** Whether {@link signUpCaptcha} currently draws a widget (for templates). */
|
|
2317
|
+
signUpCaptchaDrawn = signal(false);
|
|
2318
|
+
/**
|
|
2319
|
+
* The first factor the server was asked to send ({@link prepare}) on the
|
|
2320
|
+
* current `needs_first_factor` step — the form then collects its code. Null
|
|
2321
|
+
* otherwise.
|
|
2322
|
+
*/
|
|
2323
|
+
preparedFactor = signal(null);
|
|
2324
|
+
/**
|
|
2325
|
+
* "Email me a code instead" at the second factor ({@link sendEmailSecondFactor}):
|
|
2326
|
+
* in flight, where the code went, or why it was not sent. Null until asked,
|
|
2327
|
+
* and again once the flow leaves that step.
|
|
2328
|
+
*/
|
|
2329
|
+
emailSecondFactor = signal(null);
|
|
2330
|
+
/** "Text me a code instead" at the second factor ({@link sendSmsSecondFactor}): as {@link emailSecondFactor}. */
|
|
2331
|
+
smsSecondFactor = signal(null);
|
|
2332
|
+
/**
|
|
2333
|
+
* The MFA enrolment step (`needs_mfa_enrollment`): the authenticator app's
|
|
2334
|
+
* setup (QR code, setup key) or a texted code. Started and reset with the flow.
|
|
2335
|
+
*/
|
|
2336
|
+
enrollment;
|
|
2337
|
+
/** {@link enrollment}'s state, for templates. */
|
|
2338
|
+
enrollmentState;
|
|
2339
|
+
/**
|
|
2340
|
+
* The instance's policies (terms, privacy, …) for the create-account form:
|
|
2341
|
+
* {@link create} sends their acceptances and reloads them after a refusal.
|
|
2342
|
+
*/
|
|
2343
|
+
policies = new PolicyAcceptanceController([]);
|
|
1416
2344
|
pollTimer = null;
|
|
1417
2345
|
identifier = '';
|
|
1418
2346
|
constructor(client, reload, pending,
|
|
@@ -1424,7 +2352,23 @@ class AtlasFlowController {
|
|
|
1424
2352
|
// Seeded from a redirect that came back mid-flow (e.g. OAuth needing a second
|
|
1425
2353
|
// factor) so the user resumes at the demanded step instead of starting over.
|
|
1426
2354
|
this.state = signal(pending ? flowFromPending(pending) : initialFlow);
|
|
2355
|
+
this.enrollment = createMfaEnrollment(this.client, () => this.mfaChoices());
|
|
2356
|
+
this.enrollmentState = signal(this.enrollment.state);
|
|
2357
|
+
this.enrollment.subscribe((next) => this.enrollmentState.set(next));
|
|
1427
2358
|
this.syncCaptcha();
|
|
2359
|
+
this.syncEnrollment();
|
|
2360
|
+
}
|
|
2361
|
+
/** What the app lets the MFA steps offer (from {@link settings}). */
|
|
2362
|
+
mfaChoices() {
|
|
2363
|
+
return mfaChoicesFrom(this.settings()?.account);
|
|
2364
|
+
}
|
|
2365
|
+
/** Start the enrolment step for its attempt, or reset it once the flow leaves. */
|
|
2366
|
+
syncEnrollment() {
|
|
2367
|
+
const attempt = this.state().attempt;
|
|
2368
|
+
if (attempt?.status === 'needs_mfa_enrollment' && attempt.id)
|
|
2369
|
+
this.enrollment.start(attempt.id);
|
|
2370
|
+
else
|
|
2371
|
+
this.enrollment.reset();
|
|
1428
2372
|
}
|
|
1429
2373
|
/** Fetch the instance's public settings and draw whatever captcha they require. */
|
|
1430
2374
|
async loadSettings() {
|
|
@@ -1432,6 +2376,8 @@ class AtlasFlowController {
|
|
|
1432
2376
|
if (loaded) {
|
|
1433
2377
|
this.settings.set(loaded);
|
|
1434
2378
|
this.syncCaptcha();
|
|
2379
|
+
if (loaded.account.lastSignInMethod)
|
|
2380
|
+
this.lastMethod.set(await fetchLastSignInMethod(this.client));
|
|
1435
2381
|
}
|
|
1436
2382
|
}
|
|
1437
2383
|
/**
|
|
@@ -1486,11 +2432,18 @@ class AtlasFlowController {
|
|
|
1486
2432
|
this.signUpCaptchaDrawn.set(this.signUpCaptcha.needed);
|
|
1487
2433
|
}
|
|
1488
2434
|
setState(next) {
|
|
2435
|
+
const previousId = this.state().attempt?.id ?? null;
|
|
1489
2436
|
this.state.set(next);
|
|
1490
2437
|
// A prepared factor belongs to the first-factor step it was sent for.
|
|
1491
2438
|
if (next.attempt?.status !== 'needs_first_factor')
|
|
1492
2439
|
this.preparedFactor.set(null);
|
|
2440
|
+
// An emailed second-factor code belongs to the second-factor step of this attempt.
|
|
2441
|
+
if (next.attempt?.status !== 'needs_second_factor' || (next.attempt?.id ?? null) !== previousId) {
|
|
2442
|
+
this.emailSecondFactor.set(null);
|
|
2443
|
+
this.smsSecondFactor.set(null);
|
|
2444
|
+
}
|
|
1493
2445
|
this.syncCaptcha();
|
|
2446
|
+
this.syncEnrollment();
|
|
1494
2447
|
this.autoSend();
|
|
1495
2448
|
}
|
|
1496
2449
|
autoSentFor = null;
|
|
@@ -1524,6 +2477,17 @@ class AtlasFlowController {
|
|
|
1524
2477
|
this.identifier = out.identifier;
|
|
1525
2478
|
await this.finish(out.state);
|
|
1526
2479
|
}
|
|
2480
|
+
/** Load the sign-up's policies into {@link policies} (`<atlas-sign-up>` calls it on init). */
|
|
2481
|
+
async loadPolicies(locale) {
|
|
2482
|
+
this.policies.setPolicies(await loadPolicies(this.client, { locale }));
|
|
2483
|
+
}
|
|
2484
|
+
/** The user declined a policy a sign-in was waiting on: cancel the attempt and say why. */
|
|
2485
|
+
declinePolicy(title) {
|
|
2486
|
+
this.state.set({
|
|
2487
|
+
...initialFlow,
|
|
2488
|
+
errors: [{ code: 'POLICY_DECLINED', message: this.t('policy.declined').replace('{policy}', title) }],
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
1527
2491
|
/**
|
|
1528
2492
|
* Create an account (`POST /v1/client/sign_ups`) from a create-account form's
|
|
1529
2493
|
* values — email, password and any configured sign-up fields — carrying the
|
|
@@ -1536,10 +2500,76 @@ class AtlasFlowController {
|
|
|
1536
2500
|
consent,
|
|
1537
2501
|
captcha: this.signUpCaptcha,
|
|
1538
2502
|
t: this.t,
|
|
2503
|
+
policies: this.policies,
|
|
1539
2504
|
});
|
|
1540
2505
|
await this.finish(next);
|
|
1541
2506
|
}
|
|
2507
|
+
/**
|
|
2508
|
+
* "Email me a code instead" at the second factor (the instance's
|
|
2509
|
+
* `account.emailSecondFactor`): mails a code to the account's verified email;
|
|
2510
|
+
* it is then posted with {@link submit} like any 2FA code.
|
|
2511
|
+
*/
|
|
2512
|
+
async sendEmailSecondFactor() {
|
|
2513
|
+
await this.sendSecondFactorCode('email_code', this.emailSecondFactor);
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* "Text me a code instead" at the second factor (where the app offers texted
|
|
2517
|
+
* codes, {@link mfaChoices}`().sms`): texts a code to the account's enrolled
|
|
2518
|
+
* phone; it is then posted with {@link submit} like any 2FA code.
|
|
2519
|
+
*/
|
|
2520
|
+
async sendSmsSecondFactor() {
|
|
2521
|
+
await this.sendSecondFactorCode('sms', this.smsSecondFactor);
|
|
2522
|
+
}
|
|
2523
|
+
async sendSecondFactorCode(strategy, out) {
|
|
2524
|
+
const id = this.state().attempt?.id;
|
|
2525
|
+
if (!id)
|
|
2526
|
+
return;
|
|
2527
|
+
out.set({ busy: true });
|
|
2528
|
+
try {
|
|
2529
|
+
const { sentTo } = await prepareSecondFactorCode(this.client, id, strategy);
|
|
2530
|
+
out.set({ sentTo });
|
|
2531
|
+
}
|
|
2532
|
+
catch (error) {
|
|
2533
|
+
out.set({ error: error instanceof Error ? error.message : this.t('error.generic') });
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
/** Resend a sign-up's emailed code / link (the earlier one stops working). */
|
|
2537
|
+
async resendSignUp() {
|
|
2538
|
+
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
2539
|
+
this.state.set(await resendSignUpVerification(this.client, this.state()));
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Start a passkey-only sign-up (`POST /v1/client/sign_ups/passkey`) from the
|
|
2543
|
+
* create-account form's values — no password. Offered when the instance's
|
|
2544
|
+
* `account.passkeySignUp` is set.
|
|
2545
|
+
*/
|
|
2546
|
+
async createWithPasskey(values, consent = true) {
|
|
2547
|
+
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
2548
|
+
await this.finish(await createPasskeySignUp(this.client, values, { consent, captcha: this.signUpCaptcha, t: this.t, policies: this.policies }));
|
|
2549
|
+
}
|
|
2550
|
+
/** The passkey-only sign-up's last step: create the passkey, and with it the account. */
|
|
2551
|
+
async createPasskey() {
|
|
2552
|
+
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
2553
|
+
await this.finish(await finishPasskeySignUp(this.client, this.state(), { t: this.t }));
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* The recovery codes an MFA enrolment just completed the sign-in with, shown
|
|
2557
|
+
* once before it finishes ({@link saveRecoveryCodes}); null otherwise. They
|
|
2558
|
+
* live only in this controller's memory.
|
|
2559
|
+
*/
|
|
2560
|
+
recoveryCodes() {
|
|
2561
|
+
return pendingRecoveryCodes(this.state());
|
|
2562
|
+
}
|
|
2563
|
+
/** "I've saved my codes": forget the recovery codes and finish the sign-in. */
|
|
2564
|
+
async saveRecoveryCodes() {
|
|
2565
|
+
await this.finish(acknowledgeRecoveryCodes(this.state()));
|
|
2566
|
+
}
|
|
1542
2567
|
async finish(next) {
|
|
2568
|
+
// An enrolment's recovery codes are shown first; `saveRecoveryCodes` finishes.
|
|
2569
|
+
if (pendingRecoveryCodes(next)) {
|
|
2570
|
+
this.setState({ ...next, busy: false });
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
1543
2573
|
if (next.attempt?.status === 'complete') {
|
|
1544
2574
|
// Stays busy until this browser actually holds the session: the completion
|
|
1545
2575
|
// alone says nothing about whether the cookie was kept.
|
|
@@ -1577,6 +2607,9 @@ class AtlasFlowController {
|
|
|
1577
2607
|
void (async () => {
|
|
1578
2608
|
const { state: next, ticket } = await pollAttempt(this.client, this.state());
|
|
1579
2609
|
this.setState(next);
|
|
2610
|
+
// e.g. a passkey sign-up whose email was just proven: only this tab can go on.
|
|
2611
|
+
if (!ticket && !shouldKeepPolling(next))
|
|
2612
|
+
this.stopPolling();
|
|
1580
2613
|
if (!ticket || !next.attempt)
|
|
1581
2614
|
return;
|
|
1582
2615
|
// The ticket is exchanged by a normal request, so no session token ever
|
|
@@ -1628,6 +2661,12 @@ class SignInComponent {
|
|
|
1628
2661
|
t(key) {
|
|
1629
2662
|
return translate(this.atlas.localization, key);
|
|
1630
2663
|
}
|
|
2664
|
+
/** "Last used: Password" on the identifier step, when the instance shows the hint. */
|
|
2665
|
+
lastUsedLabel() {
|
|
2666
|
+
if (this.flow.state().attempt)
|
|
2667
|
+
return null;
|
|
2668
|
+
return lastUsedMethodLabel(this.flow.lastMethod(), (key) => this.t(key));
|
|
2669
|
+
}
|
|
1631
2670
|
signUpHref() {
|
|
1632
2671
|
if (this.signUpUrl === null)
|
|
1633
2672
|
return null;
|
|
@@ -1661,6 +2700,14 @@ class SignInComponent {
|
|
|
1661
2700
|
}
|
|
1662
2701
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignInComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1663
2702
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SignInComponent, isStandalone: true, selector: "atlas-sign-in", inputs: { afterUrl: "afterUrl", signUpUrl: "signUpUrl" }, ngImport: i0, template: `
|
|
2703
|
+
@if (lastUsedLabel(); as label) {
|
|
2704
|
+
<p class="atlas-text atlas-last-used-hint" data-atlas="last-used" [attr.data-atlas-method]="flow.lastMethod()">
|
|
2705
|
+
{{ t('signIn.lastUsed') }}: {{ label }}
|
|
2706
|
+
</p>
|
|
2707
|
+
}
|
|
2708
|
+
@if (flow.recoveryCodes(); as codes) {
|
|
2709
|
+
<atlas-recovery-codes [codes]="codes" [busy]="flow.state().busy" (saved)="flow.saveRecoveryCodes()"></atlas-recovery-codes>
|
|
2710
|
+
} @else {
|
|
1664
2711
|
<atlas-attempt-flow
|
|
1665
2712
|
[attempt]="flow.state().attempt"
|
|
1666
2713
|
[busy]="flow.state().busy"
|
|
@@ -1670,15 +2717,25 @@ class SignInComponent {
|
|
|
1670
2717
|
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
1671
2718
|
[firstFactors]="flow.firstFactors()"
|
|
1672
2719
|
[preparedFactor]="flow.preparedFactor()"
|
|
2720
|
+
[offerEmailSecondFactor]="flow.settings()?.account?.emailSecondFactor === true"
|
|
2721
|
+
[emailedSecondFactor]="flow.emailSecondFactor()"
|
|
1673
2722
|
(submitFlow)="flow.submit($event)"
|
|
1674
2723
|
(chooseFactor)="flow.prepare($event)"
|
|
2724
|
+
[offerSmsSecondFactor]="flow.settings() !== null && flow.mfaChoices().sms"
|
|
2725
|
+
[textedSecondFactor]="flow.smsSecondFactor()"
|
|
2726
|
+
[enrollment]="flow.enrollment"
|
|
2727
|
+
[enrollmentState]="flow.enrollmentState()"
|
|
2728
|
+
(emailSecondFactor)="flow.sendEmailSecondFactor()"
|
|
2729
|
+
(smsSecondFactor)="flow.sendSmsSecondFactor()"
|
|
2730
|
+
(policyDeclined)="flow.declinePolicy($event)"
|
|
1675
2731
|
></atlas-attempt-flow>
|
|
2732
|
+
}
|
|
1676
2733
|
@if (flow.state().attempt?.status !== 'complete' && signUpHref(); as href) {
|
|
1677
2734
|
<p class="atlas-footer" data-atlas-sign-up-prompt="">
|
|
1678
2735
|
{{ t('signIn.noAccount') }} <a class="atlas-link" [href]="href">{{ t('signIn.signUpLink') }}</a>
|
|
1679
2736
|
</p>
|
|
1680
2737
|
}
|
|
1681
|
-
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "sessionKept", "titleKey", "captcha", "firstFactors", "preparedFactor"], outputs: ["submitFlow", "chooseFactor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2738
|
+
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "sessionKept", "titleKey", "captcha", "firstFactors", "preparedFactor", "offerEmailSecondFactor", "emailedSecondFactor", "offerSmsSecondFactor", "textedSecondFactor", "enrollment", "enrollmentState", "emailVerification"], outputs: ["submitFlow", "chooseFactor", "emailSecondFactor", "smsSecondFactor", "resend", "policyDeclined"] }, { kind: "component", type: RecoveryCodesComponent, selector: "atlas-recovery-codes", inputs: ["codes", "busy"], outputs: ["saved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1682
2739
|
}
|
|
1683
2740
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignInComponent, decorators: [{
|
|
1684
2741
|
type: Component,
|
|
@@ -1686,8 +2743,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1686
2743
|
selector: 'atlas-sign-in',
|
|
1687
2744
|
standalone: true,
|
|
1688
2745
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1689
|
-
imports: [AttemptFlowComponent],
|
|
2746
|
+
imports: [AttemptFlowComponent, RecoveryCodesComponent],
|
|
1690
2747
|
template: `
|
|
2748
|
+
@if (lastUsedLabel(); as label) {
|
|
2749
|
+
<p class="atlas-text atlas-last-used-hint" data-atlas="last-used" [attr.data-atlas-method]="flow.lastMethod()">
|
|
2750
|
+
{{ t('signIn.lastUsed') }}: {{ label }}
|
|
2751
|
+
</p>
|
|
2752
|
+
}
|
|
2753
|
+
@if (flow.recoveryCodes(); as codes) {
|
|
2754
|
+
<atlas-recovery-codes [codes]="codes" [busy]="flow.state().busy" (saved)="flow.saveRecoveryCodes()"></atlas-recovery-codes>
|
|
2755
|
+
} @else {
|
|
1691
2756
|
<atlas-attempt-flow
|
|
1692
2757
|
[attempt]="flow.state().attempt"
|
|
1693
2758
|
[busy]="flow.state().busy"
|
|
@@ -1697,9 +2762,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1697
2762
|
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
1698
2763
|
[firstFactors]="flow.firstFactors()"
|
|
1699
2764
|
[preparedFactor]="flow.preparedFactor()"
|
|
2765
|
+
[offerEmailSecondFactor]="flow.settings()?.account?.emailSecondFactor === true"
|
|
2766
|
+
[emailedSecondFactor]="flow.emailSecondFactor()"
|
|
1700
2767
|
(submitFlow)="flow.submit($event)"
|
|
1701
2768
|
(chooseFactor)="flow.prepare($event)"
|
|
2769
|
+
[offerSmsSecondFactor]="flow.settings() !== null && flow.mfaChoices().sms"
|
|
2770
|
+
[textedSecondFactor]="flow.smsSecondFactor()"
|
|
2771
|
+
[enrollment]="flow.enrollment"
|
|
2772
|
+
[enrollmentState]="flow.enrollmentState()"
|
|
2773
|
+
(emailSecondFactor)="flow.sendEmailSecondFactor()"
|
|
2774
|
+
(smsSecondFactor)="flow.sendSmsSecondFactor()"
|
|
2775
|
+
(policyDeclined)="flow.declinePolicy($event)"
|
|
1702
2776
|
></atlas-attempt-flow>
|
|
2777
|
+
}
|
|
1703
2778
|
@if (flow.state().attempt?.status !== 'complete' && signUpHref(); as href) {
|
|
1704
2779
|
<p class="atlas-footer" data-atlas-sign-up-prompt="">
|
|
1705
2780
|
{{ t('signIn.noAccount') }} <a class="atlas-link" [href]="href">{{ t('signIn.signUpLink') }}</a>
|
|
@@ -1723,14 +2798,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1723
2798
|
*/
|
|
1724
2799
|
class SignUpFormComponent {
|
|
1725
2800
|
atlas = inject(AtlasService);
|
|
2801
|
+
cdr = inject(ChangeDetectorRef);
|
|
2802
|
+
unwatch = null;
|
|
1726
2803
|
settings = null;
|
|
1727
2804
|
busy = false;
|
|
1728
2805
|
errors = [];
|
|
1729
2806
|
captcha = null;
|
|
2807
|
+
/** Passkey-only sign-up: no password field; the email per the instance's mode. */
|
|
2808
|
+
passkey = null;
|
|
2809
|
+
/** Whether the password / passkey toggle is shown (both are offered). */
|
|
2810
|
+
switchable = false;
|
|
2811
|
+
/** The instance's policies (terms, privacy, …); they replace the legacy consent row. */
|
|
2812
|
+
policies = null;
|
|
2813
|
+
/** The user asked for the other sign-up method. */
|
|
2814
|
+
switchMode = new EventEmitter();
|
|
1730
2815
|
/** Emits the typed values and the consent tick when the form is submitted. */
|
|
1731
2816
|
submitSignUp = new EventEmitter();
|
|
1732
2817
|
values = {};
|
|
1733
2818
|
agreed = false;
|
|
2819
|
+
declined = null;
|
|
2820
|
+
ngOnChanges(changes) {
|
|
2821
|
+
if (!changes['policies'])
|
|
2822
|
+
return;
|
|
2823
|
+
this.unwatch?.();
|
|
2824
|
+
this.unwatch = this.policies?.subscribe(() => this.cdr.markForCheck()) ?? null;
|
|
2825
|
+
}
|
|
2826
|
+
ngOnDestroy() {
|
|
2827
|
+
this.unwatch?.();
|
|
2828
|
+
}
|
|
2829
|
+
hasPolicies() {
|
|
2830
|
+
return !!this.policies && this.policies.policies.length > 0;
|
|
2831
|
+
}
|
|
1734
2832
|
t(key, vars) {
|
|
1735
2833
|
return translate(this.atlas.localization, key, vars);
|
|
1736
2834
|
}
|
|
@@ -1748,8 +2846,17 @@ class SignUpFormComponent {
|
|
|
1748
2846
|
}
|
|
1749
2847
|
/** Email + password sign-up cannot complete: the app cannot send the verification email. */
|
|
1750
2848
|
emailUnavailable() {
|
|
2849
|
+
// A passkey sign-up that collects no email (or an unproven one) sends no mail.
|
|
2850
|
+
if (this.passkey && this.passkey.email !== 'required')
|
|
2851
|
+
return false;
|
|
1751
2852
|
return this.settings?.emailDelivery?.passwordSignUp === false;
|
|
1752
2853
|
}
|
|
2854
|
+
showEmail() {
|
|
2855
|
+
return !this.passkey || this.passkey.email !== 'off';
|
|
2856
|
+
}
|
|
2857
|
+
emailRequired() {
|
|
2858
|
+
return !this.passkey || this.passkey.email === 'required';
|
|
2859
|
+
}
|
|
1753
2860
|
fields() {
|
|
1754
2861
|
return this.settings?.signUpFields ?? [];
|
|
1755
2862
|
}
|
|
@@ -1774,10 +2881,15 @@ class SignUpFormComponent {
|
|
|
1774
2881
|
this.setValue(f.key, (on ? [...current, option] : current.filter((p) => p !== option)).join(', '));
|
|
1775
2882
|
}
|
|
1776
2883
|
showLegal() {
|
|
2884
|
+
// Instance policies replace the legacy single "I agree to the Terms" row.
|
|
2885
|
+
if (this.hasPolicies())
|
|
2886
|
+
return false;
|
|
1777
2887
|
const legal = this.settings?.legal;
|
|
1778
2888
|
return !!legal && (!!legal.termsUrl || !!legal.privacyUrl);
|
|
1779
2889
|
}
|
|
1780
2890
|
consentOk() {
|
|
2891
|
+
if (this.hasPolicies())
|
|
2892
|
+
return this.policies.canSubmit();
|
|
1781
2893
|
return !this.settings?.legal?.required || this.agreed;
|
|
1782
2894
|
}
|
|
1783
2895
|
formErrors() {
|
|
@@ -1790,13 +2902,27 @@ class SignUpFormComponent {
|
|
|
1790
2902
|
event.preventDefault();
|
|
1791
2903
|
if (this.emailUnavailable() || !this.consentOk())
|
|
1792
2904
|
return;
|
|
2905
|
+
this.declined = null;
|
|
2906
|
+
if (this.hasPolicies()) {
|
|
2907
|
+
// Scroll-mode policies are read in their unskippable dialog first.
|
|
2908
|
+
const values = { ...this.values };
|
|
2909
|
+
void confirmPolicies(this.policies, { t: policyUi(this.atlas).t }).then((res) => {
|
|
2910
|
+
if (res.ok)
|
|
2911
|
+
this.submitSignUp.emit({ values, consent: true });
|
|
2912
|
+
else {
|
|
2913
|
+
this.declined = res.declined;
|
|
2914
|
+
this.cdr.markForCheck();
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
1793
2919
|
this.submitSignUp.emit({
|
|
1794
2920
|
values: { ...this.values },
|
|
1795
2921
|
consent: this.settings?.legal?.required ? this.agreed : true,
|
|
1796
2922
|
});
|
|
1797
2923
|
}
|
|
1798
2924
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1799
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SignUpFormComponent, isStandalone: true, selector: "atlas-sign-up-form", inputs: { settings: "settings", busy: "busy", errors: "errors", captcha: "captcha" }, outputs: { submitSignUp: "submitSignUp" }, ngImport: i0, template: `
|
|
2925
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SignUpFormComponent, isStandalone: true, selector: "atlas-sign-up-form", inputs: { settings: "settings", busy: "busy", errors: "errors", captcha: "captcha", passkey: "passkey", switchable: "switchable", policies: "policies" }, outputs: { switchMode: "switchMode", submitSignUp: "submitSignUp" }, usesOnChanges: true, ngImport: i0, template: `
|
|
1800
2926
|
<div [class]="cls('card', 'atlas-card')">
|
|
1801
2927
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
1802
2928
|
|
|
@@ -1885,14 +3011,17 @@ class SignUpFormComponent {
|
|
|
1885
3011
|
}
|
|
1886
3012
|
}
|
|
1887
3013
|
|
|
3014
|
+
@if (showEmail()) {
|
|
1888
3015
|
<label [class]="cls('formField', 'atlas-field')">
|
|
1889
|
-
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{
|
|
3016
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{
|
|
3017
|
+
emailRequired() ? t('signUp.emailLabel') : t('signUp.optionalField', { label: t('signUp.emailLabel') })
|
|
3018
|
+
}}</span>
|
|
1890
3019
|
<input
|
|
1891
3020
|
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1892
3021
|
type="email"
|
|
1893
3022
|
name="email"
|
|
1894
3023
|
autocomplete="email"
|
|
1895
|
-
required
|
|
3024
|
+
[required]="emailRequired()"
|
|
1896
3025
|
[value]="values['email'] ?? ''"
|
|
1897
3026
|
(input)="setValue('email', $any($event.target).value)"
|
|
1898
3027
|
/>
|
|
@@ -1900,7 +3029,11 @@ class SignUpFormComponent {
|
|
|
1900
3029
|
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1901
3030
|
}
|
|
1902
3031
|
</label>
|
|
3032
|
+
}
|
|
1903
3033
|
|
|
3034
|
+
@if (passkey) {
|
|
3035
|
+
<p class="atlas-text">{{ t('signUp.passkeyIntro') }}</p>
|
|
3036
|
+
} @else {
|
|
1904
3037
|
<label [class]="cls('formField', 'atlas-field')">
|
|
1905
3038
|
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.passwordLabel') }}</span>
|
|
1906
3039
|
<input
|
|
@@ -1916,6 +3049,16 @@ class SignUpFormComponent {
|
|
|
1916
3049
|
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1917
3050
|
}
|
|
1918
3051
|
</label>
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
@if (hasPolicies()) {
|
|
3055
|
+
<div [atlasPolicies]="policies"></div>
|
|
3056
|
+
@if (declined) {
|
|
3057
|
+
<span [class]="cls('formFieldError', 'atlas-error')" role="alert" data-atlas-policy-declined="">
|
|
3058
|
+
{{ t('policy.declined', { policy: declined }) }}
|
|
3059
|
+
</span>
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
1919
3062
|
|
|
1920
3063
|
@if (showLegal()) {
|
|
1921
3064
|
<label [class]="cls('formField', 'atlas-consent')">
|
|
@@ -1956,10 +3099,26 @@ class SignUpFormComponent {
|
|
|
1956
3099
|
>
|
|
1957
3100
|
{{ t('signUp.submit') }}
|
|
1958
3101
|
</button>
|
|
3102
|
+
@if (hasPolicies()) {
|
|
3103
|
+
<p atlasPolicyKind="clickwrap" [atlasPolicies]="policies"></p>
|
|
3104
|
+
}
|
|
3105
|
+
@if (switchable) {
|
|
3106
|
+
<div class="atlas-alternatives">
|
|
3107
|
+
<button
|
|
3108
|
+
type="button"
|
|
3109
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
3110
|
+
[attr.data-atlas-sign-up-mode]="passkey ? 'password' : 'passkey'"
|
|
3111
|
+
[disabled]="busy"
|
|
3112
|
+
(click)="switchMode.emit()"
|
|
3113
|
+
>
|
|
3114
|
+
{{ t(passkey ? 'signUp.passwordOption' : 'signUp.passkeyOption') }}
|
|
3115
|
+
</button>
|
|
3116
|
+
</div>
|
|
3117
|
+
}
|
|
1959
3118
|
</form>
|
|
1960
3119
|
}
|
|
1961
3120
|
</div>
|
|
1962
|
-
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3121
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }, { kind: "directive", type: AtlasPoliciesDirective, selector: "[atlasPolicies]", inputs: ["atlasPolicies", "atlasPolicyKind"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1963
3122
|
}
|
|
1964
3123
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpFormComponent, decorators: [{
|
|
1965
3124
|
type: Component,
|
|
@@ -1967,7 +3126,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1967
3126
|
selector: 'atlas-sign-up-form',
|
|
1968
3127
|
standalone: true,
|
|
1969
3128
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1970
|
-
imports: [AtlasCaptchaSlotDirective],
|
|
3129
|
+
imports: [AtlasCaptchaSlotDirective, AtlasPoliciesDirective],
|
|
1971
3130
|
template: `
|
|
1972
3131
|
<div [class]="cls('card', 'atlas-card')">
|
|
1973
3132
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
@@ -2057,14 +3216,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2057
3216
|
}
|
|
2058
3217
|
}
|
|
2059
3218
|
|
|
3219
|
+
@if (showEmail()) {
|
|
2060
3220
|
<label [class]="cls('formField', 'atlas-field')">
|
|
2061
|
-
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{
|
|
3221
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{
|
|
3222
|
+
emailRequired() ? t('signUp.emailLabel') : t('signUp.optionalField', { label: t('signUp.emailLabel') })
|
|
3223
|
+
}}</span>
|
|
2062
3224
|
<input
|
|
2063
3225
|
[class]="cls('formFieldInput', 'atlas-input')"
|
|
2064
3226
|
type="email"
|
|
2065
3227
|
name="email"
|
|
2066
3228
|
autocomplete="email"
|
|
2067
|
-
required
|
|
3229
|
+
[required]="emailRequired()"
|
|
2068
3230
|
[value]="values['email'] ?? ''"
|
|
2069
3231
|
(input)="setValue('email', $any($event.target).value)"
|
|
2070
3232
|
/>
|
|
@@ -2072,7 +3234,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2072
3234
|
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
2073
3235
|
}
|
|
2074
3236
|
</label>
|
|
3237
|
+
}
|
|
2075
3238
|
|
|
3239
|
+
@if (passkey) {
|
|
3240
|
+
<p class="atlas-text">{{ t('signUp.passkeyIntro') }}</p>
|
|
3241
|
+
} @else {
|
|
2076
3242
|
<label [class]="cls('formField', 'atlas-field')">
|
|
2077
3243
|
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.passwordLabel') }}</span>
|
|
2078
3244
|
<input
|
|
@@ -2088,6 +3254,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2088
3254
|
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
2089
3255
|
}
|
|
2090
3256
|
</label>
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
@if (hasPolicies()) {
|
|
3260
|
+
<div [atlasPolicies]="policies"></div>
|
|
3261
|
+
@if (declined) {
|
|
3262
|
+
<span [class]="cls('formFieldError', 'atlas-error')" role="alert" data-atlas-policy-declined="">
|
|
3263
|
+
{{ t('policy.declined', { policy: declined }) }}
|
|
3264
|
+
</span>
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
2091
3267
|
|
|
2092
3268
|
@if (showLegal()) {
|
|
2093
3269
|
<label [class]="cls('formField', 'atlas-consent')">
|
|
@@ -2128,6 +3304,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2128
3304
|
>
|
|
2129
3305
|
{{ t('signUp.submit') }}
|
|
2130
3306
|
</button>
|
|
3307
|
+
@if (hasPolicies()) {
|
|
3308
|
+
<p atlasPolicyKind="clickwrap" [atlasPolicies]="policies"></p>
|
|
3309
|
+
}
|
|
3310
|
+
@if (switchable) {
|
|
3311
|
+
<div class="atlas-alternatives">
|
|
3312
|
+
<button
|
|
3313
|
+
type="button"
|
|
3314
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
3315
|
+
[attr.data-atlas-sign-up-mode]="passkey ? 'password' : 'passkey'"
|
|
3316
|
+
[disabled]="busy"
|
|
3317
|
+
(click)="switchMode.emit()"
|
|
3318
|
+
>
|
|
3319
|
+
{{ t(passkey ? 'signUp.passwordOption' : 'signUp.passkeyOption') }}
|
|
3320
|
+
</button>
|
|
3321
|
+
</div>
|
|
3322
|
+
}
|
|
2131
3323
|
</form>
|
|
2132
3324
|
}
|
|
2133
3325
|
</div>
|
|
@@ -2141,6 +3333,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2141
3333
|
type: Input
|
|
2142
3334
|
}], captcha: [{
|
|
2143
3335
|
type: Input
|
|
3336
|
+
}], passkey: [{
|
|
3337
|
+
type: Input
|
|
3338
|
+
}], switchable: [{
|
|
3339
|
+
type: Input
|
|
3340
|
+
}], policies: [{
|
|
3341
|
+
type: Input
|
|
3342
|
+
}], switchMode: [{
|
|
3343
|
+
type: Output
|
|
2144
3344
|
}], submitSignUp: [{
|
|
2145
3345
|
type: Output
|
|
2146
3346
|
}] } });
|
|
@@ -2162,24 +3362,70 @@ class SignUpComponent {
|
|
|
2162
3362
|
/** Where to send the user once the flow completes. Reserved for host routing. */
|
|
2163
3363
|
afterUrl;
|
|
2164
3364
|
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(), this.atlas.pendingAttempt, (key) => translate(this.atlas.localization, key));
|
|
3365
|
+
/** Password or passkey-only sign-up (the latter when the instance offers it). */
|
|
3366
|
+
mode = signal('password');
|
|
3367
|
+
canPasskey() {
|
|
3368
|
+
return !!this.flow.settings()?.account.passkeySignUp && passkeysSupported();
|
|
3369
|
+
}
|
|
3370
|
+
passkeyForm() {
|
|
3371
|
+
const offer = this.flow.settings()?.account.passkeySignUp ?? null;
|
|
3372
|
+
return this.mode() === 'passkey' && offer && this.canPasskey() ? { email: offer.email } : null;
|
|
3373
|
+
}
|
|
3374
|
+
toggleMode() {
|
|
3375
|
+
this.flow.state.update((current) => ({ ...current, errors: [] }));
|
|
3376
|
+
this.mode.set(this.passkeyForm() ? 'password' : 'passkey');
|
|
3377
|
+
}
|
|
3378
|
+
awaitingPasskey() {
|
|
3379
|
+
return awaitingPasskey(this.flow.state().attempt);
|
|
3380
|
+
}
|
|
3381
|
+
t(key) {
|
|
3382
|
+
return translate(this.atlas.localization, key);
|
|
3383
|
+
}
|
|
3384
|
+
cls(key, base) {
|
|
3385
|
+
return classFor(key, this.atlas.appearance, base);
|
|
3386
|
+
}
|
|
3387
|
+
errorText(error) {
|
|
3388
|
+
const layer = this.atlas.errorLocalization;
|
|
3389
|
+
return localizedErrorMessage(error, layer ?? this.atlas.localization, this.atlas.localization);
|
|
3390
|
+
}
|
|
2165
3391
|
ngOnInit() {
|
|
2166
3392
|
// §5 the instance's sign-up fields, consent policy and captcha settings.
|
|
2167
|
-
if (typeof window !== 'undefined')
|
|
3393
|
+
if (typeof window !== 'undefined') {
|
|
2168
3394
|
void this.flow.loadSettings();
|
|
3395
|
+
void this.flow.loadPolicies(this.atlas.locale ?? undefined);
|
|
3396
|
+
}
|
|
2169
3397
|
}
|
|
2170
3398
|
ngOnDestroy() {
|
|
2171
3399
|
this.flow.destroy();
|
|
2172
3400
|
}
|
|
2173
3401
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2174
3402
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SignUpComponent, isStandalone: true, selector: "atlas-sign-up", inputs: { afterUrl: "afterUrl" }, ngImport: i0, template: `
|
|
2175
|
-
@if (
|
|
3403
|
+
@if (awaitingPasskey()) {
|
|
3404
|
+
<div [class]="cls('card', 'atlas-card')" data-atlas-passkey-sign-up="">
|
|
3405
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
3406
|
+
@for (error of flow.state().errors; track error.code) {
|
|
3407
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ errorText(error) }}</p>
|
|
3408
|
+
}
|
|
3409
|
+
<p class="atlas-text">{{ t('signUp.passkeyPrompt') }}</p>
|
|
3410
|
+
<button
|
|
3411
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
3412
|
+
type="button"
|
|
3413
|
+
[disabled]="flow.state().busy"
|
|
3414
|
+
(click)="flow.createPasskey()"
|
|
3415
|
+
>
|
|
3416
|
+
{{ t('signUp.passkeyCreate') }}
|
|
3417
|
+
</button>
|
|
3418
|
+
</div>
|
|
3419
|
+
} @else if (flow.state().attempt) {
|
|
2176
3420
|
<atlas-attempt-flow
|
|
2177
3421
|
[attempt]="flow.state().attempt"
|
|
2178
3422
|
[busy]="flow.state().busy"
|
|
2179
3423
|
[errors]="flow.state().errors"
|
|
2180
3424
|
[sessionKept]="flow.state().sessionKept"
|
|
2181
3425
|
titleKey="signUp.title"
|
|
3426
|
+
[emailVerification]="flow.settings()?.account?.emailVerification ?? 'code'"
|
|
2182
3427
|
(submitFlow)="flow.submit($event)"
|
|
3428
|
+
(resend)="flow.resendSignUp()"
|
|
2183
3429
|
></atlas-attempt-flow>
|
|
2184
3430
|
} @else {
|
|
2185
3431
|
<atlas-sign-up-form
|
|
@@ -2187,10 +3433,16 @@ class SignUpComponent {
|
|
|
2187
3433
|
[busy]="flow.state().busy"
|
|
2188
3434
|
[errors]="flow.state().errors"
|
|
2189
3435
|
[captcha]="flow.signUpCaptchaDrawn() ? flow.signUpCaptcha : null"
|
|
2190
|
-
|
|
3436
|
+
[policies]="flow.policies"
|
|
3437
|
+
[passkey]="passkeyForm()"
|
|
3438
|
+
[switchable]="canPasskey()"
|
|
3439
|
+
(switchMode)="toggleMode()"
|
|
3440
|
+
(submitSignUp)="
|
|
3441
|
+
passkeyForm() ? flow.createWithPasskey($event.values, $event.consent) : flow.create($event.values, $event.consent)
|
|
3442
|
+
"
|
|
2191
3443
|
></atlas-sign-up-form>
|
|
2192
3444
|
}
|
|
2193
|
-
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "sessionKept", "titleKey", "captcha", "firstFactors", "preparedFactor"], outputs: ["submitFlow", "chooseFactor"] }, { kind: "component", type: SignUpFormComponent, selector: "atlas-sign-up-form", inputs: ["settings", "busy", "errors", "captcha"], outputs: ["submitSignUp"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3445
|
+
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "sessionKept", "titleKey", "captcha", "firstFactors", "preparedFactor", "offerEmailSecondFactor", "emailedSecondFactor", "offerSmsSecondFactor", "textedSecondFactor", "enrollment", "enrollmentState", "emailVerification"], outputs: ["submitFlow", "chooseFactor", "emailSecondFactor", "smsSecondFactor", "resend", "policyDeclined"] }, { kind: "component", type: SignUpFormComponent, selector: "atlas-sign-up-form", inputs: ["settings", "busy", "errors", "captcha", "passkey", "switchable", "policies"], outputs: ["switchMode", "submitSignUp"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2194
3446
|
}
|
|
2195
3447
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, decorators: [{
|
|
2196
3448
|
type: Component,
|
|
@@ -2200,14 +3452,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2200
3452
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2201
3453
|
imports: [AttemptFlowComponent, SignUpFormComponent],
|
|
2202
3454
|
template: `
|
|
2203
|
-
@if (
|
|
3455
|
+
@if (awaitingPasskey()) {
|
|
3456
|
+
<div [class]="cls('card', 'atlas-card')" data-atlas-passkey-sign-up="">
|
|
3457
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
3458
|
+
@for (error of flow.state().errors; track error.code) {
|
|
3459
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ errorText(error) }}</p>
|
|
3460
|
+
}
|
|
3461
|
+
<p class="atlas-text">{{ t('signUp.passkeyPrompt') }}</p>
|
|
3462
|
+
<button
|
|
3463
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
3464
|
+
type="button"
|
|
3465
|
+
[disabled]="flow.state().busy"
|
|
3466
|
+
(click)="flow.createPasskey()"
|
|
3467
|
+
>
|
|
3468
|
+
{{ t('signUp.passkeyCreate') }}
|
|
3469
|
+
</button>
|
|
3470
|
+
</div>
|
|
3471
|
+
} @else if (flow.state().attempt) {
|
|
2204
3472
|
<atlas-attempt-flow
|
|
2205
3473
|
[attempt]="flow.state().attempt"
|
|
2206
3474
|
[busy]="flow.state().busy"
|
|
2207
3475
|
[errors]="flow.state().errors"
|
|
2208
3476
|
[sessionKept]="flow.state().sessionKept"
|
|
2209
3477
|
titleKey="signUp.title"
|
|
3478
|
+
[emailVerification]="flow.settings()?.account?.emailVerification ?? 'code'"
|
|
2210
3479
|
(submitFlow)="flow.submit($event)"
|
|
3480
|
+
(resend)="flow.resendSignUp()"
|
|
2211
3481
|
></atlas-attempt-flow>
|
|
2212
3482
|
} @else {
|
|
2213
3483
|
<atlas-sign-up-form
|
|
@@ -2215,7 +3485,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2215
3485
|
[busy]="flow.state().busy"
|
|
2216
3486
|
[errors]="flow.state().errors"
|
|
2217
3487
|
[captcha]="flow.signUpCaptchaDrawn() ? flow.signUpCaptcha : null"
|
|
2218
|
-
|
|
3488
|
+
[policies]="flow.policies"
|
|
3489
|
+
[passkey]="passkeyForm()"
|
|
3490
|
+
[switchable]="canPasskey()"
|
|
3491
|
+
(switchMode)="toggleMode()"
|
|
3492
|
+
(submitSignUp)="
|
|
3493
|
+
passkeyForm() ? flow.createWithPasskey($event.values, $event.consent) : flow.create($event.values, $event.consent)
|
|
3494
|
+
"
|
|
2219
3495
|
></atlas-sign-up-form>
|
|
2220
3496
|
}
|
|
2221
3497
|
`,
|
|
@@ -2549,32 +3825,392 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2549
3825
|
}] });
|
|
2550
3826
|
|
|
2551
3827
|
/**
|
|
2552
|
-
* `<atlas-organization-profile>` — the active organization's
|
|
2553
|
-
*
|
|
2554
|
-
*
|
|
2555
|
-
*
|
|
3828
|
+
* `<atlas-organization-profile>` — the active organization's members (admins
|
|
3829
|
+
* change roles and remove), invitations (admins invite, revoke and resend) and
|
|
3830
|
+
* settings (leave; an admin deletes after confirming their password). The
|
|
3831
|
+
* Angular peer of React's `<OrganizationProfile/>`, on the same headless
|
|
3832
|
+
* controller. Every control is gated on the ACTIVE org role, matching the
|
|
3833
|
+
* server's rule. Renders nothing with no active org. Emits `exit` with `left`
|
|
3834
|
+
* or `deleted`.
|
|
2556
3835
|
*/
|
|
2557
3836
|
class OrganizationProfileComponent {
|
|
2558
3837
|
atlas = inject(AtlasService);
|
|
2559
|
-
|
|
2560
|
-
|
|
3838
|
+
exit = new EventEmitter();
|
|
3839
|
+
client = this.atlas.createFapiClient();
|
|
3840
|
+
controller = null;
|
|
3841
|
+
unsubscribe = null;
|
|
3842
|
+
state = signal(null);
|
|
3843
|
+
tab = signal('members');
|
|
3844
|
+
email = signal('');
|
|
3845
|
+
inviteRole = signal('org:member');
|
|
3846
|
+
confirm = signal(null);
|
|
3847
|
+
needsPassword = signal(false);
|
|
3848
|
+
password = signal('');
|
|
3849
|
+
// The custom-role editor: null = creating, else the id of the role being edited.
|
|
3850
|
+
editingRole = signal(null);
|
|
3851
|
+
roleKey = signal('');
|
|
3852
|
+
roleName = signal('');
|
|
3853
|
+
rolePerms = signal([]);
|
|
3854
|
+
customRoles = computed(() => (this.state()?.roles ?? []).filter((r) => Boolean(r.organization_id)));
|
|
3855
|
+
userId = computed(() => this.atlas.claims()?.sub ?? null);
|
|
3856
|
+
canManage = computed(() => {
|
|
3857
|
+
this.atlas.claims();
|
|
3858
|
+
return this.atlas.has({ permission: 'org:sys_memberships:manage' });
|
|
3859
|
+
});
|
|
3860
|
+
isAdmin = computed(() => this.atlas.organization()?.role === 'org:admin');
|
|
3861
|
+
tabs = computed(() => ['members', ...(this.canManage() ? ['invitations', 'roles'] : []), 'settings']);
|
|
3862
|
+
roles = computed(() => {
|
|
3863
|
+
const roles = this.state()?.roles ?? [];
|
|
3864
|
+
return roles.length > 0 ? roles : [{ key: 'org:member', name: 'org:member' }];
|
|
3865
|
+
});
|
|
3866
|
+
errorMessage = computed(() => {
|
|
3867
|
+
const err = this.state()?.error;
|
|
3868
|
+
if (!err || err.needsReverification)
|
|
3869
|
+
return null;
|
|
3870
|
+
return localizedErrorMessage(err, this.atlas.errorLocalization, this.atlas.localization);
|
|
3871
|
+
});
|
|
3872
|
+
constructor() {
|
|
3873
|
+
effect(() => {
|
|
3874
|
+
const id = this.atlas.organization()?.organization.id ?? null;
|
|
3875
|
+
untracked(() => this.bind(id));
|
|
3876
|
+
});
|
|
3877
|
+
inject(DestroyRef).onDestroy(() => this.unsubscribe?.());
|
|
3878
|
+
}
|
|
3879
|
+
bind(id) {
|
|
3880
|
+
if (this.controller?.organizationId === id)
|
|
3881
|
+
return;
|
|
3882
|
+
this.unsubscribe?.();
|
|
3883
|
+
this.controller = null;
|
|
3884
|
+
this.state.set(null);
|
|
3885
|
+
if (!id)
|
|
3886
|
+
return;
|
|
3887
|
+
const controller = new OrganizationProfileController(this.client, id, {
|
|
3888
|
+
canManage: this.canManage(),
|
|
3889
|
+
onChange: async () => {
|
|
3890
|
+
await this.atlas.reload();
|
|
3891
|
+
},
|
|
3892
|
+
});
|
|
3893
|
+
this.controller = controller;
|
|
3894
|
+
this.unsubscribe = controller.subscribe((next) => this.state.set(next));
|
|
3895
|
+
this.state.set(controller.getState());
|
|
3896
|
+
void controller.load();
|
|
3897
|
+
}
|
|
3898
|
+
t(key, vars) {
|
|
3899
|
+
return translate(this.atlas.localization, key, vars);
|
|
2561
3900
|
}
|
|
2562
3901
|
cls(key, base) {
|
|
2563
3902
|
return classFor(key, this.atlas.appearance, base);
|
|
2564
3903
|
}
|
|
3904
|
+
memberName(m) {
|
|
3905
|
+
return [m.user?.first_name, m.user?.last_name].filter(Boolean).join(' ') || this.t('organization.unnamedMember');
|
|
3906
|
+
}
|
|
3907
|
+
changeRole(userId, role) {
|
|
3908
|
+
void this.controller?.changeRole(userId, role);
|
|
3909
|
+
}
|
|
3910
|
+
removeMember(userId) {
|
|
3911
|
+
void this.controller?.removeMember(userId);
|
|
3912
|
+
}
|
|
3913
|
+
async sendInvite() {
|
|
3914
|
+
const address = this.email().trim();
|
|
3915
|
+
if (this.controller && address && (await this.controller.invite(address, this.inviteRole())))
|
|
3916
|
+
this.email.set('');
|
|
3917
|
+
}
|
|
3918
|
+
resend(id) {
|
|
3919
|
+
void this.controller?.resendInvitation(id);
|
|
3920
|
+
}
|
|
3921
|
+
revoke(id) {
|
|
3922
|
+
void this.controller?.revokeInvitation(id);
|
|
3923
|
+
}
|
|
3924
|
+
async leave() {
|
|
3925
|
+
if (this.controller && (await this.controller.leave()))
|
|
3926
|
+
this.exit.emit('left');
|
|
3927
|
+
this.confirm.set(null);
|
|
3928
|
+
}
|
|
3929
|
+
async remove() {
|
|
3930
|
+
const controller = this.controller;
|
|
3931
|
+
if (!controller)
|
|
3932
|
+
return;
|
|
3933
|
+
if (this.needsPassword()) {
|
|
3934
|
+
try {
|
|
3935
|
+
await this.atlas.reauthenticate(this.password());
|
|
3936
|
+
}
|
|
3937
|
+
catch {
|
|
3938
|
+
return;
|
|
3939
|
+
}
|
|
3940
|
+
this.password.set('');
|
|
3941
|
+
this.needsPassword.set(false);
|
|
3942
|
+
}
|
|
3943
|
+
if (await controller.deleteOrganization()) {
|
|
3944
|
+
this.exit.emit('deleted');
|
|
3945
|
+
this.confirm.set(null);
|
|
3946
|
+
return;
|
|
3947
|
+
}
|
|
3948
|
+
const err = controller.getState().error;
|
|
3949
|
+
if (err?.needsReverification)
|
|
3950
|
+
this.needsPassword.set(true);
|
|
3951
|
+
}
|
|
3952
|
+
editRole(role) {
|
|
3953
|
+
this.editingRole.set(role.id ?? null);
|
|
3954
|
+
this.roleKey.set(role.key);
|
|
3955
|
+
this.roleName.set(role.name);
|
|
3956
|
+
this.rolePerms.set([...(role.permissions ?? [])]);
|
|
3957
|
+
}
|
|
3958
|
+
resetRoleForm() {
|
|
3959
|
+
this.editingRole.set(null);
|
|
3960
|
+
this.roleKey.set('');
|
|
3961
|
+
this.roleName.set('');
|
|
3962
|
+
this.rolePerms.set([]);
|
|
3963
|
+
}
|
|
3964
|
+
togglePermission(key, on) {
|
|
3965
|
+
this.rolePerms.update((current) => (on ? [...current, key] : current.filter((k) => k !== key)));
|
|
3966
|
+
}
|
|
3967
|
+
async saveRole() {
|
|
3968
|
+
const controller = this.controller;
|
|
3969
|
+
if (!controller)
|
|
3970
|
+
return;
|
|
3971
|
+
const editing = this.editingRole();
|
|
3972
|
+
const out = editing
|
|
3973
|
+
? await controller.updateRole(editing, { name: this.roleName().trim(), permissions: this.rolePerms() })
|
|
3974
|
+
: await controller.createRole({ key: this.roleKey().trim(), name: this.roleName().trim(), permissions: this.rolePerms() });
|
|
3975
|
+
if (out)
|
|
3976
|
+
this.resetRoleForm();
|
|
3977
|
+
}
|
|
3978
|
+
deleteRole(role) {
|
|
3979
|
+
if (role.id)
|
|
3980
|
+
void this.controller?.deleteRole(role.id);
|
|
3981
|
+
}
|
|
3982
|
+
cancel() {
|
|
3983
|
+
this.confirm.set(null);
|
|
3984
|
+
this.needsPassword.set(false);
|
|
3985
|
+
this.password.set('');
|
|
3986
|
+
}
|
|
2565
3987
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OrganizationProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2566
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OrganizationProfileComponent, isStandalone: true, selector: "atlas-organization-profile", ngImport: i0, template: `
|
|
3988
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OrganizationProfileComponent, isStandalone: true, selector: "atlas-organization-profile", outputs: { exit: "exit" }, ngImport: i0, template: `
|
|
2567
3989
|
@if (atlas.organization(); as membership) {
|
|
2568
|
-
|
|
2569
|
-
<
|
|
2570
|
-
|
|
3990
|
+
@if (state(); as st) {
|
|
3991
|
+
<div [class]="cls('card', 'atlas-card')" data-atlas="organization-profile">
|
|
3992
|
+
<h1>{{ membership.organization.name }}</h1>
|
|
3993
|
+
<p>{{ t('organization.roleLabel') }}: {{ membership.role }}</p>
|
|
2571
3994
|
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3995
|
+
<div [class]="cls('menu', 'atlas-menu')" role="tablist">
|
|
3996
|
+
@for (key of tabs(); track key) {
|
|
3997
|
+
<button
|
|
3998
|
+
type="button"
|
|
3999
|
+
role="tab"
|
|
4000
|
+
[attr.aria-selected]="tab() === key"
|
|
4001
|
+
[class]="cls('menuItem', 'atlas-menu-item')"
|
|
4002
|
+
[attr.data-atlas]="'organization-tab-' + key"
|
|
4003
|
+
(click)="tab.set(key)"
|
|
4004
|
+
>
|
|
4005
|
+
{{ t('organization.' + key) }}
|
|
4006
|
+
</button>
|
|
4007
|
+
}
|
|
4008
|
+
</div>
|
|
4009
|
+
|
|
4010
|
+
@if (errorMessage(); as message) {
|
|
4011
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ message }}</p>
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
@if (tab() === 'members') {
|
|
4015
|
+
<ul class="atlas-list" data-atlas="organization-members">
|
|
4016
|
+
@for (m of st.members; track m.id) {
|
|
4017
|
+
<li data-atlas="organization-member">
|
|
4018
|
+
<span>{{ memberName(m) }}{{ m.user?.id === userId() ? ' ' + t('organization.you') : '' }}</span>
|
|
4019
|
+
@if (canManage() && m.user && m.user.id !== userId()) {
|
|
4020
|
+
<select
|
|
4021
|
+
[attr.aria-label]="t('organization.roleLabel')"
|
|
4022
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
4023
|
+
[disabled]="st.busy === 'role:' + m.user.id"
|
|
4024
|
+
(change)="changeRole(m.user.id, $any($event.target).value)"
|
|
4025
|
+
>
|
|
4026
|
+
@for (r of roles(); track r.key) {
|
|
4027
|
+
<option [value]="r.key" [selected]="r.key === m.role">{{ r.name }}</option>
|
|
4028
|
+
}
|
|
4029
|
+
</select>
|
|
4030
|
+
<button
|
|
4031
|
+
type="button"
|
|
4032
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
4033
|
+
[disabled]="st.busy === 'remove:' + m.user.id"
|
|
4034
|
+
(click)="removeMember(m.user.id)"
|
|
4035
|
+
>
|
|
4036
|
+
{{ t('organization.remove') }}
|
|
4037
|
+
</button>
|
|
4038
|
+
} @else {
|
|
4039
|
+
<span> · {{ m.role }}</span>
|
|
4040
|
+
}
|
|
4041
|
+
</li>
|
|
4042
|
+
}
|
|
4043
|
+
</ul>
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
@if (tab() === 'invitations' && canManage()) {
|
|
4047
|
+
<div data-atlas="organization-invitations">
|
|
4048
|
+
<form (submit)="$event.preventDefault(); sendInvite()">
|
|
4049
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
4050
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.emailLabel') }}</span>
|
|
4051
|
+
<input
|
|
4052
|
+
type="email"
|
|
4053
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
4054
|
+
[value]="email()"
|
|
4055
|
+
(input)="email.set($any($event.target).value)"
|
|
4056
|
+
/>
|
|
4057
|
+
</label>
|
|
4058
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
4059
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.roleLabel') }}</span>
|
|
4060
|
+
<select [class]="cls('formFieldInput', 'atlas-input')" (change)="inviteRole.set($any($event.target).value)">
|
|
4061
|
+
@for (r of roles(); track r.key) {
|
|
4062
|
+
<option [value]="r.key" [selected]="r.key === inviteRole()">{{ r.name }}</option>
|
|
4063
|
+
}
|
|
4064
|
+
</select>
|
|
4065
|
+
</label>
|
|
4066
|
+
<button
|
|
4067
|
+
type="submit"
|
|
4068
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
4069
|
+
[disabled]="st.busy === 'invite' || !email().trim()"
|
|
4070
|
+
>
|
|
4071
|
+
{{ t('organization.sendInvite') }}
|
|
4072
|
+
</button>
|
|
4073
|
+
</form>
|
|
4074
|
+
@if (st.invitations.length === 0) {
|
|
4075
|
+
<p class="atlas-text">{{ t('organization.noInvitations') }}</p>
|
|
4076
|
+
} @else {
|
|
4077
|
+
<ul class="atlas-list">
|
|
4078
|
+
@for (inv of st.invitations; track inv.id) {
|
|
4079
|
+
<li data-atlas="organization-invitation">
|
|
4080
|
+
<span>{{ inv.email_address }} · {{ inv.role }}</span>
|
|
4081
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="resend(inv.id)">
|
|
4082
|
+
{{ t('organization.resend') }}
|
|
4083
|
+
</button>
|
|
4084
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="revoke(inv.id)">
|
|
4085
|
+
{{ t('organization.revoke') }}
|
|
4086
|
+
</button>
|
|
4087
|
+
</li>
|
|
4088
|
+
}
|
|
4089
|
+
</ul>
|
|
4090
|
+
}
|
|
4091
|
+
</div>
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
@if (tab() === 'roles' && canManage()) {
|
|
4095
|
+
<div data-atlas="organization-roles">
|
|
4096
|
+
@if (customRoles().length === 0) {
|
|
4097
|
+
<p class="atlas-text">{{ t('organization.noCustomRoles') }}</p>
|
|
4098
|
+
} @else {
|
|
4099
|
+
<ul class="atlas-list">
|
|
4100
|
+
@for (role of customRoles(); track role.key) {
|
|
4101
|
+
<li data-atlas="organization-role">
|
|
4102
|
+
<span>{{ role.name }} · {{ role.key }}</span>
|
|
4103
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="editRole(role)">
|
|
4104
|
+
{{ t('organization.editRole') }}
|
|
4105
|
+
</button>
|
|
4106
|
+
<button
|
|
4107
|
+
type="button"
|
|
4108
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
4109
|
+
[disabled]="!role.id || st.busy === 'role:delete:' + role.id"
|
|
4110
|
+
(click)="deleteRole(role)"
|
|
4111
|
+
>
|
|
4112
|
+
{{ t('organization.deleteRole') }}
|
|
4113
|
+
</button>
|
|
4114
|
+
</li>
|
|
4115
|
+
}
|
|
4116
|
+
</ul>
|
|
4117
|
+
}
|
|
4118
|
+
<form data-atlas="organization-role-form" (submit)="$event.preventDefault(); saveRole()">
|
|
4119
|
+
@if (!editingRole()) {
|
|
4120
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
4121
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.roleKey') }}</span>
|
|
4122
|
+
<input
|
|
4123
|
+
name="role_key"
|
|
4124
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
4125
|
+
[value]="roleKey()"
|
|
4126
|
+
(input)="roleKey.set($any($event.target).value)"
|
|
4127
|
+
/>
|
|
4128
|
+
</label>
|
|
4129
|
+
}
|
|
4130
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
4131
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.roleName') }}</span>
|
|
4132
|
+
<input
|
|
4133
|
+
name="role_name"
|
|
4134
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
4135
|
+
[value]="roleName()"
|
|
4136
|
+
(input)="roleName.set($any($event.target).value)"
|
|
4137
|
+
/>
|
|
4138
|
+
</label>
|
|
4139
|
+
<fieldset [class]="cls('formField', 'atlas-field')">
|
|
4140
|
+
<legend [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.rolePermissions') }}</legend>
|
|
4141
|
+
@for (p of st.permissions; track p.key) {
|
|
4142
|
+
<label data-atlas="organization-role-permission">
|
|
4143
|
+
<input
|
|
4144
|
+
type="checkbox"
|
|
4145
|
+
[value]="p.key"
|
|
4146
|
+
[checked]="rolePerms().includes(p.key)"
|
|
4147
|
+
(change)="togglePermission(p.key, $any($event.target).checked)"
|
|
4148
|
+
/>
|
|
4149
|
+
{{ p.name }}
|
|
4150
|
+
</label>
|
|
4151
|
+
}
|
|
4152
|
+
</fieldset>
|
|
4153
|
+
<button
|
|
4154
|
+
type="submit"
|
|
4155
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
4156
|
+
[disabled]="!roleName().trim() || (!editingRole() && !roleKey().trim()) || (st.busy ?? '').startsWith('role:')"
|
|
4157
|
+
>
|
|
4158
|
+
{{ editingRole() ? t('organization.saveRole') : t('organization.createRole') }}
|
|
4159
|
+
</button>
|
|
4160
|
+
@if (editingRole()) {
|
|
4161
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="resetRoleForm()">
|
|
4162
|
+
{{ t('organization.cancel') }}
|
|
4163
|
+
</button>
|
|
4164
|
+
}
|
|
4165
|
+
</form>
|
|
4166
|
+
</div>
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
@if (tab() === 'settings') {
|
|
4170
|
+
<div data-atlas="organization-settings">
|
|
4171
|
+
@if (confirm(); as action) {
|
|
4172
|
+
<p class="atlas-text">
|
|
4173
|
+
{{ t(action === 'leave' ? 'organization.leaveConfirm' : 'organization.deleteConfirm', { name: membership.organization.name }) }}
|
|
4174
|
+
</p>
|
|
4175
|
+
@if (needsPassword()) {
|
|
4176
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
4177
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.passwordPrompt') }}</span>
|
|
4178
|
+
<input
|
|
4179
|
+
type="password"
|
|
4180
|
+
autocomplete="current-password"
|
|
4181
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
4182
|
+
[value]="password()"
|
|
4183
|
+
(input)="password.set($any($event.target).value)"
|
|
4184
|
+
/>
|
|
4185
|
+
</label>
|
|
4186
|
+
}
|
|
4187
|
+
<button
|
|
4188
|
+
type="button"
|
|
4189
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
4190
|
+
[disabled]="st.busy === 'leave' || st.busy === 'delete'"
|
|
4191
|
+
(click)="action === 'leave' ? leave() : remove()"
|
|
4192
|
+
>
|
|
4193
|
+
{{ t('organization.confirm') }}
|
|
4194
|
+
</button>
|
|
4195
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="cancel()">
|
|
4196
|
+
{{ t('organization.cancel') }}
|
|
4197
|
+
</button>
|
|
4198
|
+
} @else {
|
|
4199
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="confirm.set('leave')">
|
|
4200
|
+
{{ t('organization.leave') }}
|
|
4201
|
+
</button>
|
|
4202
|
+
@if (isAdmin()) {
|
|
4203
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="confirm.set('delete')">
|
|
4204
|
+
{{ t('organization.delete') }}
|
|
4205
|
+
</button>
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
</div>
|
|
4209
|
+
}
|
|
4210
|
+
</div>
|
|
4211
|
+
}
|
|
2576
4212
|
}
|
|
2577
|
-
`, isInline: true,
|
|
4213
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2578
4214
|
}
|
|
2579
4215
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OrganizationProfileComponent, decorators: [{
|
|
2580
4216
|
type: Component,
|
|
@@ -2582,21 +4218,346 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2582
4218
|
selector: 'atlas-organization-profile',
|
|
2583
4219
|
standalone: true,
|
|
2584
4220
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2585
|
-
imports: [AtlasProtectDirective],
|
|
2586
4221
|
template: `
|
|
2587
4222
|
@if (atlas.organization(); as membership) {
|
|
2588
|
-
|
|
2589
|
-
<
|
|
2590
|
-
|
|
4223
|
+
@if (state(); as st) {
|
|
4224
|
+
<div [class]="cls('card', 'atlas-card')" data-atlas="organization-profile">
|
|
4225
|
+
<h1>{{ membership.organization.name }}</h1>
|
|
4226
|
+
<p>{{ t('organization.roleLabel') }}: {{ membership.role }}</p>
|
|
2591
4227
|
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
4228
|
+
<div [class]="cls('menu', 'atlas-menu')" role="tablist">
|
|
4229
|
+
@for (key of tabs(); track key) {
|
|
4230
|
+
<button
|
|
4231
|
+
type="button"
|
|
4232
|
+
role="tab"
|
|
4233
|
+
[attr.aria-selected]="tab() === key"
|
|
4234
|
+
[class]="cls('menuItem', 'atlas-menu-item')"
|
|
4235
|
+
[attr.data-atlas]="'organization-tab-' + key"
|
|
4236
|
+
(click)="tab.set(key)"
|
|
4237
|
+
>
|
|
4238
|
+
{{ t('organization.' + key) }}
|
|
4239
|
+
</button>
|
|
4240
|
+
}
|
|
4241
|
+
</div>
|
|
4242
|
+
|
|
4243
|
+
@if (errorMessage(); as message) {
|
|
4244
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ message }}</p>
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
@if (tab() === 'members') {
|
|
4248
|
+
<ul class="atlas-list" data-atlas="organization-members">
|
|
4249
|
+
@for (m of st.members; track m.id) {
|
|
4250
|
+
<li data-atlas="organization-member">
|
|
4251
|
+
<span>{{ memberName(m) }}{{ m.user?.id === userId() ? ' ' + t('organization.you') : '' }}</span>
|
|
4252
|
+
@if (canManage() && m.user && m.user.id !== userId()) {
|
|
4253
|
+
<select
|
|
4254
|
+
[attr.aria-label]="t('organization.roleLabel')"
|
|
4255
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
4256
|
+
[disabled]="st.busy === 'role:' + m.user.id"
|
|
4257
|
+
(change)="changeRole(m.user.id, $any($event.target).value)"
|
|
4258
|
+
>
|
|
4259
|
+
@for (r of roles(); track r.key) {
|
|
4260
|
+
<option [value]="r.key" [selected]="r.key === m.role">{{ r.name }}</option>
|
|
4261
|
+
}
|
|
4262
|
+
</select>
|
|
4263
|
+
<button
|
|
4264
|
+
type="button"
|
|
4265
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
4266
|
+
[disabled]="st.busy === 'remove:' + m.user.id"
|
|
4267
|
+
(click)="removeMember(m.user.id)"
|
|
4268
|
+
>
|
|
4269
|
+
{{ t('organization.remove') }}
|
|
4270
|
+
</button>
|
|
4271
|
+
} @else {
|
|
4272
|
+
<span> · {{ m.role }}</span>
|
|
4273
|
+
}
|
|
4274
|
+
</li>
|
|
4275
|
+
}
|
|
4276
|
+
</ul>
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
@if (tab() === 'invitations' && canManage()) {
|
|
4280
|
+
<div data-atlas="organization-invitations">
|
|
4281
|
+
<form (submit)="$event.preventDefault(); sendInvite()">
|
|
4282
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
4283
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.emailLabel') }}</span>
|
|
4284
|
+
<input
|
|
4285
|
+
type="email"
|
|
4286
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
4287
|
+
[value]="email()"
|
|
4288
|
+
(input)="email.set($any($event.target).value)"
|
|
4289
|
+
/>
|
|
4290
|
+
</label>
|
|
4291
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
4292
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.roleLabel') }}</span>
|
|
4293
|
+
<select [class]="cls('formFieldInput', 'atlas-input')" (change)="inviteRole.set($any($event.target).value)">
|
|
4294
|
+
@for (r of roles(); track r.key) {
|
|
4295
|
+
<option [value]="r.key" [selected]="r.key === inviteRole()">{{ r.name }}</option>
|
|
4296
|
+
}
|
|
4297
|
+
</select>
|
|
4298
|
+
</label>
|
|
4299
|
+
<button
|
|
4300
|
+
type="submit"
|
|
4301
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
4302
|
+
[disabled]="st.busy === 'invite' || !email().trim()"
|
|
4303
|
+
>
|
|
4304
|
+
{{ t('organization.sendInvite') }}
|
|
4305
|
+
</button>
|
|
4306
|
+
</form>
|
|
4307
|
+
@if (st.invitations.length === 0) {
|
|
4308
|
+
<p class="atlas-text">{{ t('organization.noInvitations') }}</p>
|
|
4309
|
+
} @else {
|
|
4310
|
+
<ul class="atlas-list">
|
|
4311
|
+
@for (inv of st.invitations; track inv.id) {
|
|
4312
|
+
<li data-atlas="organization-invitation">
|
|
4313
|
+
<span>{{ inv.email_address }} · {{ inv.role }}</span>
|
|
4314
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="resend(inv.id)">
|
|
4315
|
+
{{ t('organization.resend') }}
|
|
4316
|
+
</button>
|
|
4317
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="revoke(inv.id)">
|
|
4318
|
+
{{ t('organization.revoke') }}
|
|
4319
|
+
</button>
|
|
4320
|
+
</li>
|
|
4321
|
+
}
|
|
4322
|
+
</ul>
|
|
4323
|
+
}
|
|
4324
|
+
</div>
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
@if (tab() === 'roles' && canManage()) {
|
|
4328
|
+
<div data-atlas="organization-roles">
|
|
4329
|
+
@if (customRoles().length === 0) {
|
|
4330
|
+
<p class="atlas-text">{{ t('organization.noCustomRoles') }}</p>
|
|
4331
|
+
} @else {
|
|
4332
|
+
<ul class="atlas-list">
|
|
4333
|
+
@for (role of customRoles(); track role.key) {
|
|
4334
|
+
<li data-atlas="organization-role">
|
|
4335
|
+
<span>{{ role.name }} · {{ role.key }}</span>
|
|
4336
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="editRole(role)">
|
|
4337
|
+
{{ t('organization.editRole') }}
|
|
4338
|
+
</button>
|
|
4339
|
+
<button
|
|
4340
|
+
type="button"
|
|
4341
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
4342
|
+
[disabled]="!role.id || st.busy === 'role:delete:' + role.id"
|
|
4343
|
+
(click)="deleteRole(role)"
|
|
4344
|
+
>
|
|
4345
|
+
{{ t('organization.deleteRole') }}
|
|
4346
|
+
</button>
|
|
4347
|
+
</li>
|
|
4348
|
+
}
|
|
4349
|
+
</ul>
|
|
4350
|
+
}
|
|
4351
|
+
<form data-atlas="organization-role-form" (submit)="$event.preventDefault(); saveRole()">
|
|
4352
|
+
@if (!editingRole()) {
|
|
4353
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
4354
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.roleKey') }}</span>
|
|
4355
|
+
<input
|
|
4356
|
+
name="role_key"
|
|
4357
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
4358
|
+
[value]="roleKey()"
|
|
4359
|
+
(input)="roleKey.set($any($event.target).value)"
|
|
4360
|
+
/>
|
|
4361
|
+
</label>
|
|
4362
|
+
}
|
|
4363
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
4364
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.roleName') }}</span>
|
|
4365
|
+
<input
|
|
4366
|
+
name="role_name"
|
|
4367
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
4368
|
+
[value]="roleName()"
|
|
4369
|
+
(input)="roleName.set($any($event.target).value)"
|
|
4370
|
+
/>
|
|
4371
|
+
</label>
|
|
4372
|
+
<fieldset [class]="cls('formField', 'atlas-field')">
|
|
4373
|
+
<legend [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.rolePermissions') }}</legend>
|
|
4374
|
+
@for (p of st.permissions; track p.key) {
|
|
4375
|
+
<label data-atlas="organization-role-permission">
|
|
4376
|
+
<input
|
|
4377
|
+
type="checkbox"
|
|
4378
|
+
[value]="p.key"
|
|
4379
|
+
[checked]="rolePerms().includes(p.key)"
|
|
4380
|
+
(change)="togglePermission(p.key, $any($event.target).checked)"
|
|
4381
|
+
/>
|
|
4382
|
+
{{ p.name }}
|
|
4383
|
+
</label>
|
|
4384
|
+
}
|
|
4385
|
+
</fieldset>
|
|
4386
|
+
<button
|
|
4387
|
+
type="submit"
|
|
4388
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
4389
|
+
[disabled]="!roleName().trim() || (!editingRole() && !roleKey().trim()) || (st.busy ?? '').startsWith('role:')"
|
|
4390
|
+
>
|
|
4391
|
+
{{ editingRole() ? t('organization.saveRole') : t('organization.createRole') }}
|
|
4392
|
+
</button>
|
|
4393
|
+
@if (editingRole()) {
|
|
4394
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="resetRoleForm()">
|
|
4395
|
+
{{ t('organization.cancel') }}
|
|
4396
|
+
</button>
|
|
4397
|
+
}
|
|
4398
|
+
</form>
|
|
4399
|
+
</div>
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
@if (tab() === 'settings') {
|
|
4403
|
+
<div data-atlas="organization-settings">
|
|
4404
|
+
@if (confirm(); as action) {
|
|
4405
|
+
<p class="atlas-text">
|
|
4406
|
+
{{ t(action === 'leave' ? 'organization.leaveConfirm' : 'organization.deleteConfirm', { name: membership.organization.name }) }}
|
|
4407
|
+
</p>
|
|
4408
|
+
@if (needsPassword()) {
|
|
4409
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
4410
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('organization.passwordPrompt') }}</span>
|
|
4411
|
+
<input
|
|
4412
|
+
type="password"
|
|
4413
|
+
autocomplete="current-password"
|
|
4414
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
4415
|
+
[value]="password()"
|
|
4416
|
+
(input)="password.set($any($event.target).value)"
|
|
4417
|
+
/>
|
|
4418
|
+
</label>
|
|
4419
|
+
}
|
|
4420
|
+
<button
|
|
4421
|
+
type="button"
|
|
4422
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
4423
|
+
[disabled]="st.busy === 'leave' || st.busy === 'delete'"
|
|
4424
|
+
(click)="action === 'leave' ? leave() : remove()"
|
|
4425
|
+
>
|
|
4426
|
+
{{ t('organization.confirm') }}
|
|
4427
|
+
</button>
|
|
4428
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="cancel()">
|
|
4429
|
+
{{ t('organization.cancel') }}
|
|
4430
|
+
</button>
|
|
4431
|
+
} @else {
|
|
4432
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="confirm.set('leave')">
|
|
4433
|
+
{{ t('organization.leave') }}
|
|
4434
|
+
</button>
|
|
4435
|
+
@if (isAdmin()) {
|
|
4436
|
+
<button type="button" [class]="cls('formButtonSecondary', 'atlas-button-secondary')" (click)="confirm.set('delete')">
|
|
4437
|
+
{{ t('organization.delete') }}
|
|
4438
|
+
</button>
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
</div>
|
|
4442
|
+
}
|
|
4443
|
+
</div>
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
`,
|
|
4447
|
+
}]
|
|
4448
|
+
}], ctorParameters: () => [], propDecorators: { exit: [{
|
|
4449
|
+
type: Output
|
|
4450
|
+
}] } });
|
|
4451
|
+
|
|
4452
|
+
/**
|
|
4453
|
+
* `<atlas-organization-audit-log>` — the active organization's audit trail for
|
|
4454
|
+
* its admins, next to `<atlas-organization-profile>`. The Angular peer of
|
|
4455
|
+
* React's `<OrganizationAuditLog/>`. Renders nothing for a member who may not
|
|
4456
|
+
* read it (the server is the authority; the component just follows it). Labels
|
|
4457
|
+
* are inputs so an app can localise them.
|
|
4458
|
+
*/
|
|
4459
|
+
class OrganizationAuditLogComponent {
|
|
4460
|
+
/** Entries per page (the server caps it). */
|
|
4461
|
+
pageSize;
|
|
4462
|
+
/** Narrow to one action, or a prefix ending in `.`. */
|
|
4463
|
+
action;
|
|
4464
|
+
labels = {};
|
|
4465
|
+
formatDate = (ms) => new Date(ms).toLocaleString();
|
|
4466
|
+
atlas = inject(AtlasService);
|
|
4467
|
+
/** Created in ngOnInit, once the inputs are set (templates render after it). */
|
|
4468
|
+
log;
|
|
4469
|
+
ngOnInit() {
|
|
4470
|
+
this.log = this.atlas.organizationAuditLog({ pageSize: this.pageSize, action: this.action });
|
|
4471
|
+
}
|
|
4472
|
+
ngOnDestroy() {
|
|
4473
|
+
this.log.destroy();
|
|
4474
|
+
}
|
|
4475
|
+
describe(action) {
|
|
4476
|
+
return describeAuditAction(action);
|
|
4477
|
+
}
|
|
4478
|
+
cls(key, base) {
|
|
4479
|
+
return classFor(key, this.atlas.appearance, base);
|
|
4480
|
+
}
|
|
4481
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OrganizationAuditLogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4482
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OrganizationAuditLogComponent, isStandalone: true, selector: "atlas-organization-audit-log", inputs: { pageSize: "pageSize", action: "action", labels: "labels", formatDate: "formatDate" }, ngImport: i0, template: `
|
|
4483
|
+
@if (atlas.organization() && !log.state().forbidden) {
|
|
4484
|
+
<div [class]="cls('card', 'atlas-card')" data-atlas="organization-audit-log">
|
|
4485
|
+
<h2 [class]="cls('headerTitle', 'atlas-title')">{{ labels.title ?? 'Audit log' }}</h2>
|
|
4486
|
+
@if (log.state().entries.length === 0 && !log.state().loading) {
|
|
4487
|
+
<p [class]="cls('headerSubtitle', 'atlas-subtitle')">{{ labels.empty ?? 'No activity recorded yet.' }}</p>
|
|
4488
|
+
} @else {
|
|
4489
|
+
<ul [class]="cls('list', 'atlas-audit-list')">
|
|
4490
|
+
@for (entry of log.state().entries; track entry.id) {
|
|
4491
|
+
<li [class]="cls('listItem', 'atlas-audit-item')" [attr.data-action]="entry.action">
|
|
4492
|
+
<strong>{{ describe(entry.action) }}</strong>
|
|
4493
|
+
<span class="atlas-audit-meta">
|
|
4494
|
+
· {{ formatDate(entry.created_at) }}{{ entry.actor.id ? ' · ' + (labels.actor ?? 'by') + ' ' + entry.actor.type + ' ' + entry.actor.id : '' }}
|
|
4495
|
+
</span>
|
|
4496
|
+
</li>
|
|
4497
|
+
}
|
|
4498
|
+
</ul>
|
|
4499
|
+
}
|
|
4500
|
+
@if (log.state().hasMore) {
|
|
4501
|
+
<button
|
|
4502
|
+
type="button"
|
|
4503
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
4504
|
+
[disabled]="log.state().loading"
|
|
4505
|
+
(click)="log.loadMore()"
|
|
4506
|
+
>
|
|
4507
|
+
{{ labels.loadMore ?? 'Load more' }}
|
|
4508
|
+
</button>
|
|
4509
|
+
}
|
|
4510
|
+
</div>
|
|
4511
|
+
}
|
|
4512
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4513
|
+
}
|
|
4514
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OrganizationAuditLogComponent, decorators: [{
|
|
4515
|
+
type: Component,
|
|
4516
|
+
args: [{
|
|
4517
|
+
selector: 'atlas-organization-audit-log',
|
|
4518
|
+
standalone: true,
|
|
4519
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4520
|
+
template: `
|
|
4521
|
+
@if (atlas.organization() && !log.state().forbidden) {
|
|
4522
|
+
<div [class]="cls('card', 'atlas-card')" data-atlas="organization-audit-log">
|
|
4523
|
+
<h2 [class]="cls('headerTitle', 'atlas-title')">{{ labels.title ?? 'Audit log' }}</h2>
|
|
4524
|
+
@if (log.state().entries.length === 0 && !log.state().loading) {
|
|
4525
|
+
<p [class]="cls('headerSubtitle', 'atlas-subtitle')">{{ labels.empty ?? 'No activity recorded yet.' }}</p>
|
|
4526
|
+
} @else {
|
|
4527
|
+
<ul [class]="cls('list', 'atlas-audit-list')">
|
|
4528
|
+
@for (entry of log.state().entries; track entry.id) {
|
|
4529
|
+
<li [class]="cls('listItem', 'atlas-audit-item')" [attr.data-action]="entry.action">
|
|
4530
|
+
<strong>{{ describe(entry.action) }}</strong>
|
|
4531
|
+
<span class="atlas-audit-meta">
|
|
4532
|
+
· {{ formatDate(entry.created_at) }}{{ entry.actor.id ? ' · ' + (labels.actor ?? 'by') + ' ' + entry.actor.type + ' ' + entry.actor.id : '' }}
|
|
4533
|
+
</span>
|
|
4534
|
+
</li>
|
|
4535
|
+
}
|
|
4536
|
+
</ul>
|
|
4537
|
+
}
|
|
4538
|
+
@if (log.state().hasMore) {
|
|
4539
|
+
<button
|
|
4540
|
+
type="button"
|
|
4541
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
4542
|
+
[disabled]="log.state().loading"
|
|
4543
|
+
(click)="log.loadMore()"
|
|
4544
|
+
>
|
|
4545
|
+
{{ labels.loadMore ?? 'Load more' }}
|
|
4546
|
+
</button>
|
|
4547
|
+
}
|
|
2595
4548
|
</div>
|
|
2596
4549
|
}
|
|
2597
4550
|
`,
|
|
2598
4551
|
}]
|
|
2599
|
-
}]
|
|
4552
|
+
}], propDecorators: { pageSize: [{
|
|
4553
|
+
type: Input
|
|
4554
|
+
}], action: [{
|
|
4555
|
+
type: Input
|
|
4556
|
+
}], labels: [{
|
|
4557
|
+
type: Input
|
|
4558
|
+
}], formatDate: [{
|
|
4559
|
+
type: Input
|
|
4560
|
+
}] } });
|
|
2600
4561
|
|
|
2601
4562
|
/**
|
|
2602
4563
|
* Every standalone directive and component the SDK ships. Import the array in a
|
|
@@ -2620,6 +4581,10 @@ const ATLAS_UI = [
|
|
|
2620
4581
|
OrganizationSwitcherComponent,
|
|
2621
4582
|
UserProfileComponent,
|
|
2622
4583
|
OrganizationProfileComponent,
|
|
4584
|
+
OrganizationAuditLogComponent,
|
|
4585
|
+
AtlasPoliciesDirective,
|
|
4586
|
+
PolicyAcceptStepComponent,
|
|
4587
|
+
PolicyGateComponent,
|
|
2623
4588
|
];
|
|
2624
4589
|
|
|
2625
4590
|
/**
|
|
@@ -2677,7 +4642,7 @@ class AtlasModule {
|
|
|
2677
4642
|
return { ngModule: AtlasModule, providers };
|
|
2678
4643
|
}
|
|
2679
4644
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
2680
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: AtlasModule, imports: [AtlasSignedInDirective, AtlasSignedOutDirective, AtlasLoadingDirective, AtlasLoadedDirective, AtlasProtectDirective, AtlasRootDirective, SignInComponent, SignUpComponent, UserButtonComponent, OrganizationSwitcherComponent, UserProfileComponent, OrganizationProfileComponent], exports: [AtlasSignedInDirective, AtlasSignedOutDirective, AtlasLoadingDirective, AtlasLoadedDirective, AtlasProtectDirective, AtlasRootDirective, SignInComponent, SignUpComponent, UserButtonComponent, OrganizationSwitcherComponent, UserProfileComponent, OrganizationProfileComponent] });
|
|
4645
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: AtlasModule, imports: [AtlasSignedInDirective, AtlasSignedOutDirective, AtlasLoadingDirective, AtlasLoadedDirective, AtlasProtectDirective, AtlasRootDirective, SignInComponent, SignUpComponent, UserButtonComponent, OrganizationSwitcherComponent, UserProfileComponent, OrganizationProfileComponent, OrganizationAuditLogComponent, AtlasPoliciesDirective, PolicyAcceptStepComponent, PolicyGateComponent], exports: [AtlasSignedInDirective, AtlasSignedOutDirective, AtlasLoadingDirective, AtlasLoadedDirective, AtlasProtectDirective, AtlasRootDirective, SignInComponent, SignUpComponent, UserButtonComponent, OrganizationSwitcherComponent, UserProfileComponent, OrganizationProfileComponent, OrganizationAuditLogComponent, AtlasPoliciesDirective, PolicyAcceptStepComponent, PolicyGateComponent] });
|
|
2681
4646
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasModule });
|
|
2682
4647
|
}
|
|
2683
4648
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasModule, decorators: [{
|
|
@@ -2690,6 +4655,75 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2690
4655
|
}]
|
|
2691
4656
|
}] });
|
|
2692
4657
|
|
|
4658
|
+
/**
|
|
4659
|
+
* `<atlas-impersonation-banner>` — shown only while the active session is an
|
|
4660
|
+
* impersonation, naming the user acted as, with "Return to admin" (or "End
|
|
4661
|
+
* impersonation" when there is no session of the impersonator's to resume).
|
|
4662
|
+
* Emits `ended` with the outcome. The Angular peer of React's
|
|
4663
|
+
* `<ImpersonationBanner/>`.
|
|
4664
|
+
*/
|
|
4665
|
+
class ImpersonationBannerComponent {
|
|
4666
|
+
atlas = inject(AtlasService);
|
|
4667
|
+
ended = new EventEmitter();
|
|
4668
|
+
busy = signal(false);
|
|
4669
|
+
actor = computed(() => actorFromClaims(this.atlas.claims()) ?? this.atlas.session()?.actor ?? null);
|
|
4670
|
+
canReturn = computed(() => this.actor() !== null && Boolean(this.atlas.session()?.actor_session_id));
|
|
4671
|
+
name = computed(() => {
|
|
4672
|
+
const u = this.atlas.user();
|
|
4673
|
+
return ([u?.first_name, u?.last_name].filter(Boolean).join(' ') ||
|
|
4674
|
+
u?.username ||
|
|
4675
|
+
u?.email_addresses?.find((e) => e.primary)?.email_address ||
|
|
4676
|
+
u?.id ||
|
|
4677
|
+
'');
|
|
4678
|
+
});
|
|
4679
|
+
t(key, vars) {
|
|
4680
|
+
return translate(this.atlas.localization, key, vars);
|
|
4681
|
+
}
|
|
4682
|
+
cls(key, base) {
|
|
4683
|
+
return classFor(key, this.atlas.appearance, base);
|
|
4684
|
+
}
|
|
4685
|
+
async end() {
|
|
4686
|
+
this.busy.set(true);
|
|
4687
|
+
try {
|
|
4688
|
+
this.ended.emit(await this.atlas.endImpersonation());
|
|
4689
|
+
}
|
|
4690
|
+
finally {
|
|
4691
|
+
this.busy.set(false);
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImpersonationBannerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4695
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ImpersonationBannerComponent, isStandalone: true, selector: "atlas-impersonation-banner", outputs: { ended: "ended" }, ngImport: i0, template: `
|
|
4696
|
+
@if (actor()) {
|
|
4697
|
+
<div [class]="cls('card', 'atlas-alert atlas-impersonation-banner')" role="status">
|
|
4698
|
+
<span [class]="cls('headerSubtitle', 'atlas-text')">{{ t('impersonation.banner', { name: name() }) }}</span>
|
|
4699
|
+
<button type="button" [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy()" (click)="end()">
|
|
4700
|
+
{{ canReturn() ? t('impersonation.returnToAdmin') : t('impersonation.end') }}
|
|
4701
|
+
</button>
|
|
4702
|
+
</div>
|
|
4703
|
+
}
|
|
4704
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4705
|
+
}
|
|
4706
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImpersonationBannerComponent, decorators: [{
|
|
4707
|
+
type: Component,
|
|
4708
|
+
args: [{
|
|
4709
|
+
selector: 'atlas-impersonation-banner',
|
|
4710
|
+
standalone: true,
|
|
4711
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4712
|
+
template: `
|
|
4713
|
+
@if (actor()) {
|
|
4714
|
+
<div [class]="cls('card', 'atlas-alert atlas-impersonation-banner')" role="status">
|
|
4715
|
+
<span [class]="cls('headerSubtitle', 'atlas-text')">{{ t('impersonation.banner', { name: name() }) }}</span>
|
|
4716
|
+
<button type="button" [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy()" (click)="end()">
|
|
4717
|
+
{{ canReturn() ? t('impersonation.returnToAdmin') : t('impersonation.end') }}
|
|
4718
|
+
</button>
|
|
4719
|
+
</div>
|
|
4720
|
+
}
|
|
4721
|
+
`,
|
|
4722
|
+
}]
|
|
4723
|
+
}], propDecorators: { ended: [{
|
|
4724
|
+
type: Output
|
|
4725
|
+
}] } });
|
|
4726
|
+
|
|
2693
4727
|
/**
|
|
2694
4728
|
* Route guard factory that gates a route on session — and optionally on a
|
|
2695
4729
|
* permission/role. The Angular peer of using `<SignedIn>` / `<Protect>` to fence
|
|
@@ -2769,5 +4803,5 @@ function atlasChildGuard(options = {}) {
|
|
|
2769
4803
|
* Generated bundle index. Do not edit.
|
|
2770
4804
|
*/
|
|
2771
4805
|
|
|
2772
|
-
export { ATLAS_ANGULAR_SDK, ATLAS_ANGULAR_VERSION, ATLAS_OPTIONS, ATLAS_UI, AtlasCaptchaSlotDirective, AtlasFlowController, AtlasLoadedDirective, AtlasLoadingDirective, AtlasModule, AtlasProtectDirective, AtlasRootDirective, AtlasService, AtlasSignedInDirective, AtlasSignedOutDirective, AttemptFlowComponent, DEFAULT_TOKENS, EN_US, OrganizationProfileComponent, OrganizationSwitcherComponent, SignInComponent, SignUpComponent, SignUpFormComponent, THEMES, UserButtonComponent, UserProfileComponent, atlasChildGuard, atlasGuard, classFor, cssVariables, explicitTokens, provideAtlas, renderAppearance, resolveTokens, translate, withFallback };
|
|
4806
|
+
export { ATLAS_ANGULAR_SDK, ATLAS_ANGULAR_VERSION, ATLAS_OPTIONS, ATLAS_UI, AtlasCaptchaSlotDirective, AtlasFlowController, AtlasLoadedDirective, AtlasLoadingDirective, AtlasModule, AtlasOrganizationAuditLog, AtlasPoliciesDirective, AtlasProtectDirective, AtlasRootDirective, AtlasService, AtlasSignedInDirective, AtlasSignedOutDirective, AttemptFlowComponent, DEFAULT_TOKENS, EN_US, ImpersonationBannerComponent, OrganizationAuditLogComponent, OrganizationProfileComponent, OrganizationSwitcherComponent, PolicyAcceptStepComponent, PolicyGateComponent, SignInComponent, SignUpComponent, SignUpFormComponent, THEMES, UserButtonComponent, UserProfileComponent, atlasChildGuard, atlasGuard, classFor, cssVariables, explicitTokens, provideAtlas, renderAppearance, resolveTokens, translate, withFallback };
|
|
2773
4807
|
//# sourceMappingURL=atlasauth-angular.mjs.map
|