@atlasauth/angular 0.12.0 → 0.14.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 +18 -0
- package/dist/esm2022/lib/components/attempt-flow.component.mjs +104 -6
- package/dist/esm2022/lib/components/flow-controller.mjs +92 -9
- package/dist/esm2022/lib/components/sign-in.component.mjs +65 -6
- package/dist/esm2022/lib/components/sign-up.component.mjs +46 -7
- package/dist/esm2022/lib/i18n.mjs +14 -1
- package/dist/esm2022/lib/version.mjs +2 -2
- package/dist/fesm2022/atlasauth-angular.mjs +314 -22
- package/dist/fesm2022/atlasauth-angular.mjs.map +1 -1
- package/dist/lib/components/attempt-flow.component.d.ts +47 -6
- package/dist/lib/components/flow-controller.d.ts +38 -5
- package/dist/lib/components/sign-in.component.d.ts +5 -0
- package/dist/lib/components/sign-up.component.d.ts +4 -1
- package/dist/lib/version.d.ts +1 -1
- package/package.json +5 -5
|
@@ -4,7 +4,7 @@ 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 { 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,
|
|
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, createPasskeySignInController, startSignInWithAtlas, mfaChoicesFrom, fetchInstanceSettings, fetchLastSignInMethod, offeredFirstFactors, preparePasswordHoldProof, prepareFactor, signInCaptchaOptions, flowNeedsCaptcha, planFirstFactor, attemptPasswordHoldProof, advanceWithCaptcha, loadPolicies, createSignUp, prepareSecondFactorCode, resendSignUpVerification, createPasskeySignUp, finishPasskeySignUp, pendingRecoveryCodes, acknowledgeRecoveryCodes, pendingExistingAccountNotice, acknowledgeExistingAccount, shouldKeepPolling, pollAttempt, lastUsedMethodLabel, signUp, passkeysSupported, confirmPolicies, existingAccountResetUrl, awaitingPasskey, listCibaRequests, getCibaPin, decideCibaRequest, setCibaPin, clearCibaPin, 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';
|
|
@@ -204,12 +204,21 @@ const EN_US = {
|
|
|
204
204
|
'signIn.signUpLink': 'Sign up',
|
|
205
205
|
'signIn.orDivider': 'or',
|
|
206
206
|
'signIn.socialButton': 'Continue with {provider}',
|
|
207
|
+
'signIn.continueWithAtlas': 'Continue with Atlas',
|
|
207
208
|
'signIn.emailCodeLabel': 'Verification code',
|
|
208
209
|
'signIn.useEmailCode': 'Email me a code',
|
|
210
|
+
'signIn.useEmailLink': 'Email me a sign-in link',
|
|
211
|
+
'signIn.linkSentTo': 'We sent a sign-in link to {email}. Open it on any device to continue.',
|
|
212
|
+
'signIn.resendLink': 'Resend link',
|
|
213
|
+
'signIn.passwordOption': 'Use your password instead',
|
|
214
|
+
'signIn.passwordHoldReleased': 'Your email is verified. Enter your password to sign in.',
|
|
215
|
+
'signUp.existingAccount': 'You already had an account. Your existing password still applies.',
|
|
209
216
|
'signIn.codeSentTo': 'We sent a code to {email}. Enter it below.',
|
|
210
217
|
'signIn.resendCode': 'Resend code',
|
|
211
218
|
'signIn.sending': 'Sending…',
|
|
212
219
|
'signIn.emailCodeOption': 'Email me a code instead',
|
|
220
|
+
'signIn.passkeyButton': 'Sign in with a passkey',
|
|
221
|
+
'signIn.passkeyCancelled': "Passkey sign-in didn't finish. Try again, or use another way to sign in.",
|
|
213
222
|
'signIn.magicLinkSent': 'Check your email for a sign-in link.',
|
|
214
223
|
'signIn.signedIn': 'You\'re signed in.',
|
|
215
224
|
'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.',
|
|
@@ -363,6 +372,9 @@ const EN_US = {
|
|
|
363
372
|
'error.email_delivery_unavailable': "This app can't send email right now. Try another sign-in method or contact the app owner.",
|
|
364
373
|
'error.strategy_disabled': 'This sign-in method is not available for this app.',
|
|
365
374
|
'error.sign_up_restricted': 'Sign-up on this app is by invitation only.',
|
|
375
|
+
'error.sign_up_denied': 'Sign-up is not available for this account.',
|
|
376
|
+
'error.password_not_yet_usable': 'Verify your email address before signing in with this password.',
|
|
377
|
+
'error.account_deleted': 'The account for this address was deleted, so it can’t be used to sign up again.',
|
|
366
378
|
'error.waitlisted': "You're on the waitlist. We'll email you when a place opens up.",
|
|
367
379
|
'error.identifier_not_allowed': 'This email or phone number is not allowed on this app.',
|
|
368
380
|
'error.disposable_email_blocked': 'Sign-up with a disposable or temporary email address is not allowed.',
|
|
@@ -371,6 +383,7 @@ const EN_US = {
|
|
|
371
383
|
'error.sign_ups_disabled': 'Sign-ups are temporarily disabled.',
|
|
372
384
|
'error.high_risk_blocked': 'This sign-in was blocked because it looked unusual.',
|
|
373
385
|
'error.sms_region_blocked': 'Text messages are not available for this phone number.',
|
|
386
|
+
'error.github_team_required': 'Your GitHub account is not a member of a GitHub organization or team allowed to sign in here.',
|
|
374
387
|
};
|
|
375
388
|
/**
|
|
376
389
|
* Look up a key and interpolate `{placeholders}`.
|
|
@@ -389,7 +402,7 @@ function withFallback(locale) {
|
|
|
389
402
|
}
|
|
390
403
|
|
|
391
404
|
/** This package's version, sent as `X-Atlas-SDK` (kept in step with package.json by a test). */
|
|
392
|
-
const ATLAS_ANGULAR_VERSION = '0.
|
|
405
|
+
const ATLAS_ANGULAR_VERSION = '0.14.0';
|
|
393
406
|
const ATLAS_ANGULAR_SDK = { name: '@atlasauth/angular', version: ATLAS_ANGULAR_VERSION };
|
|
394
407
|
|
|
395
408
|
/**
|
|
@@ -1481,10 +1494,16 @@ class AttemptFlowComponent {
|
|
|
1481
1494
|
firstFactors = null;
|
|
1482
1495
|
/** The first factor already sent (`AtlasFlowController.preparedFactor()`): its code is collected. */
|
|
1483
1496
|
preparedFactor = null;
|
|
1497
|
+
/** `FlowState.passwordHold`: where the proof went, and whether the password signs in now. */
|
|
1498
|
+
passwordHold = null;
|
|
1499
|
+
/** `AtlasFlowController.holdProof()`: the emailed proof a password hold still waits on. */
|
|
1500
|
+
holdProof = null;
|
|
1484
1501
|
/** Emits the collected field values when the form is submitted. */
|
|
1485
1502
|
submitFlow = new EventEmitter();
|
|
1486
1503
|
/** Emits when the user picks a first factor the server must send first (an emailed code). */
|
|
1487
1504
|
chooseFactor = new EventEmitter();
|
|
1505
|
+
/** Emits on "Use your password instead", from an emailed factor already sent. */
|
|
1506
|
+
usePassword = new EventEmitter();
|
|
1488
1507
|
/**
|
|
1489
1508
|
* Offer "Email me a code instead" at the second factor (the instance's
|
|
1490
1509
|
* `account.emailSecondFactor`); `emailSecondFactor` asks for it and
|
|
@@ -1545,6 +1564,11 @@ class AttemptFlowComponent {
|
|
|
1545
1564
|
});
|
|
1546
1565
|
return;
|
|
1547
1566
|
}
|
|
1567
|
+
// Only what is on screen: a code typed earlier must not turn a password into a code attempt.
|
|
1568
|
+
if (this.step().kind === 'collect_first_factor') {
|
|
1569
|
+
this.submitFlow.emit(this.preparedFactor === 'email_code' ? { code: this.values['code'] ?? '' } : { password: this.values['password'] ?? '' });
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1548
1572
|
this.submitFlow.emit(this.values);
|
|
1549
1573
|
}
|
|
1550
1574
|
/** The shared enrolment step is drawn (a controller was passed and the flow is enrolling). */
|
|
@@ -1616,7 +1640,13 @@ class AttemptFlowComponent {
|
|
|
1616
1640
|
codeSentText() {
|
|
1617
1641
|
if (this.step().kind !== 'collect_first_factor' || this.preparedFactor !== 'email_code')
|
|
1618
1642
|
return null;
|
|
1619
|
-
return this.t('signIn.codeSentTo', { email: this.attempt?.identifier ?? '' });
|
|
1643
|
+
return this.t('signIn.codeSentTo', { email: this.passwordHold?.email ?? this.attempt?.identifier ?? '' });
|
|
1644
|
+
}
|
|
1645
|
+
/** The "we sent a link to …" line, once a link was sent. */
|
|
1646
|
+
linkSentText() {
|
|
1647
|
+
if (this.step().kind !== 'collect_first_factor' || this.preparedFactor !== 'email_link')
|
|
1648
|
+
return null;
|
|
1649
|
+
return this.t('signIn.linkSentTo', { email: this.passwordHold?.email ?? this.attempt?.identifier ?? '' });
|
|
1620
1650
|
}
|
|
1621
1651
|
/** The attempt is `needs_policy_acceptance`. */
|
|
1622
1652
|
awaitingPolicies() {
|
|
@@ -1627,7 +1657,7 @@ class AttemptFlowComponent {
|
|
|
1627
1657
|
// Nothing to submit on a first-factor step that only offers an emailed code
|
|
1628
1658
|
// until that code is sent.
|
|
1629
1659
|
if (kind === 'collect_first_factor')
|
|
1630
|
-
return this.fields().
|
|
1660
|
+
return this.fields().some((f) => f.kind === 'input');
|
|
1631
1661
|
if (this.linkOnly())
|
|
1632
1662
|
return false;
|
|
1633
1663
|
// Enrolment: nothing to submit while choosing a method or fetching the setup.
|
|
@@ -1639,17 +1669,27 @@ class AttemptFlowComponent {
|
|
|
1639
1669
|
const step = this.step();
|
|
1640
1670
|
switch (step.kind) {
|
|
1641
1671
|
case 'collect_identifier':
|
|
1642
|
-
|
|
1672
|
+
// `webauthn`: saved passkeys appear in this field's autofill while <atlas-sign-in> holds a conditional request.
|
|
1673
|
+
return [{ kind: 'input', name: 'identifier', labelKey: 'signIn.identifierLabel', type: 'email', autocomplete: 'username webauthn' }];
|
|
1643
1674
|
case 'collect_first_factor': {
|
|
1675
|
+
// A link proved an address on an existing account: no session, and the password signs in.
|
|
1676
|
+
if (this.attempt?.object === 'sign_up_attempt' && !(this.attempt.supported_first_factors ?? []).includes('passkey')) {
|
|
1677
|
+
return [released()];
|
|
1678
|
+
}
|
|
1644
1679
|
if (this.preparedFactor === 'email_code') {
|
|
1645
1680
|
return [{ kind: 'input', name: 'code', labelKey: 'signIn.emailCodeLabel', type: 'text' }];
|
|
1646
1681
|
}
|
|
1682
|
+
if (this.preparedFactor === 'email_link')
|
|
1683
|
+
return [];
|
|
1647
1684
|
// The password field unless the only offer is an emailed code (chosen
|
|
1648
1685
|
// with the button below the form).
|
|
1649
1686
|
const offered = this.offered();
|
|
1650
1687
|
return offered.includes('email_code') && !offered.includes('password')
|
|
1651
1688
|
? []
|
|
1652
|
-
: [
|
|
1689
|
+
: [
|
|
1690
|
+
...(this.passwordHold?.released ? [released()] : []),
|
|
1691
|
+
{ kind: 'input', name: 'password', labelKey: 'signIn.passwordLabel', type: 'password' },
|
|
1692
|
+
];
|
|
1653
1693
|
}
|
|
1654
1694
|
case 'collect_second_factor':
|
|
1655
1695
|
return [{ kind: 'input', name: 'code', labelKey: 'mfa.codeLabel', type: 'text' }];
|
|
@@ -1693,7 +1733,7 @@ class AttemptFlowComponent {
|
|
|
1693
1733
|
}
|
|
1694
1734
|
}
|
|
1695
1735
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
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: `
|
|
1736
|
+
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", passwordHold: "passwordHold", holdProof: "holdProof", offerEmailSecondFactor: "offerEmailSecondFactor", emailedSecondFactor: "emailedSecondFactor", offerSmsSecondFactor: "offerSmsSecondFactor", textedSecondFactor: "textedSecondFactor", enrollment: "enrollment", enrollmentState: "enrollmentState", emailVerification: "emailVerification" }, outputs: { submitFlow: "submitFlow", chooseFactor: "chooseFactor", usePassword: "usePassword", emailSecondFactor: "emailSecondFactor", smsSecondFactor: "smsSecondFactor", resend: "resend", policyDeclined: "policyDeclined" }, ngImport: i0, template: `
|
|
1697
1737
|
<div [class]="cls('card', 'atlas-card')">
|
|
1698
1738
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
1699
1739
|
|
|
@@ -1705,6 +1745,9 @@ class AttemptFlowComponent {
|
|
|
1705
1745
|
@if (codeSentText(); as sentText) {
|
|
1706
1746
|
<p class="atlas-text" data-atlas-code-sent="">{{ sentText }}</p>
|
|
1707
1747
|
}
|
|
1748
|
+
@if (linkSentText(); as sentLink) {
|
|
1749
|
+
<p class="atlas-text" data-atlas-link-sent="">{{ sentLink }}</p>
|
|
1750
|
+
}
|
|
1708
1751
|
@if (linkOnly()) {
|
|
1709
1752
|
<!-- Nothing to type: the flow polls until the emailed link is opened (anywhere). -->
|
|
1710
1753
|
<p class="atlas-text" data-atlas-link-sent="" aria-live="polite">
|
|
@@ -1805,6 +1848,7 @@ class AttemptFlowComponent {
|
|
|
1805
1848
|
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1806
1849
|
[type]="f.type"
|
|
1807
1850
|
[attr.name]="f.name"
|
|
1851
|
+
[attr.autocomplete]="f.autocomplete ?? null"
|
|
1808
1852
|
[value]="values[f.name] ?? ''"
|
|
1809
1853
|
(input)="setValue(f.name, $any($event.target).value)"
|
|
1810
1854
|
/>
|
|
@@ -1812,6 +1856,8 @@ class AttemptFlowComponent {
|
|
|
1812
1856
|
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1813
1857
|
}
|
|
1814
1858
|
</label>
|
|
1859
|
+
} @else if (f.kind === 'released') {
|
|
1860
|
+
<p class="atlas-text" data-atlas-password-released="">{{ t(f.labelKey) }}</p>
|
|
1815
1861
|
} @else if (f.kind === 'alert') {
|
|
1816
1862
|
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ t(f.labelKey) }}</p>
|
|
1817
1863
|
} @else {
|
|
@@ -1893,6 +1939,31 @@ class AttemptFlowComponent {
|
|
|
1893
1939
|
{{ t(preparedFactor === 'email_code' ? 'signIn.resendCode' : 'signIn.useEmailCode') }}
|
|
1894
1940
|
</button>
|
|
1895
1941
|
}
|
|
1942
|
+
|
|
1943
|
+
@if (holdProof === 'email_link' && step().kind === 'collect_first_factor') {
|
|
1944
|
+
<button
|
|
1945
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1946
|
+
[disabled]="busy"
|
|
1947
|
+
type="button"
|
|
1948
|
+
[attr.data-strategy]="preparedFactor === 'email_link' ? null : 'email_link'"
|
|
1949
|
+
[attr.data-atlas-resend]="preparedFactor === 'email_link' ? '' : null"
|
|
1950
|
+
(click)="chooseFactor.emit('email_link')"
|
|
1951
|
+
>
|
|
1952
|
+
{{ t(preparedFactor === 'email_link' ? 'signIn.resendLink' : 'signIn.useEmailLink') }}
|
|
1953
|
+
</button>
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
@if (preparedFactor && offered().includes('password')) {
|
|
1957
|
+
<button
|
|
1958
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1959
|
+
[disabled]="busy"
|
|
1960
|
+
type="button"
|
|
1961
|
+
data-strategy="password"
|
|
1962
|
+
(click)="usePassword.emit()"
|
|
1963
|
+
>
|
|
1964
|
+
{{ t('signIn.passwordOption') }}
|
|
1965
|
+
</button>
|
|
1966
|
+
}
|
|
1896
1967
|
</form>
|
|
1897
1968
|
</div>
|
|
1898
1969
|
`, 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 });
|
|
@@ -1916,6 +1987,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1916
1987
|
@if (codeSentText(); as sentText) {
|
|
1917
1988
|
<p class="atlas-text" data-atlas-code-sent="">{{ sentText }}</p>
|
|
1918
1989
|
}
|
|
1990
|
+
@if (linkSentText(); as sentLink) {
|
|
1991
|
+
<p class="atlas-text" data-atlas-link-sent="">{{ sentLink }}</p>
|
|
1992
|
+
}
|
|
1919
1993
|
@if (linkOnly()) {
|
|
1920
1994
|
<!-- Nothing to type: the flow polls until the emailed link is opened (anywhere). -->
|
|
1921
1995
|
<p class="atlas-text" data-atlas-link-sent="" aria-live="polite">
|
|
@@ -2016,6 +2090,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2016
2090
|
[class]="cls('formFieldInput', 'atlas-input')"
|
|
2017
2091
|
[type]="f.type"
|
|
2018
2092
|
[attr.name]="f.name"
|
|
2093
|
+
[attr.autocomplete]="f.autocomplete ?? null"
|
|
2019
2094
|
[value]="values[f.name] ?? ''"
|
|
2020
2095
|
(input)="setValue(f.name, $any($event.target).value)"
|
|
2021
2096
|
/>
|
|
@@ -2023,6 +2098,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2023
2098
|
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
2024
2099
|
}
|
|
2025
2100
|
</label>
|
|
2101
|
+
} @else if (f.kind === 'released') {
|
|
2102
|
+
<p class="atlas-text" data-atlas-password-released="">{{ t(f.labelKey) }}</p>
|
|
2026
2103
|
} @else if (f.kind === 'alert') {
|
|
2027
2104
|
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ t(f.labelKey) }}</p>
|
|
2028
2105
|
} @else {
|
|
@@ -2104,6 +2181,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2104
2181
|
{{ t(preparedFactor === 'email_code' ? 'signIn.resendCode' : 'signIn.useEmailCode') }}
|
|
2105
2182
|
</button>
|
|
2106
2183
|
}
|
|
2184
|
+
|
|
2185
|
+
@if (holdProof === 'email_link' && step().kind === 'collect_first_factor') {
|
|
2186
|
+
<button
|
|
2187
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
2188
|
+
[disabled]="busy"
|
|
2189
|
+
type="button"
|
|
2190
|
+
[attr.data-strategy]="preparedFactor === 'email_link' ? null : 'email_link'"
|
|
2191
|
+
[attr.data-atlas-resend]="preparedFactor === 'email_link' ? '' : null"
|
|
2192
|
+
(click)="chooseFactor.emit('email_link')"
|
|
2193
|
+
>
|
|
2194
|
+
{{ t(preparedFactor === 'email_link' ? 'signIn.resendLink' : 'signIn.useEmailLink') }}
|
|
2195
|
+
</button>
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
@if (preparedFactor && offered().includes('password')) {
|
|
2199
|
+
<button
|
|
2200
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
2201
|
+
[disabled]="busy"
|
|
2202
|
+
type="button"
|
|
2203
|
+
data-strategy="password"
|
|
2204
|
+
(click)="usePassword.emit()"
|
|
2205
|
+
>
|
|
2206
|
+
{{ t('signIn.passwordOption') }}
|
|
2207
|
+
</button>
|
|
2208
|
+
}
|
|
2107
2209
|
</form>
|
|
2108
2210
|
</div>
|
|
2109
2211
|
`,
|
|
@@ -2124,10 +2226,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2124
2226
|
type: Input
|
|
2125
2227
|
}], preparedFactor: [{
|
|
2126
2228
|
type: Input
|
|
2229
|
+
}], passwordHold: [{
|
|
2230
|
+
type: Input
|
|
2231
|
+
}], holdProof: [{
|
|
2232
|
+
type: Input
|
|
2127
2233
|
}], submitFlow: [{
|
|
2128
2234
|
type: Output
|
|
2129
2235
|
}], chooseFactor: [{
|
|
2130
2236
|
type: Output
|
|
2237
|
+
}], usePassword: [{
|
|
2238
|
+
type: Output
|
|
2131
2239
|
}], offerEmailSecondFactor: [{
|
|
2132
2240
|
type: Input
|
|
2133
2241
|
}], emailedSecondFactor: [{
|
|
@@ -2155,6 +2263,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2155
2263
|
function msg(labelKey) {
|
|
2156
2264
|
return { kind: 'message', name: '', labelKey, type: 'text' };
|
|
2157
2265
|
}
|
|
2266
|
+
function released() {
|
|
2267
|
+
return { kind: 'released', name: '', labelKey: 'signIn.passwordHoldReleased', type: 'text' };
|
|
2268
|
+
}
|
|
2158
2269
|
|
|
2159
2270
|
/**
|
|
2160
2271
|
* The recovery codes a mid-sign-in MFA enrolment came with, shown once before
|
|
@@ -2290,6 +2401,7 @@ class AtlasFlowController {
|
|
|
2290
2401
|
client;
|
|
2291
2402
|
reload;
|
|
2292
2403
|
t;
|
|
2404
|
+
holdExistingAccountNotice;
|
|
2293
2405
|
state;
|
|
2294
2406
|
/**
|
|
2295
2407
|
* The instance's public sign-in / sign-up settings (captcha, sign-up fields,
|
|
@@ -2341,23 +2453,60 @@ class AtlasFlowController {
|
|
|
2341
2453
|
* {@link create} sends their acceptances and reloads them after a refusal.
|
|
2342
2454
|
*/
|
|
2343
2455
|
policies = new PolicyAcceptanceController([]);
|
|
2456
|
+
/**
|
|
2457
|
+
* Passkey sign-in (the "Sign in with a passkey" button and passkey autofill on
|
|
2458
|
+
* the identifier field). The outcome goes through the same completion as every sign-in.
|
|
2459
|
+
*/
|
|
2460
|
+
passkey;
|
|
2461
|
+
/** Whether this browser can run WebAuthn; `<atlas-sign-in>` sets it after it is created in a browser. */
|
|
2462
|
+
passkeySupported = signal(false);
|
|
2344
2463
|
pollTimer = null;
|
|
2345
2464
|
identifier = '';
|
|
2346
2465
|
constructor(client, reload, pending,
|
|
2347
2466
|
/** Resolves the captcha messages' catalog keys. Defaults to en-US. */
|
|
2348
|
-
t = (key) => translate(EN_US, key)
|
|
2467
|
+
t = (key) => translate(EN_US, key),
|
|
2468
|
+
/** Wait for {@link continueAfterExistingAccount} (`<atlas-sign-up>` draws the notice). */
|
|
2469
|
+
holdExistingAccountNotice = false) {
|
|
2349
2470
|
this.client = client;
|
|
2350
2471
|
this.reload = reload;
|
|
2351
2472
|
this.t = t;
|
|
2473
|
+
this.holdExistingAccountNotice = holdExistingAccountNotice;
|
|
2352
2474
|
// Seeded from a redirect that came back mid-flow (e.g. OAuth needing a second
|
|
2353
2475
|
// factor) so the user resumes at the demanded step instead of starting over.
|
|
2354
2476
|
this.state = signal(pending ? flowFromPending(pending) : initialFlow);
|
|
2355
2477
|
this.enrollment = createMfaEnrollment(this.client, () => this.mfaChoices());
|
|
2356
2478
|
this.enrollmentState = signal(this.enrollment.state);
|
|
2357
2479
|
this.enrollment.subscribe((next) => this.enrollmentState.set(next));
|
|
2480
|
+
this.passkey = createPasskeySignInController({
|
|
2481
|
+
client: this.client,
|
|
2482
|
+
onStart: () => this.state.update((current) => ({ ...current, busy: true, errors: [] })),
|
|
2483
|
+
onResult: (next) => this.finish(next),
|
|
2484
|
+
captcha: () => this.captcha,
|
|
2485
|
+
cancelledMessage: () => this.t('signIn.passkeyCancelled'),
|
|
2486
|
+
});
|
|
2358
2487
|
this.syncCaptcha();
|
|
2359
2488
|
this.syncEnrollment();
|
|
2360
2489
|
}
|
|
2490
|
+
/** Why "Continue with Atlas" could not start (the server's message); null otherwise. */
|
|
2491
|
+
signInWithAtlasError = signal(null);
|
|
2492
|
+
/**
|
|
2493
|
+
* "Continue with Atlas" (when `settings().signInWithAtlas` is set): starts the
|
|
2494
|
+
* SSO sign-in and sends the browser to Atlas.
|
|
2495
|
+
*/
|
|
2496
|
+
async signInWithAtlas(connectionId) {
|
|
2497
|
+
if (typeof window === 'undefined')
|
|
2498
|
+
return;
|
|
2499
|
+
this.signInWithAtlasError.set(null);
|
|
2500
|
+
const started = await startSignInWithAtlas(this.client, connectionId, window.location.href);
|
|
2501
|
+
if ('url' in started)
|
|
2502
|
+
window.location.assign(started.url);
|
|
2503
|
+
else
|
|
2504
|
+
this.signInWithAtlasError.set(started.error);
|
|
2505
|
+
}
|
|
2506
|
+
/** "Sign in with a passkey" is offered: the first step, the instance turned passkey sign-in on, and the browser can. */
|
|
2507
|
+
passkeyOffered() {
|
|
2508
|
+
return this.passkeySupported() && !this.state().attempt && this.settings()?.passkeySignIn === true;
|
|
2509
|
+
}
|
|
2361
2510
|
/** What the app lets the MFA steps offer (from {@link settings}). */
|
|
2362
2511
|
mfaChoices() {
|
|
2363
2512
|
return mfaChoicesFrom(this.settings()?.account);
|
|
@@ -2388,9 +2537,27 @@ class AtlasFlowController {
|
|
|
2388
2537
|
firstFactors() {
|
|
2389
2538
|
const attempt = this.state().attempt;
|
|
2390
2539
|
const step = attempt ? nextStep(attempt) : null;
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2540
|
+
if (step?.kind !== 'collect_first_factor')
|
|
2541
|
+
return [];
|
|
2542
|
+
const offered = offeredFirstFactors(step.strategies, this.settings()?.emailDelivery);
|
|
2543
|
+
const proof = this.holdProof();
|
|
2544
|
+
return proof && !offered.includes(proof) ? [...offered, proof] : offered;
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* The emailed proof a password hold still waits on (`FlowState.passwordHold`): the
|
|
2548
|
+
* code, or a link where the instance verifies by link only. Null with no hold, once
|
|
2549
|
+
* proven, or while the app cannot deliver email.
|
|
2550
|
+
*/
|
|
2551
|
+
holdProof() {
|
|
2552
|
+
const hold = this.state().passwordHold;
|
|
2553
|
+
if (!hold || hold.released || this.settings()?.emailDelivery?.available === false)
|
|
2554
|
+
return null;
|
|
2555
|
+
return this.settings()?.account?.emailVerification === 'link' ? 'email_link' : 'email_code';
|
|
2556
|
+
}
|
|
2557
|
+
/** Back from the emailed factor to the password field. */
|
|
2558
|
+
usePassword() {
|
|
2559
|
+
this.preparedFactor.set(null);
|
|
2560
|
+
this.state.update((current) => ({ ...current, prepared: null, pollSecret: null, errors: [] }));
|
|
2394
2561
|
}
|
|
2395
2562
|
/**
|
|
2396
2563
|
* True when email + password sign-up cannot complete because the app cannot
|
|
@@ -2407,16 +2574,21 @@ class AtlasFlowController {
|
|
|
2407
2574
|
/**
|
|
2408
2575
|
* Ask the server to email a sign-in code for the current `needs_first_factor`
|
|
2409
2576
|
* step (`POST …/prepare_first_factor`); the form then collects the code, which
|
|
2410
|
-
* {@link submit} posts as the `email_code` strategy.
|
|
2577
|
+
* {@link submit} posts as the `email_code` strategy. While a password hold waits
|
|
2578
|
+
* on it ({@link holdProof}), this emails that proof instead.
|
|
2411
2579
|
*/
|
|
2412
2580
|
async prepare(strategy) {
|
|
2413
2581
|
if (!this.firstFactors().includes(strategy))
|
|
2414
2582
|
return;
|
|
2583
|
+
const proof = this.holdProof();
|
|
2415
2584
|
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
2416
|
-
const next =
|
|
2585
|
+
const next = strategy === proof
|
|
2586
|
+
? await preparePasswordHoldProof(this.client, this.state(), proof)
|
|
2587
|
+
: await prepareFactor(this.client, this.state(), strategy);
|
|
2417
2588
|
this.setState(next);
|
|
2418
2589
|
if (!next.errors.length)
|
|
2419
2590
|
this.preparedFactor.set(strategy);
|
|
2591
|
+
this.syncPolling();
|
|
2420
2592
|
}
|
|
2421
2593
|
/** Keep both captchas in step with the settings and the flow's current step. */
|
|
2422
2594
|
syncCaptcha() {
|
|
@@ -2432,8 +2604,12 @@ class AtlasFlowController {
|
|
|
2432
2604
|
this.signUpCaptchaDrawn.set(this.signUpCaptcha.needed);
|
|
2433
2605
|
}
|
|
2434
2606
|
setState(next) {
|
|
2435
|
-
const
|
|
2607
|
+
const previous = this.state();
|
|
2608
|
+
const previousId = previous.attempt?.id ?? null;
|
|
2436
2609
|
this.state.set(next);
|
|
2610
|
+
// A proven hold goes back to the password field.
|
|
2611
|
+
if (next.passwordHold?.released && !previous.passwordHold?.released)
|
|
2612
|
+
this.preparedFactor.set(null);
|
|
2437
2613
|
// A prepared factor belongs to the first-factor step it was sent for.
|
|
2438
2614
|
if (next.attempt?.status !== 'needs_first_factor')
|
|
2439
2615
|
this.preparedFactor.set(null);
|
|
@@ -2469,6 +2645,11 @@ class AtlasFlowController {
|
|
|
2469
2645
|
*/
|
|
2470
2646
|
async submit(values) {
|
|
2471
2647
|
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
2648
|
+
// A code that lifts a password hold goes to its own attempt, not this sign-in.
|
|
2649
|
+
if (this.preparedFactor() === 'email_code' && this.holdProof() === 'email_code') {
|
|
2650
|
+
this.setState(await attemptPasswordHoldProof(this.client, this.state(), values.code ?? ''));
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2472
2653
|
const out = await advanceWithCaptcha(this.client, this.state(), values, {
|
|
2473
2654
|
captcha: this.captcha,
|
|
2474
2655
|
identifier: this.identifier,
|
|
@@ -2564,6 +2745,14 @@ class AtlasFlowController {
|
|
|
2564
2745
|
async saveRecoveryCodes() {
|
|
2565
2746
|
await this.finish(acknowledgeRecoveryCodes(this.state()));
|
|
2566
2747
|
}
|
|
2748
|
+
/** The address belonged to an existing account: the notice is up until {@link continueAfterExistingAccount}. */
|
|
2749
|
+
existingAccountNotice() {
|
|
2750
|
+
return pendingExistingAccountNotice(this.state());
|
|
2751
|
+
}
|
|
2752
|
+
/** The user read the notice: finish the sign-up. */
|
|
2753
|
+
async continueAfterExistingAccount() {
|
|
2754
|
+
await this.finish(acknowledgeExistingAccount(this.state()));
|
|
2755
|
+
}
|
|
2567
2756
|
async finish(next) {
|
|
2568
2757
|
// An enrolment's recovery codes are shown first; `saveRecoveryCodes` finishes.
|
|
2569
2758
|
if (pendingRecoveryCodes(next)) {
|
|
@@ -2582,6 +2771,10 @@ class AtlasFlowController {
|
|
|
2582
2771
|
});
|
|
2583
2772
|
exchanged = res.ok;
|
|
2584
2773
|
}
|
|
2774
|
+
if (exchanged && this.holdExistingAccountNotice && pendingExistingAccountNotice(next)) {
|
|
2775
|
+
this.setState({ ...next, ticket: null, busy: false });
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2585
2778
|
const kept = exchanged && (await this.reload());
|
|
2586
2779
|
this.setState({ ...next, busy: false, sessionKept: kept });
|
|
2587
2780
|
}
|
|
@@ -2632,6 +2825,7 @@ class AtlasFlowController {
|
|
|
2632
2825
|
}
|
|
2633
2826
|
/** Release the poll timer and the captcha widgets — call from the component's `ngOnDestroy`. */
|
|
2634
2827
|
destroy() {
|
|
2828
|
+
this.passkey.stopAutofill();
|
|
2635
2829
|
this.stopPolling();
|
|
2636
2830
|
this.captcha.destroy();
|
|
2637
2831
|
this.signUpCaptcha.destroy();
|
|
@@ -2667,6 +2861,12 @@ class SignInComponent {
|
|
|
2667
2861
|
return null;
|
|
2668
2862
|
return lastUsedMethodLabel(this.flow.lastMethod(), (key) => this.t(key));
|
|
2669
2863
|
}
|
|
2864
|
+
/** The "Sign in with Atlas" connection, on the first step, when the instance has it on. */
|
|
2865
|
+
atlasConnection() {
|
|
2866
|
+
if (this.flow.state().attempt || this.flow.recoveryCodes())
|
|
2867
|
+
return null;
|
|
2868
|
+
return this.flow.settings()?.signInWithAtlas?.connectionId ?? null;
|
|
2869
|
+
}
|
|
2670
2870
|
signUpHref() {
|
|
2671
2871
|
if (this.signUpUrl === null)
|
|
2672
2872
|
return null;
|
|
@@ -2686,7 +2886,16 @@ class SignInComponent {
|
|
|
2686
2886
|
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(),
|
|
2687
2887
|
// Resume mid-flow if an OAuth redirect came back owing a second factor.
|
|
2688
2888
|
this.atlas.pendingAttempt, (key) => translate(this.atlas.localization, key));
|
|
2889
|
+
// Passkey autofill runs while the first step offers passkeys (a computed, so it re-arms only when that changes).
|
|
2890
|
+
passkeyOffered = computed(() => this.flow.passkeyOffered());
|
|
2891
|
+
passkeyAutofill = effect(() => (this.passkeyOffered() ? this.flow.passkey.armAutofill() : this.flow.passkey.stopAutofill()));
|
|
2892
|
+
submitIdentifier(values) {
|
|
2893
|
+
this.flow.passkey.stopAutofill();
|
|
2894
|
+
void this.flow.submit(values);
|
|
2895
|
+
}
|
|
2689
2896
|
ngOnInit() {
|
|
2897
|
+
if (typeof window !== 'undefined')
|
|
2898
|
+
this.flow.passkeySupported.set(passkeysSupported());
|
|
2690
2899
|
// If seeded from a pending attempt that is already pollable, start polling.
|
|
2691
2900
|
this.flow.syncPolling();
|
|
2692
2901
|
// §5 the instance's captcha settings (switching captcha on in the dashboard
|
|
@@ -2705,6 +2914,14 @@ class SignInComponent {
|
|
|
2705
2914
|
{{ t('signIn.lastUsed') }}: {{ label }}
|
|
2706
2915
|
</p>
|
|
2707
2916
|
}
|
|
2917
|
+
@if (atlasConnection(); as connectionId) {
|
|
2918
|
+
<button type="button" class="atlas-social-button" data-atlas-provider="atlas" (click)="flow.signInWithAtlas(connectionId)">
|
|
2919
|
+
{{ t('signIn.continueWithAtlas') }}
|
|
2920
|
+
</button>
|
|
2921
|
+
@if (flow.signInWithAtlasError(); as message) {
|
|
2922
|
+
<p class="atlas-error" role="alert">{{ message }}</p>
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2708
2925
|
@if (flow.recoveryCodes(); as codes) {
|
|
2709
2926
|
<atlas-recovery-codes [codes]="codes" [busy]="flow.state().busy" (saved)="flow.saveRecoveryCodes()"></atlas-recovery-codes>
|
|
2710
2927
|
} @else {
|
|
@@ -2717,9 +2934,12 @@ class SignInComponent {
|
|
|
2717
2934
|
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
2718
2935
|
[firstFactors]="flow.firstFactors()"
|
|
2719
2936
|
[preparedFactor]="flow.preparedFactor()"
|
|
2937
|
+
[passwordHold]="flow.state().passwordHold ?? null"
|
|
2938
|
+
[holdProof]="flow.holdProof()"
|
|
2939
|
+
(usePassword)="flow.usePassword()"
|
|
2720
2940
|
[offerEmailSecondFactor]="flow.settings()?.account?.emailSecondFactor === true"
|
|
2721
2941
|
[emailedSecondFactor]="flow.emailSecondFactor()"
|
|
2722
|
-
(submitFlow)="
|
|
2942
|
+
(submitFlow)="submitIdentifier($event)"
|
|
2723
2943
|
(chooseFactor)="flow.prepare($event)"
|
|
2724
2944
|
[offerSmsSecondFactor]="flow.settings() !== null && flow.mfaChoices().sms"
|
|
2725
2945
|
[textedSecondFactor]="flow.smsSecondFactor()"
|
|
@@ -2730,12 +2950,23 @@ class SignInComponent {
|
|
|
2730
2950
|
(policyDeclined)="flow.declinePolicy($event)"
|
|
2731
2951
|
></atlas-attempt-flow>
|
|
2732
2952
|
}
|
|
2953
|
+
@if (flow.passkeyOffered() && !flow.recoveryCodes()) {
|
|
2954
|
+
<button
|
|
2955
|
+
type="button"
|
|
2956
|
+
class="atlas-social-button atlas-passkey-button"
|
|
2957
|
+
data-atlas-passkey-sign-in=""
|
|
2958
|
+
[disabled]="flow.state().busy"
|
|
2959
|
+
(click)="flow.passkey.signIn()"
|
|
2960
|
+
>
|
|
2961
|
+
{{ t('signIn.passkeyButton') }}
|
|
2962
|
+
</button>
|
|
2963
|
+
}
|
|
2733
2964
|
@if (flow.state().attempt?.status !== 'complete' && signUpHref(); as href) {
|
|
2734
2965
|
<p class="atlas-footer" data-atlas-sign-up-prompt="">
|
|
2735
2966
|
{{ t('signIn.noAccount') }} <a class="atlas-link" [href]="href">{{ t('signIn.signUpLink') }}</a>
|
|
2736
2967
|
</p>
|
|
2737
2968
|
}
|
|
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 });
|
|
2969
|
+
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "sessionKept", "titleKey", "captcha", "firstFactors", "preparedFactor", "passwordHold", "holdProof", "offerEmailSecondFactor", "emailedSecondFactor", "offerSmsSecondFactor", "textedSecondFactor", "enrollment", "enrollmentState", "emailVerification"], outputs: ["submitFlow", "chooseFactor", "usePassword", "emailSecondFactor", "smsSecondFactor", "resend", "policyDeclined"] }, { kind: "component", type: RecoveryCodesComponent, selector: "atlas-recovery-codes", inputs: ["codes", "busy"], outputs: ["saved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2739
2970
|
}
|
|
2740
2971
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignInComponent, decorators: [{
|
|
2741
2972
|
type: Component,
|
|
@@ -2750,6 +2981,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2750
2981
|
{{ t('signIn.lastUsed') }}: {{ label }}
|
|
2751
2982
|
</p>
|
|
2752
2983
|
}
|
|
2984
|
+
@if (atlasConnection(); as connectionId) {
|
|
2985
|
+
<button type="button" class="atlas-social-button" data-atlas-provider="atlas" (click)="flow.signInWithAtlas(connectionId)">
|
|
2986
|
+
{{ t('signIn.continueWithAtlas') }}
|
|
2987
|
+
</button>
|
|
2988
|
+
@if (flow.signInWithAtlasError(); as message) {
|
|
2989
|
+
<p class="atlas-error" role="alert">{{ message }}</p>
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2753
2992
|
@if (flow.recoveryCodes(); as codes) {
|
|
2754
2993
|
<atlas-recovery-codes [codes]="codes" [busy]="flow.state().busy" (saved)="flow.saveRecoveryCodes()"></atlas-recovery-codes>
|
|
2755
2994
|
} @else {
|
|
@@ -2762,9 +3001,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2762
3001
|
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
2763
3002
|
[firstFactors]="flow.firstFactors()"
|
|
2764
3003
|
[preparedFactor]="flow.preparedFactor()"
|
|
3004
|
+
[passwordHold]="flow.state().passwordHold ?? null"
|
|
3005
|
+
[holdProof]="flow.holdProof()"
|
|
3006
|
+
(usePassword)="flow.usePassword()"
|
|
2765
3007
|
[offerEmailSecondFactor]="flow.settings()?.account?.emailSecondFactor === true"
|
|
2766
3008
|
[emailedSecondFactor]="flow.emailSecondFactor()"
|
|
2767
|
-
(submitFlow)="
|
|
3009
|
+
(submitFlow)="submitIdentifier($event)"
|
|
2768
3010
|
(chooseFactor)="flow.prepare($event)"
|
|
2769
3011
|
[offerSmsSecondFactor]="flow.settings() !== null && flow.mfaChoices().sms"
|
|
2770
3012
|
[textedSecondFactor]="flow.smsSecondFactor()"
|
|
@@ -2775,6 +3017,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2775
3017
|
(policyDeclined)="flow.declinePolicy($event)"
|
|
2776
3018
|
></atlas-attempt-flow>
|
|
2777
3019
|
}
|
|
3020
|
+
@if (flow.passkeyOffered() && !flow.recoveryCodes()) {
|
|
3021
|
+
<button
|
|
3022
|
+
type="button"
|
|
3023
|
+
class="atlas-social-button atlas-passkey-button"
|
|
3024
|
+
data-atlas-passkey-sign-in=""
|
|
3025
|
+
[disabled]="flow.state().busy"
|
|
3026
|
+
(click)="flow.passkey.signIn()"
|
|
3027
|
+
>
|
|
3028
|
+
{{ t('signIn.passkeyButton') }}
|
|
3029
|
+
</button>
|
|
3030
|
+
}
|
|
2778
3031
|
@if (flow.state().attempt?.status !== 'complete' && signUpHref(); as href) {
|
|
2779
3032
|
<p class="atlas-footer" data-atlas-sign-up-prompt="">
|
|
2780
3033
|
{{ t('signIn.noAccount') }} <a class="atlas-link" [href]="href">{{ t('signIn.signUpLink') }}</a>
|
|
@@ -3361,7 +3614,9 @@ class SignUpComponent {
|
|
|
3361
3614
|
atlas = inject(AtlasService);
|
|
3362
3615
|
/** Where to send the user once the flow completes. Reserved for host routing. */
|
|
3363
3616
|
afterUrl;
|
|
3364
|
-
|
|
3617
|
+
/** Where the existing-account notice's "Forgot password?" goes. Default: the hosted sign-in page; `null` hides it. */
|
|
3618
|
+
resetPasswordUrl;
|
|
3619
|
+
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(), this.atlas.pendingAttempt, (key) => translate(this.atlas.localization, key), true);
|
|
3365
3620
|
/** Password or passkey-only sign-up (the latter when the instance offers it). */
|
|
3366
3621
|
mode = signal('password');
|
|
3367
3622
|
canPasskey() {
|
|
@@ -3375,6 +3630,9 @@ class SignUpComponent {
|
|
|
3375
3630
|
this.flow.state.update((current) => ({ ...current, errors: [] }));
|
|
3376
3631
|
this.mode.set(this.passkeyForm() ? 'password' : 'passkey');
|
|
3377
3632
|
}
|
|
3633
|
+
resetHref() {
|
|
3634
|
+
return existingAccountResetUrl({ url: this.resetPasswordUrl, frontendApi: this.atlas.frontendApi, redirectUrl: this.afterUrl });
|
|
3635
|
+
}
|
|
3378
3636
|
awaitingPasskey() {
|
|
3379
3637
|
return awaitingPasskey(this.flow.state().attempt);
|
|
3380
3638
|
}
|
|
@@ -3399,8 +3657,24 @@ class SignUpComponent {
|
|
|
3399
3657
|
this.flow.destroy();
|
|
3400
3658
|
}
|
|
3401
3659
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
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: `
|
|
3403
|
-
@if (
|
|
3660
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SignUpComponent, isStandalone: true, selector: "atlas-sign-up", inputs: { afterUrl: "afterUrl", resetPasswordUrl: "resetPasswordUrl" }, ngImport: i0, template: `
|
|
3661
|
+
@if (flow.existingAccountNotice()) {
|
|
3662
|
+
<div [class]="cls('card', 'atlas-card')" data-atlas-existing-account="">
|
|
3663
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
3664
|
+
<p class="atlas-text">{{ t('signUp.existingAccount') }}</p>
|
|
3665
|
+
@if (resetHref(); as href) {
|
|
3666
|
+
<p class="atlas-text"><a class="atlas-link" [href]="href">{{ t('signIn.forgotPassword') }}</a></p>
|
|
3667
|
+
}
|
|
3668
|
+
<button
|
|
3669
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
3670
|
+
type="button"
|
|
3671
|
+
[disabled]="flow.state().busy"
|
|
3672
|
+
(click)="flow.continueAfterExistingAccount()"
|
|
3673
|
+
>
|
|
3674
|
+
{{ t('signUp.submit') }}
|
|
3675
|
+
</button>
|
|
3676
|
+
</div>
|
|
3677
|
+
} @else if (awaitingPasskey()) {
|
|
3404
3678
|
<div [class]="cls('card', 'atlas-card')" data-atlas-passkey-sign-up="">
|
|
3405
3679
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
3406
3680
|
@for (error of flow.state().errors; track error.code) {
|
|
@@ -3442,7 +3716,7 @@ class SignUpComponent {
|
|
|
3442
3716
|
"
|
|
3443
3717
|
></atlas-sign-up-form>
|
|
3444
3718
|
}
|
|
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 });
|
|
3719
|
+
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "sessionKept", "titleKey", "captcha", "firstFactors", "preparedFactor", "passwordHold", "holdProof", "offerEmailSecondFactor", "emailedSecondFactor", "offerSmsSecondFactor", "textedSecondFactor", "enrollment", "enrollmentState", "emailVerification"], outputs: ["submitFlow", "chooseFactor", "usePassword", "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 });
|
|
3446
3720
|
}
|
|
3447
3721
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, decorators: [{
|
|
3448
3722
|
type: Component,
|
|
@@ -3452,7 +3726,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
3452
3726
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3453
3727
|
imports: [AttemptFlowComponent, SignUpFormComponent],
|
|
3454
3728
|
template: `
|
|
3455
|
-
@if (
|
|
3729
|
+
@if (flow.existingAccountNotice()) {
|
|
3730
|
+
<div [class]="cls('card', 'atlas-card')" data-atlas-existing-account="">
|
|
3731
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
3732
|
+
<p class="atlas-text">{{ t('signUp.existingAccount') }}</p>
|
|
3733
|
+
@if (resetHref(); as href) {
|
|
3734
|
+
<p class="atlas-text"><a class="atlas-link" [href]="href">{{ t('signIn.forgotPassword') }}</a></p>
|
|
3735
|
+
}
|
|
3736
|
+
<button
|
|
3737
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
3738
|
+
type="button"
|
|
3739
|
+
[disabled]="flow.state().busy"
|
|
3740
|
+
(click)="flow.continueAfterExistingAccount()"
|
|
3741
|
+
>
|
|
3742
|
+
{{ t('signUp.submit') }}
|
|
3743
|
+
</button>
|
|
3744
|
+
</div>
|
|
3745
|
+
} @else if (awaitingPasskey()) {
|
|
3456
3746
|
<div [class]="cls('card', 'atlas-card')" data-atlas-passkey-sign-up="">
|
|
3457
3747
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
3458
3748
|
@for (error of flow.state().errors; track error.code) {
|
|
@@ -3498,6 +3788,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
3498
3788
|
}]
|
|
3499
3789
|
}], propDecorators: { afterUrl: [{
|
|
3500
3790
|
type: Input
|
|
3791
|
+
}], resetPasswordUrl: [{
|
|
3792
|
+
type: Input
|
|
3501
3793
|
}] } });
|
|
3502
3794
|
|
|
3503
3795
|
/**
|