@atlasauth/angular 0.13.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 +14 -0
- package/dist/esm2022/lib/components/attempt-flow.component.mjs +100 -5
- package/dist/esm2022/lib/components/flow-controller.mjs +57 -9
- package/dist/esm2022/lib/components/sign-in.component.mjs +8 -2
- package/dist/esm2022/lib/components/sign-up.component.mjs +46 -7
- package/dist/esm2022/lib/i18n.mjs +11 -1
- package/dist/esm2022/lib/version.mjs +2 -2
- package/dist/fesm2022/atlasauth-angular.mjs +217 -19
- package/dist/fesm2022/atlasauth-angular.mjs.map +1 -1
- package/dist/lib/components/attempt-flow.component.d.ts +46 -6
- package/dist/lib/components/flow-controller.d.ts +21 -4
- package/dist/lib/components/sign-up.component.d.ts +4 -1
- package/dist/lib/version.d.ts +1 -1
- package/package.json +6 -6
|
@@ -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, createPasskeySignInController, startSignInWithAtlas, mfaChoicesFrom, fetchInstanceSettings, fetchLastSignInMethod, offeredFirstFactors, prepareFactor, signInCaptchaOptions, flowNeedsCaptcha, planFirstFactor, advanceWithCaptcha, loadPolicies, createSignUp, prepareSecondFactorCode, resendSignUpVerification, createPasskeySignUp, finishPasskeySignUp, pendingRecoveryCodes, acknowledgeRecoveryCodes, shouldKeepPolling, pollAttempt, lastUsedMethodLabel, signUp, passkeysSupported, confirmPolicies, awaitingPasskey, listCibaRequests, getCibaPin, decideCibaRequest, setCibaPin, clearCibaPin, OrganizationProfileController, describeAuditAction, actorFromClaims } 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, 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';
|
|
@@ -207,6 +207,12 @@ const EN_US = {
|
|
|
207
207
|
'signIn.continueWithAtlas': 'Continue with Atlas',
|
|
208
208
|
'signIn.emailCodeLabel': 'Verification code',
|
|
209
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.',
|
|
210
216
|
'signIn.codeSentTo': 'We sent a code to {email}. Enter it below.',
|
|
211
217
|
'signIn.resendCode': 'Resend code',
|
|
212
218
|
'signIn.sending': 'Sending…',
|
|
@@ -366,6 +372,9 @@ const EN_US = {
|
|
|
366
372
|
'error.email_delivery_unavailable': "This app can't send email right now. Try another sign-in method or contact the app owner.",
|
|
367
373
|
'error.strategy_disabled': 'This sign-in method is not available for this app.',
|
|
368
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.',
|
|
369
378
|
'error.waitlisted': "You're on the waitlist. We'll email you when a place opens up.",
|
|
370
379
|
'error.identifier_not_allowed': 'This email or phone number is not allowed on this app.',
|
|
371
380
|
'error.disposable_email_blocked': 'Sign-up with a disposable or temporary email address is not allowed.',
|
|
@@ -374,6 +383,7 @@ const EN_US = {
|
|
|
374
383
|
'error.sign_ups_disabled': 'Sign-ups are temporarily disabled.',
|
|
375
384
|
'error.high_risk_blocked': 'This sign-in was blocked because it looked unusual.',
|
|
376
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.',
|
|
377
387
|
};
|
|
378
388
|
/**
|
|
379
389
|
* Look up a key and interpolate `{placeholders}`.
|
|
@@ -392,7 +402,7 @@ function withFallback(locale) {
|
|
|
392
402
|
}
|
|
393
403
|
|
|
394
404
|
/** This package's version, sent as `X-Atlas-SDK` (kept in step with package.json by a test). */
|
|
395
|
-
const ATLAS_ANGULAR_VERSION = '0.
|
|
405
|
+
const ATLAS_ANGULAR_VERSION = '0.14.0';
|
|
396
406
|
const ATLAS_ANGULAR_SDK = { name: '@atlasauth/angular', version: ATLAS_ANGULAR_VERSION };
|
|
397
407
|
|
|
398
408
|
/**
|
|
@@ -1484,10 +1494,16 @@ class AttemptFlowComponent {
|
|
|
1484
1494
|
firstFactors = null;
|
|
1485
1495
|
/** The first factor already sent (`AtlasFlowController.preparedFactor()`): its code is collected. */
|
|
1486
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;
|
|
1487
1501
|
/** Emits the collected field values when the form is submitted. */
|
|
1488
1502
|
submitFlow = new EventEmitter();
|
|
1489
1503
|
/** Emits when the user picks a first factor the server must send first (an emailed code). */
|
|
1490
1504
|
chooseFactor = new EventEmitter();
|
|
1505
|
+
/** Emits on "Use your password instead", from an emailed factor already sent. */
|
|
1506
|
+
usePassword = new EventEmitter();
|
|
1491
1507
|
/**
|
|
1492
1508
|
* Offer "Email me a code instead" at the second factor (the instance's
|
|
1493
1509
|
* `account.emailSecondFactor`); `emailSecondFactor` asks for it and
|
|
@@ -1548,6 +1564,11 @@ class AttemptFlowComponent {
|
|
|
1548
1564
|
});
|
|
1549
1565
|
return;
|
|
1550
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
|
+
}
|
|
1551
1572
|
this.submitFlow.emit(this.values);
|
|
1552
1573
|
}
|
|
1553
1574
|
/** The shared enrolment step is drawn (a controller was passed and the flow is enrolling). */
|
|
@@ -1619,7 +1640,13 @@ class AttemptFlowComponent {
|
|
|
1619
1640
|
codeSentText() {
|
|
1620
1641
|
if (this.step().kind !== 'collect_first_factor' || this.preparedFactor !== 'email_code')
|
|
1621
1642
|
return null;
|
|
1622
|
-
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 ?? '' });
|
|
1623
1650
|
}
|
|
1624
1651
|
/** The attempt is `needs_policy_acceptance`. */
|
|
1625
1652
|
awaitingPolicies() {
|
|
@@ -1630,7 +1657,7 @@ class AttemptFlowComponent {
|
|
|
1630
1657
|
// Nothing to submit on a first-factor step that only offers an emailed code
|
|
1631
1658
|
// until that code is sent.
|
|
1632
1659
|
if (kind === 'collect_first_factor')
|
|
1633
|
-
return this.fields().
|
|
1660
|
+
return this.fields().some((f) => f.kind === 'input');
|
|
1634
1661
|
if (this.linkOnly())
|
|
1635
1662
|
return false;
|
|
1636
1663
|
// Enrolment: nothing to submit while choosing a method or fetching the setup.
|
|
@@ -1645,15 +1672,24 @@ class AttemptFlowComponent {
|
|
|
1645
1672
|
// `webauthn`: saved passkeys appear in this field's autofill while <atlas-sign-in> holds a conditional request.
|
|
1646
1673
|
return [{ kind: 'input', name: 'identifier', labelKey: 'signIn.identifierLabel', type: 'email', autocomplete: 'username webauthn' }];
|
|
1647
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
|
+
}
|
|
1648
1679
|
if (this.preparedFactor === 'email_code') {
|
|
1649
1680
|
return [{ kind: 'input', name: 'code', labelKey: 'signIn.emailCodeLabel', type: 'text' }];
|
|
1650
1681
|
}
|
|
1682
|
+
if (this.preparedFactor === 'email_link')
|
|
1683
|
+
return [];
|
|
1651
1684
|
// The password field unless the only offer is an emailed code (chosen
|
|
1652
1685
|
// with the button below the form).
|
|
1653
1686
|
const offered = this.offered();
|
|
1654
1687
|
return offered.includes('email_code') && !offered.includes('password')
|
|
1655
1688
|
? []
|
|
1656
|
-
: [
|
|
1689
|
+
: [
|
|
1690
|
+
...(this.passwordHold?.released ? [released()] : []),
|
|
1691
|
+
{ kind: 'input', name: 'password', labelKey: 'signIn.passwordLabel', type: 'password' },
|
|
1692
|
+
];
|
|
1657
1693
|
}
|
|
1658
1694
|
case 'collect_second_factor':
|
|
1659
1695
|
return [{ kind: 'input', name: 'code', labelKey: 'mfa.codeLabel', type: 'text' }];
|
|
@@ -1697,7 +1733,7 @@ class AttemptFlowComponent {
|
|
|
1697
1733
|
}
|
|
1698
1734
|
}
|
|
1699
1735
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1700
|
-
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: `
|
|
1701
1737
|
<div [class]="cls('card', 'atlas-card')">
|
|
1702
1738
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
1703
1739
|
|
|
@@ -1709,6 +1745,9 @@ class AttemptFlowComponent {
|
|
|
1709
1745
|
@if (codeSentText(); as sentText) {
|
|
1710
1746
|
<p class="atlas-text" data-atlas-code-sent="">{{ sentText }}</p>
|
|
1711
1747
|
}
|
|
1748
|
+
@if (linkSentText(); as sentLink) {
|
|
1749
|
+
<p class="atlas-text" data-atlas-link-sent="">{{ sentLink }}</p>
|
|
1750
|
+
}
|
|
1712
1751
|
@if (linkOnly()) {
|
|
1713
1752
|
<!-- Nothing to type: the flow polls until the emailed link is opened (anywhere). -->
|
|
1714
1753
|
<p class="atlas-text" data-atlas-link-sent="" aria-live="polite">
|
|
@@ -1817,6 +1856,8 @@ class AttemptFlowComponent {
|
|
|
1817
1856
|
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1818
1857
|
}
|
|
1819
1858
|
</label>
|
|
1859
|
+
} @else if (f.kind === 'released') {
|
|
1860
|
+
<p class="atlas-text" data-atlas-password-released="">{{ t(f.labelKey) }}</p>
|
|
1820
1861
|
} @else if (f.kind === 'alert') {
|
|
1821
1862
|
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ t(f.labelKey) }}</p>
|
|
1822
1863
|
} @else {
|
|
@@ -1898,6 +1939,31 @@ class AttemptFlowComponent {
|
|
|
1898
1939
|
{{ t(preparedFactor === 'email_code' ? 'signIn.resendCode' : 'signIn.useEmailCode') }}
|
|
1899
1940
|
</button>
|
|
1900
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
|
+
}
|
|
1901
1967
|
</form>
|
|
1902
1968
|
</div>
|
|
1903
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 });
|
|
@@ -1921,6 +1987,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1921
1987
|
@if (codeSentText(); as sentText) {
|
|
1922
1988
|
<p class="atlas-text" data-atlas-code-sent="">{{ sentText }}</p>
|
|
1923
1989
|
}
|
|
1990
|
+
@if (linkSentText(); as sentLink) {
|
|
1991
|
+
<p class="atlas-text" data-atlas-link-sent="">{{ sentLink }}</p>
|
|
1992
|
+
}
|
|
1924
1993
|
@if (linkOnly()) {
|
|
1925
1994
|
<!-- Nothing to type: the flow polls until the emailed link is opened (anywhere). -->
|
|
1926
1995
|
<p class="atlas-text" data-atlas-link-sent="" aria-live="polite">
|
|
@@ -2029,6 +2098,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2029
2098
|
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
2030
2099
|
}
|
|
2031
2100
|
</label>
|
|
2101
|
+
} @else if (f.kind === 'released') {
|
|
2102
|
+
<p class="atlas-text" data-atlas-password-released="">{{ t(f.labelKey) }}</p>
|
|
2032
2103
|
} @else if (f.kind === 'alert') {
|
|
2033
2104
|
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ t(f.labelKey) }}</p>
|
|
2034
2105
|
} @else {
|
|
@@ -2110,6 +2181,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2110
2181
|
{{ t(preparedFactor === 'email_code' ? 'signIn.resendCode' : 'signIn.useEmailCode') }}
|
|
2111
2182
|
</button>
|
|
2112
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
|
+
}
|
|
2113
2209
|
</form>
|
|
2114
2210
|
</div>
|
|
2115
2211
|
`,
|
|
@@ -2130,10 +2226,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2130
2226
|
type: Input
|
|
2131
2227
|
}], preparedFactor: [{
|
|
2132
2228
|
type: Input
|
|
2229
|
+
}], passwordHold: [{
|
|
2230
|
+
type: Input
|
|
2231
|
+
}], holdProof: [{
|
|
2232
|
+
type: Input
|
|
2133
2233
|
}], submitFlow: [{
|
|
2134
2234
|
type: Output
|
|
2135
2235
|
}], chooseFactor: [{
|
|
2136
2236
|
type: Output
|
|
2237
|
+
}], usePassword: [{
|
|
2238
|
+
type: Output
|
|
2137
2239
|
}], offerEmailSecondFactor: [{
|
|
2138
2240
|
type: Input
|
|
2139
2241
|
}], emailedSecondFactor: [{
|
|
@@ -2161,6 +2263,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2161
2263
|
function msg(labelKey) {
|
|
2162
2264
|
return { kind: 'message', name: '', labelKey, type: 'text' };
|
|
2163
2265
|
}
|
|
2266
|
+
function released() {
|
|
2267
|
+
return { kind: 'released', name: '', labelKey: 'signIn.passwordHoldReleased', type: 'text' };
|
|
2268
|
+
}
|
|
2164
2269
|
|
|
2165
2270
|
/**
|
|
2166
2271
|
* The recovery codes a mid-sign-in MFA enrolment came with, shown once before
|
|
@@ -2296,6 +2401,7 @@ class AtlasFlowController {
|
|
|
2296
2401
|
client;
|
|
2297
2402
|
reload;
|
|
2298
2403
|
t;
|
|
2404
|
+
holdExistingAccountNotice;
|
|
2299
2405
|
state;
|
|
2300
2406
|
/**
|
|
2301
2407
|
* The instance's public sign-in / sign-up settings (captcha, sign-up fields,
|
|
@@ -2358,10 +2464,13 @@ class AtlasFlowController {
|
|
|
2358
2464
|
identifier = '';
|
|
2359
2465
|
constructor(client, reload, pending,
|
|
2360
2466
|
/** Resolves the captcha messages' catalog keys. Defaults to en-US. */
|
|
2361
|
-
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) {
|
|
2362
2470
|
this.client = client;
|
|
2363
2471
|
this.reload = reload;
|
|
2364
2472
|
this.t = t;
|
|
2473
|
+
this.holdExistingAccountNotice = holdExistingAccountNotice;
|
|
2365
2474
|
// Seeded from a redirect that came back mid-flow (e.g. OAuth needing a second
|
|
2366
2475
|
// factor) so the user resumes at the demanded step instead of starting over.
|
|
2367
2476
|
this.state = signal(pending ? flowFromPending(pending) : initialFlow);
|
|
@@ -2428,9 +2537,27 @@ class AtlasFlowController {
|
|
|
2428
2537
|
firstFactors() {
|
|
2429
2538
|
const attempt = this.state().attempt;
|
|
2430
2539
|
const step = attempt ? nextStep(attempt) : null;
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
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: [] }));
|
|
2434
2561
|
}
|
|
2435
2562
|
/**
|
|
2436
2563
|
* True when email + password sign-up cannot complete because the app cannot
|
|
@@ -2447,16 +2574,21 @@ class AtlasFlowController {
|
|
|
2447
2574
|
/**
|
|
2448
2575
|
* Ask the server to email a sign-in code for the current `needs_first_factor`
|
|
2449
2576
|
* step (`POST …/prepare_first_factor`); the form then collects the code, which
|
|
2450
|
-
* {@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.
|
|
2451
2579
|
*/
|
|
2452
2580
|
async prepare(strategy) {
|
|
2453
2581
|
if (!this.firstFactors().includes(strategy))
|
|
2454
2582
|
return;
|
|
2583
|
+
const proof = this.holdProof();
|
|
2455
2584
|
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
2456
|
-
const next =
|
|
2585
|
+
const next = strategy === proof
|
|
2586
|
+
? await preparePasswordHoldProof(this.client, this.state(), proof)
|
|
2587
|
+
: await prepareFactor(this.client, this.state(), strategy);
|
|
2457
2588
|
this.setState(next);
|
|
2458
2589
|
if (!next.errors.length)
|
|
2459
2590
|
this.preparedFactor.set(strategy);
|
|
2591
|
+
this.syncPolling();
|
|
2460
2592
|
}
|
|
2461
2593
|
/** Keep both captchas in step with the settings and the flow's current step. */
|
|
2462
2594
|
syncCaptcha() {
|
|
@@ -2472,8 +2604,12 @@ class AtlasFlowController {
|
|
|
2472
2604
|
this.signUpCaptchaDrawn.set(this.signUpCaptcha.needed);
|
|
2473
2605
|
}
|
|
2474
2606
|
setState(next) {
|
|
2475
|
-
const
|
|
2607
|
+
const previous = this.state();
|
|
2608
|
+
const previousId = previous.attempt?.id ?? null;
|
|
2476
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);
|
|
2477
2613
|
// A prepared factor belongs to the first-factor step it was sent for.
|
|
2478
2614
|
if (next.attempt?.status !== 'needs_first_factor')
|
|
2479
2615
|
this.preparedFactor.set(null);
|
|
@@ -2509,6 +2645,11 @@ class AtlasFlowController {
|
|
|
2509
2645
|
*/
|
|
2510
2646
|
async submit(values) {
|
|
2511
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
|
+
}
|
|
2512
2653
|
const out = await advanceWithCaptcha(this.client, this.state(), values, {
|
|
2513
2654
|
captcha: this.captcha,
|
|
2514
2655
|
identifier: this.identifier,
|
|
@@ -2604,6 +2745,14 @@ class AtlasFlowController {
|
|
|
2604
2745
|
async saveRecoveryCodes() {
|
|
2605
2746
|
await this.finish(acknowledgeRecoveryCodes(this.state()));
|
|
2606
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
|
+
}
|
|
2607
2756
|
async finish(next) {
|
|
2608
2757
|
// An enrolment's recovery codes are shown first; `saveRecoveryCodes` finishes.
|
|
2609
2758
|
if (pendingRecoveryCodes(next)) {
|
|
@@ -2622,6 +2771,10 @@ class AtlasFlowController {
|
|
|
2622
2771
|
});
|
|
2623
2772
|
exchanged = res.ok;
|
|
2624
2773
|
}
|
|
2774
|
+
if (exchanged && this.holdExistingAccountNotice && pendingExistingAccountNotice(next)) {
|
|
2775
|
+
this.setState({ ...next, ticket: null, busy: false });
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2625
2778
|
const kept = exchanged && (await this.reload());
|
|
2626
2779
|
this.setState({ ...next, busy: false, sessionKept: kept });
|
|
2627
2780
|
}
|
|
@@ -2781,6 +2934,9 @@ class SignInComponent {
|
|
|
2781
2934
|
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
2782
2935
|
[firstFactors]="flow.firstFactors()"
|
|
2783
2936
|
[preparedFactor]="flow.preparedFactor()"
|
|
2937
|
+
[passwordHold]="flow.state().passwordHold ?? null"
|
|
2938
|
+
[holdProof]="flow.holdProof()"
|
|
2939
|
+
(usePassword)="flow.usePassword()"
|
|
2784
2940
|
[offerEmailSecondFactor]="flow.settings()?.account?.emailSecondFactor === true"
|
|
2785
2941
|
[emailedSecondFactor]="flow.emailSecondFactor()"
|
|
2786
2942
|
(submitFlow)="submitIdentifier($event)"
|
|
@@ -2810,7 +2966,7 @@ class SignInComponent {
|
|
|
2810
2966
|
{{ t('signIn.noAccount') }} <a class="atlas-link" [href]="href">{{ t('signIn.signUpLink') }}</a>
|
|
2811
2967
|
</p>
|
|
2812
2968
|
}
|
|
2813
|
-
`, 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 });
|
|
2814
2970
|
}
|
|
2815
2971
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignInComponent, decorators: [{
|
|
2816
2972
|
type: Component,
|
|
@@ -2845,6 +3001,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2845
3001
|
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
2846
3002
|
[firstFactors]="flow.firstFactors()"
|
|
2847
3003
|
[preparedFactor]="flow.preparedFactor()"
|
|
3004
|
+
[passwordHold]="flow.state().passwordHold ?? null"
|
|
3005
|
+
[holdProof]="flow.holdProof()"
|
|
3006
|
+
(usePassword)="flow.usePassword()"
|
|
2848
3007
|
[offerEmailSecondFactor]="flow.settings()?.account?.emailSecondFactor === true"
|
|
2849
3008
|
[emailedSecondFactor]="flow.emailSecondFactor()"
|
|
2850
3009
|
(submitFlow)="submitIdentifier($event)"
|
|
@@ -3455,7 +3614,9 @@ class SignUpComponent {
|
|
|
3455
3614
|
atlas = inject(AtlasService);
|
|
3456
3615
|
/** Where to send the user once the flow completes. Reserved for host routing. */
|
|
3457
3616
|
afterUrl;
|
|
3458
|
-
|
|
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);
|
|
3459
3620
|
/** Password or passkey-only sign-up (the latter when the instance offers it). */
|
|
3460
3621
|
mode = signal('password');
|
|
3461
3622
|
canPasskey() {
|
|
@@ -3469,6 +3630,9 @@ class SignUpComponent {
|
|
|
3469
3630
|
this.flow.state.update((current) => ({ ...current, errors: [] }));
|
|
3470
3631
|
this.mode.set(this.passkeyForm() ? 'password' : 'passkey');
|
|
3471
3632
|
}
|
|
3633
|
+
resetHref() {
|
|
3634
|
+
return existingAccountResetUrl({ url: this.resetPasswordUrl, frontendApi: this.atlas.frontendApi, redirectUrl: this.afterUrl });
|
|
3635
|
+
}
|
|
3472
3636
|
awaitingPasskey() {
|
|
3473
3637
|
return awaitingPasskey(this.flow.state().attempt);
|
|
3474
3638
|
}
|
|
@@ -3493,8 +3657,24 @@ class SignUpComponent {
|
|
|
3493
3657
|
this.flow.destroy();
|
|
3494
3658
|
}
|
|
3495
3659
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3496
|
-
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: `
|
|
3497
|
-
@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()) {
|
|
3498
3678
|
<div [class]="cls('card', 'atlas-card')" data-atlas-passkey-sign-up="">
|
|
3499
3679
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
3500
3680
|
@for (error of flow.state().errors; track error.code) {
|
|
@@ -3536,7 +3716,7 @@ class SignUpComponent {
|
|
|
3536
3716
|
"
|
|
3537
3717
|
></atlas-sign-up-form>
|
|
3538
3718
|
}
|
|
3539
|
-
`, 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 });
|
|
3540
3720
|
}
|
|
3541
3721
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, decorators: [{
|
|
3542
3722
|
type: Component,
|
|
@@ -3546,7 +3726,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
3546
3726
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3547
3727
|
imports: [AttemptFlowComponent, SignUpFormComponent],
|
|
3548
3728
|
template: `
|
|
3549
|
-
@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()) {
|
|
3550
3746
|
<div [class]="cls('card', 'atlas-card')" data-atlas-passkey-sign-up="">
|
|
3551
3747
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
3552
3748
|
@for (error of flow.state().errors; track error.code) {
|
|
@@ -3592,6 +3788,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
3592
3788
|
}]
|
|
3593
3789
|
}], propDecorators: { afterUrl: [{
|
|
3594
3790
|
type: Input
|
|
3791
|
+
}], resetPasswordUrl: [{
|
|
3792
|
+
type: Input
|
|
3595
3793
|
}] } });
|
|
3596
3794
|
|
|
3597
3795
|
/**
|