@atlasauth/angular 0.2.0 → 0.3.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 +67 -0
- package/README.md +1 -1
- package/dist/README.md +1 -1
- package/dist/esm2022/lib/atlas.config.mjs +1 -1
- package/dist/esm2022/lib/atlas.service.mjs +13 -5
- package/dist/esm2022/lib/components/attempt-flow.component.mjs +31 -3
- package/dist/esm2022/lib/components/flow-controller.mjs +83 -7
- package/dist/esm2022/lib/components/sign-in.component.mjs +13 -4
- package/dist/esm2022/lib/components/sign-up-form.component.mjs +410 -0
- package/dist/esm2022/lib/components/sign-up.component.mjs +52 -22
- package/dist/esm2022/lib/directives/captcha-slot.directive.mjs +42 -0
- package/dist/esm2022/lib/i18n.mjs +13 -1
- package/dist/esm2022/public-api.mjs +3 -1
- package/dist/fesm2022/atlasauth-angular.mjs +638 -36
- package/dist/fesm2022/atlasauth-angular.mjs.map +1 -1
- package/dist/lib/atlas.config.d.ts +7 -2
- package/dist/lib/atlas.service.d.ts +2 -2
- package/dist/lib/components/attempt-flow.component.d.ts +9 -2
- package/dist/lib/components/flow-controller.d.ts +40 -3
- package/dist/lib/components/sign-in.component.d.ts +3 -1
- package/dist/lib/components/sign-up-form.component.d.ts +43 -0
- package/dist/lib/components/sign-up.component.d.ts +9 -5
- package/dist/lib/directives/captcha-slot.directive.d.ts +19 -0
- package/dist/public-api.d.ts +3 -0
- package/package.json +5 -4
|
@@ -4,7 +4,7 @@ import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
|
4
4
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
5
5
|
import { BehaviorSubject, combineLatest, firstValueFrom } from 'rxjs';
|
|
6
6
|
import { map, distinctUntilChanged, filter, take } from 'rxjs/operators';
|
|
7
|
-
import { TokenCache, FapiClient, getProviderToken, readRedirectResult, stripRedirectParams, readExpiry, msUntilRefresh, nextStep, flowFromPending, initialFlow,
|
|
7
|
+
import { TokenCache, resolveFrontendApi, FapiClient, getProviderToken, runFrontChannelLogoutFromResponse, readRedirectResult, stripRedirectParams, readExpiry, msUntilRefresh, nextStep, createCaptchaController, flowFromPending, initialFlow, fetchInstanceSettings, signInCaptchaOptions, flowNeedsCaptcha, advanceWithCaptcha, createSignUp, shouldKeepPolling, pollAttempt } from '@atlasauth/js';
|
|
8
8
|
export { FapiClient, advance, flowFromPending, getProviderToken, initialFlow, pollAttempt, prepareFactor, requestForStep, shouldKeepPolling } from '@atlasauth/js';
|
|
9
9
|
import { evaluate } from '@atlasauth/authz';
|
|
10
10
|
export { evaluate, hasFromClaims } from '@atlasauth/authz';
|
|
@@ -173,6 +173,18 @@ const EN_US = {
|
|
|
173
173
|
'signUp.submit': 'Continue',
|
|
174
174
|
'signUp.haveAccount': 'Already have an account?',
|
|
175
175
|
'signUp.signInLink': 'Sign in',
|
|
176
|
+
'signUp.emailLabel': 'Email address',
|
|
177
|
+
'signUp.passwordLabel': 'Password',
|
|
178
|
+
'signUp.optionalField': '{label} (optional)',
|
|
179
|
+
'signUp.selectPlaceholder': 'Select…',
|
|
180
|
+
'signUp.consentAgree': 'I agree to the',
|
|
181
|
+
'signUp.consentNotice': 'By continuing you agree to the',
|
|
182
|
+
'signUp.terms': 'Terms of Service',
|
|
183
|
+
'signUp.privacy': 'Privacy Policy',
|
|
184
|
+
'signUp.and': 'and',
|
|
185
|
+
'captcha.prompt': 'Complete the security check to continue.',
|
|
186
|
+
'captcha.required': 'Please complete the security check, then try again.',
|
|
187
|
+
'captcha.failed': "The security check didn't pass. Please try again.",
|
|
176
188
|
'mfa.title': 'Two-step verification',
|
|
177
189
|
'mfa.subtitle': 'Enter the code from your authenticator app.',
|
|
178
190
|
'mfa.codeLabel': 'Code',
|
|
@@ -299,7 +311,11 @@ class AtlasService {
|
|
|
299
311
|
constructor() {
|
|
300
312
|
const options = this.options;
|
|
301
313
|
this.publishableKey = options.publishableKey;
|
|
302
|
-
|
|
314
|
+
// Throws an `AtlasConfigError` on a missing/invalid value rather than quietly
|
|
315
|
+
// sending every request to a relative path on the app's origin.
|
|
316
|
+
this.baseUrl = resolveFrontendApi(options.frontendApi, {
|
|
317
|
+
component: 'provideAtlas() / AtlasModule.forRoot()',
|
|
318
|
+
});
|
|
303
319
|
this.doFetch = options.fetchImpl ?? ((...args) => fetch(...args));
|
|
304
320
|
this.catalog = options.localization ? withFallback(options.localization) : EN_US;
|
|
305
321
|
this.fapiClient = new FapiClient({
|
|
@@ -397,12 +413,16 @@ class AtlasService {
|
|
|
397
413
|
return evaluate(this._claims.value, condition).allowed;
|
|
398
414
|
}
|
|
399
415
|
/** Revoke the current session server-side and drop all local auth state. */
|
|
400
|
-
async signOut() {
|
|
416
|
+
async signOut(options) {
|
|
401
417
|
const current = this.cache.peek();
|
|
402
418
|
if (current) {
|
|
403
|
-
await this.call(`/v1/client/sessions/${current.sessionId}/revoke`, {
|
|
419
|
+
const revoked = await this.call(`/v1/client/sessions/${current.sessionId}/revoke`, {
|
|
404
420
|
method: 'POST',
|
|
405
421
|
}).catch(() => undefined);
|
|
422
|
+
// OIDC Front-Channel Logout: let the apps signed in through this session
|
|
423
|
+
// clear theirs in this browser (hidden iframes, capped at 5 s). Best-effort.
|
|
424
|
+
if (options?.frontchannel !== false)
|
|
425
|
+
await runFrontChannelLogoutFromResponse(revoked);
|
|
406
426
|
}
|
|
407
427
|
this.clearRefreshTimer();
|
|
408
428
|
this.cache.clear();
|
|
@@ -819,6 +839,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
819
839
|
}]
|
|
820
840
|
}] });
|
|
821
841
|
|
|
842
|
+
/**
|
|
843
|
+
* `[atlasCaptchaSlot]="controller"` — the element a flow's captcha widget draws
|
|
844
|
+
* into (see `createCaptchaController` in `@atlasauth/js`). Attaches the
|
|
845
|
+
* controller to the host element while it is rendered and detaches it (removing
|
|
846
|
+
* the widget) when it goes away.
|
|
847
|
+
*/
|
|
848
|
+
class AtlasCaptchaSlotDirective {
|
|
849
|
+
host = inject(ElementRef);
|
|
850
|
+
current = null;
|
|
851
|
+
initialized = false;
|
|
852
|
+
set controller(next) {
|
|
853
|
+
if (next === this.current)
|
|
854
|
+
return;
|
|
855
|
+
if (this.initialized)
|
|
856
|
+
this.current?.attach(null);
|
|
857
|
+
this.current = next;
|
|
858
|
+
if (this.initialized)
|
|
859
|
+
this.current?.attach(this.host.nativeElement);
|
|
860
|
+
}
|
|
861
|
+
ngOnInit() {
|
|
862
|
+
this.initialized = true;
|
|
863
|
+
this.current?.attach(this.host.nativeElement);
|
|
864
|
+
}
|
|
865
|
+
ngOnDestroy() {
|
|
866
|
+
this.current?.attach(null);
|
|
867
|
+
}
|
|
868
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasCaptchaSlotDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
869
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: AtlasCaptchaSlotDirective, isStandalone: true, selector: "[atlasCaptchaSlot]", inputs: { controller: ["atlasCaptchaSlot", "controller"] }, ngImport: i0 });
|
|
870
|
+
}
|
|
871
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasCaptchaSlotDirective, decorators: [{
|
|
872
|
+
type: Directive,
|
|
873
|
+
args: [{
|
|
874
|
+
selector: '[atlasCaptchaSlot]',
|
|
875
|
+
standalone: true,
|
|
876
|
+
}]
|
|
877
|
+
}], propDecorators: { controller: [{
|
|
878
|
+
type: Input,
|
|
879
|
+
args: ['atlasCaptchaSlot']
|
|
880
|
+
}] } });
|
|
881
|
+
|
|
822
882
|
/**
|
|
823
883
|
* The server-driven flow shell, shared by `<atlas-sign-in>` and
|
|
824
884
|
* `<atlas-sign-up>`. The Angular peer of React's internal `AttemptFlow`.
|
|
@@ -835,6 +895,12 @@ class AttemptFlowComponent {
|
|
|
835
895
|
busy = false;
|
|
836
896
|
errors = [];
|
|
837
897
|
titleKey = 'signIn.title';
|
|
898
|
+
/**
|
|
899
|
+
* The sign-in captcha, drawn above the submit button on the identifier step
|
|
900
|
+
* (an always-on sign-in captcha) and on the challenge step the server demands
|
|
901
|
+
* with `needs_captcha`. Null when the flow draws none.
|
|
902
|
+
*/
|
|
903
|
+
captcha = null;
|
|
838
904
|
/** Emits the collected field values when the form is submitted. */
|
|
839
905
|
submitFlow = new EventEmitter();
|
|
840
906
|
values = {};
|
|
@@ -860,6 +926,10 @@ class AttemptFlowComponent {
|
|
|
860
926
|
step() {
|
|
861
927
|
return this.attempt ? nextStep(this.attempt) : { kind: 'collect_identifier' };
|
|
862
928
|
}
|
|
929
|
+
showCaptcha() {
|
|
930
|
+
const kind = this.step().kind;
|
|
931
|
+
return !!this.captcha && (kind === 'collect_identifier' || kind === 'collect_captcha');
|
|
932
|
+
}
|
|
863
933
|
showSubmit() {
|
|
864
934
|
const kind = this.step().kind;
|
|
865
935
|
return kind !== 'done' && kind !== 'await_oauth';
|
|
@@ -884,6 +954,10 @@ class AttemptFlowComponent {
|
|
|
884
954
|
return [{ kind: 'input', name: 'code', labelKey: 'signIn.emailCodeLabel', type: 'text' }];
|
|
885
955
|
case 'collect_new_password':
|
|
886
956
|
return [{ kind: 'input', name: 'password', labelKey: 'reset.newPasswordLabel', type: 'password' }];
|
|
957
|
+
case 'collect_captcha':
|
|
958
|
+
// §5 the server withheld the attempt until a captcha clears. Solving the
|
|
959
|
+
// widget resubmits the identifier on its own; the button retries.
|
|
960
|
+
return [msg('captcha.prompt')];
|
|
887
961
|
case 'await_oauth':
|
|
888
962
|
return [msg('signIn.magicLinkSent')];
|
|
889
963
|
case 'done':
|
|
@@ -897,7 +971,7 @@ class AttemptFlowComponent {
|
|
|
897
971
|
}
|
|
898
972
|
}
|
|
899
973
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
900
|
-
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", titleKey: "titleKey" }, outputs: { submitFlow: "submitFlow" }, ngImport: i0, template: `
|
|
974
|
+
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", titleKey: "titleKey", captcha: "captcha" }, outputs: { submitFlow: "submitFlow" }, ngImport: i0, template: `
|
|
901
975
|
<div [class]="cls('card', 'atlas-card')">
|
|
902
976
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
903
977
|
|
|
@@ -913,6 +987,7 @@ class AttemptFlowComponent {
|
|
|
913
987
|
<input
|
|
914
988
|
[class]="cls('formFieldInput', 'atlas-input')"
|
|
915
989
|
[type]="f.type"
|
|
990
|
+
[attr.name]="f.name"
|
|
916
991
|
[value]="values[f.name] ?? ''"
|
|
917
992
|
(input)="setValue(f.name, $any($event.target).value)"
|
|
918
993
|
/>
|
|
@@ -925,6 +1000,10 @@ class AttemptFlowComponent {
|
|
|
925
1000
|
}
|
|
926
1001
|
}
|
|
927
1002
|
|
|
1003
|
+
@if (showCaptcha()) {
|
|
1004
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1005
|
+
}
|
|
1006
|
+
|
|
928
1007
|
@if (showSubmit()) {
|
|
929
1008
|
<button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy" type="submit">
|
|
930
1009
|
{{ t('signIn.submit') }}
|
|
@@ -932,7 +1011,7 @@ class AttemptFlowComponent {
|
|
|
932
1011
|
}
|
|
933
1012
|
</form>
|
|
934
1013
|
</div>
|
|
935
|
-
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1014
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
936
1015
|
}
|
|
937
1016
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, decorators: [{
|
|
938
1017
|
type: Component,
|
|
@@ -940,6 +1019,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
940
1019
|
selector: 'atlas-attempt-flow',
|
|
941
1020
|
standalone: true,
|
|
942
1021
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1022
|
+
imports: [AtlasCaptchaSlotDirective],
|
|
943
1023
|
template: `
|
|
944
1024
|
<div [class]="cls('card', 'atlas-card')">
|
|
945
1025
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
@@ -956,6 +1036,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
956
1036
|
<input
|
|
957
1037
|
[class]="cls('formFieldInput', 'atlas-input')"
|
|
958
1038
|
[type]="f.type"
|
|
1039
|
+
[attr.name]="f.name"
|
|
959
1040
|
[value]="values[f.name] ?? ''"
|
|
960
1041
|
(input)="setValue(f.name, $any($event.target).value)"
|
|
961
1042
|
/>
|
|
@@ -968,6 +1049,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
968
1049
|
}
|
|
969
1050
|
}
|
|
970
1051
|
|
|
1052
|
+
@if (showCaptcha()) {
|
|
1053
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1054
|
+
}
|
|
1055
|
+
|
|
971
1056
|
@if (showSubmit()) {
|
|
972
1057
|
<button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy" type="submit">
|
|
973
1058
|
{{ t('signIn.submit') }}
|
|
@@ -985,6 +1070,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
985
1070
|
type: Input
|
|
986
1071
|
}], titleKey: [{
|
|
987
1072
|
type: Input
|
|
1073
|
+
}], captcha: [{
|
|
1074
|
+
type: Input
|
|
988
1075
|
}], submitFlow: [{
|
|
989
1076
|
type: Output
|
|
990
1077
|
}] } });
|
|
@@ -1001,18 +1088,68 @@ function msg(labelKey) {
|
|
|
1001
1088
|
* server via `advance`, and this controller only posts whatever the current step
|
|
1002
1089
|
* needs and re-renders the returned status. State lives in a signal so templates
|
|
1003
1090
|
* update whether the app runs zoned or zoneless.
|
|
1091
|
+
*
|
|
1092
|
+
* `<atlas-sign-up>` starts the attempt with {@link create}
|
|
1093
|
+
* (`POST /v1/client/sign_ups`); the emailed-code step then runs through the same
|
|
1094
|
+
* {@link submit}. Both flows draw the instance's captcha (from
|
|
1095
|
+
* {@link loadSettings}) when it is required.
|
|
1004
1096
|
*/
|
|
1005
1097
|
class AtlasFlowController {
|
|
1006
1098
|
client;
|
|
1007
1099
|
reload;
|
|
1100
|
+
t;
|
|
1008
1101
|
state;
|
|
1102
|
+
/**
|
|
1103
|
+
* The instance's public sign-in / sign-up settings (captcha, sign-up fields,
|
|
1104
|
+
* legal consent) from `GET /v1/appearance`; null until {@link loadSettings}
|
|
1105
|
+
* resolves, or when the server could not be reached.
|
|
1106
|
+
*/
|
|
1107
|
+
settings = signal(null);
|
|
1108
|
+
/** The sign-in captcha (identifier step / `needs_captcha` challenge). */
|
|
1109
|
+
captcha = createCaptchaController();
|
|
1110
|
+
/** The sign-up captcha (the create-account step). */
|
|
1111
|
+
signUpCaptcha = createCaptchaController();
|
|
1112
|
+
/** Whether {@link captcha} currently draws a widget (for templates). */
|
|
1113
|
+
captchaDrawn = signal(false);
|
|
1114
|
+
/** Whether {@link signUpCaptcha} currently draws a widget (for templates). */
|
|
1115
|
+
signUpCaptchaDrawn = signal(false);
|
|
1009
1116
|
pollTimer = null;
|
|
1010
|
-
|
|
1117
|
+
identifier = '';
|
|
1118
|
+
constructor(client, reload, pending,
|
|
1119
|
+
/** Resolves the captcha messages' catalog keys. Defaults to en-US. */
|
|
1120
|
+
t = (key) => translate(EN_US, key)) {
|
|
1011
1121
|
this.client = client;
|
|
1012
1122
|
this.reload = reload;
|
|
1123
|
+
this.t = t;
|
|
1013
1124
|
// Seeded from a redirect that came back mid-flow (e.g. OAuth needing a second
|
|
1014
1125
|
// factor) so the user resumes at the demanded step instead of starting over.
|
|
1015
1126
|
this.state = signal(pending ? flowFromPending(pending) : initialFlow);
|
|
1127
|
+
this.syncCaptcha();
|
|
1128
|
+
}
|
|
1129
|
+
/** Fetch the instance's public settings and draw whatever captcha they require. */
|
|
1130
|
+
async loadSettings() {
|
|
1131
|
+
const loaded = await fetchInstanceSettings(this.client);
|
|
1132
|
+
if (loaded) {
|
|
1133
|
+
this.settings.set(loaded);
|
|
1134
|
+
this.syncCaptcha();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/** Keep both captchas in step with the settings and the flow's current step. */
|
|
1138
|
+
syncCaptcha() {
|
|
1139
|
+
const settings = this.settings();
|
|
1140
|
+
const state = this.state();
|
|
1141
|
+
this.captcha.configure(signInCaptchaOptions(settings?.captchaFlows.signIn, state, () => void this.submit({})));
|
|
1142
|
+
const signUpFlow = settings?.captchaFlows.signUp;
|
|
1143
|
+
this.signUpCaptcha.configure({
|
|
1144
|
+
flow: signUpFlow,
|
|
1145
|
+
active: !state.attempt && flowNeedsCaptcha(signUpFlow),
|
|
1146
|
+
});
|
|
1147
|
+
this.captchaDrawn.set(this.captcha.needed);
|
|
1148
|
+
this.signUpCaptchaDrawn.set(this.signUpCaptcha.needed);
|
|
1149
|
+
}
|
|
1150
|
+
setState(next) {
|
|
1151
|
+
this.state.set(next);
|
|
1152
|
+
this.syncCaptcha();
|
|
1016
1153
|
}
|
|
1017
1154
|
/**
|
|
1018
1155
|
* Advance the flow with the values the user typed. On completion, exchange the
|
|
@@ -1022,8 +1159,31 @@ class AtlasFlowController {
|
|
|
1022
1159
|
*/
|
|
1023
1160
|
async submit(values) {
|
|
1024
1161
|
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1162
|
+
const out = await advanceWithCaptcha(this.client, this.state(), values, {
|
|
1163
|
+
captcha: this.captcha,
|
|
1164
|
+
identifier: this.identifier,
|
|
1165
|
+
t: this.t,
|
|
1166
|
+
});
|
|
1167
|
+
this.identifier = out.identifier;
|
|
1168
|
+
await this.finish(out.state);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Create an account (`POST /v1/client/sign_ups`) from a create-account form's
|
|
1172
|
+
* values — email, password and any configured sign-up fields — carrying the
|
|
1173
|
+
* sign-up captcha's token when the instance requires one. The emailed code is
|
|
1174
|
+
* then posted with {@link submit}.
|
|
1175
|
+
*/
|
|
1176
|
+
async create(values, consent = true) {
|
|
1177
|
+
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
1178
|
+
const next = await createSignUp(this.client, values, {
|
|
1179
|
+
consent,
|
|
1180
|
+
captcha: this.signUpCaptcha,
|
|
1181
|
+
t: this.t,
|
|
1182
|
+
});
|
|
1183
|
+
await this.finish(next);
|
|
1184
|
+
}
|
|
1185
|
+
async finish(next) {
|
|
1186
|
+
this.setState(next);
|
|
1027
1187
|
if (next.attempt?.status === 'complete') {
|
|
1028
1188
|
if (next.ticket) {
|
|
1029
1189
|
await this.client.post('/v1/client/tickets/exchange', {
|
|
@@ -1053,7 +1213,7 @@ class AtlasFlowController {
|
|
|
1053
1213
|
this.pollTimer = setInterval(() => {
|
|
1054
1214
|
void (async () => {
|
|
1055
1215
|
const { state: next, ticket } = await pollAttempt(this.client, this.state());
|
|
1056
|
-
this.
|
|
1216
|
+
this.setState(next);
|
|
1057
1217
|
if (!ticket || !next.attempt)
|
|
1058
1218
|
return;
|
|
1059
1219
|
// The ticket is exchanged by a normal request, so no session token ever
|
|
@@ -1073,15 +1233,19 @@ class AtlasFlowController {
|
|
|
1073
1233
|
this.pollTimer = null;
|
|
1074
1234
|
}
|
|
1075
1235
|
}
|
|
1076
|
-
/** Release the poll timer — call from the component's `ngOnDestroy`. */
|
|
1236
|
+
/** Release the poll timer and the captcha widgets — call from the component's `ngOnDestroy`. */
|
|
1077
1237
|
destroy() {
|
|
1078
1238
|
this.stopPolling();
|
|
1239
|
+
this.captcha.destroy();
|
|
1240
|
+
this.signUpCaptcha.destroy();
|
|
1079
1241
|
}
|
|
1080
1242
|
}
|
|
1081
1243
|
|
|
1082
1244
|
/**
|
|
1083
1245
|
* `<atlas-sign-in>` — the full, multi-step sign-in flow driven entirely by the
|
|
1084
|
-
* server-side attempt status. The Angular peer of React's `<SignIn/>`.
|
|
1246
|
+
* server-side attempt status. The Angular peer of React's `<SignIn/>`. Draws the
|
|
1247
|
+
* instance's sign-in captcha when it is required (always-on, or a
|
|
1248
|
+
* `needs_captcha` challenge).
|
|
1085
1249
|
*
|
|
1086
1250
|
* ```html
|
|
1087
1251
|
* <atlas-sign-in *atlasSignedOut afterUrl="/dashboard"></atlas-sign-in>
|
|
@@ -1093,10 +1257,14 @@ class SignInComponent {
|
|
|
1093
1257
|
afterUrl;
|
|
1094
1258
|
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(),
|
|
1095
1259
|
// Resume mid-flow if an OAuth redirect came back owing a second factor.
|
|
1096
|
-
this.atlas.pendingAttempt);
|
|
1260
|
+
this.atlas.pendingAttempt, (key) => translate(this.atlas.localization, key));
|
|
1097
1261
|
ngOnInit() {
|
|
1098
1262
|
// If seeded from a pending attempt that is already pollable, start polling.
|
|
1099
1263
|
this.flow.syncPolling();
|
|
1264
|
+
// §5 the instance's captcha settings: switching captcha on in the dashboard
|
|
1265
|
+
// draws it here with no code change.
|
|
1266
|
+
if (typeof window !== 'undefined')
|
|
1267
|
+
void this.flow.loadSettings();
|
|
1100
1268
|
}
|
|
1101
1269
|
ngOnDestroy() {
|
|
1102
1270
|
this.flow.destroy();
|
|
@@ -1108,9 +1276,10 @@ class SignInComponent {
|
|
|
1108
1276
|
[busy]="flow.state().busy"
|
|
1109
1277
|
[errors]="flow.state().errors"
|
|
1110
1278
|
titleKey="signIn.title"
|
|
1279
|
+
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
1111
1280
|
(submitFlow)="flow.submit($event)"
|
|
1112
1281
|
></atlas-attempt-flow>
|
|
1113
|
-
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "titleKey"], outputs: ["submitFlow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1282
|
+
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "titleKey", "captcha"], outputs: ["submitFlow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1114
1283
|
}
|
|
1115
1284
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignInComponent, decorators: [{
|
|
1116
1285
|
type: Component,
|
|
@@ -1125,6 +1294,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1125
1294
|
[busy]="flow.state().busy"
|
|
1126
1295
|
[errors]="flow.state().errors"
|
|
1127
1296
|
titleKey="signIn.title"
|
|
1297
|
+
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
1128
1298
|
(submitFlow)="flow.submit($event)"
|
|
1129
1299
|
></atlas-attempt-flow>
|
|
1130
1300
|
`,
|
|
@@ -1134,9 +1304,416 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1134
1304
|
}] } });
|
|
1135
1305
|
|
|
1136
1306
|
/**
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1307
|
+
* The create-account step of `<atlas-sign-up>`: the instance's configured extra
|
|
1308
|
+
* fields, email + password, the legal-consent row and — when the instance
|
|
1309
|
+
* requires one on sign-up — the captcha widget. Values live on the component so
|
|
1310
|
+
* a refused submit never clears them.
|
|
1311
|
+
*/
|
|
1312
|
+
class SignUpFormComponent {
|
|
1313
|
+
atlas = inject(AtlasService);
|
|
1314
|
+
settings = null;
|
|
1315
|
+
busy = false;
|
|
1316
|
+
errors = [];
|
|
1317
|
+
captcha = null;
|
|
1318
|
+
/** Emits the typed values and the consent tick when the form is submitted. */
|
|
1319
|
+
submitSignUp = new EventEmitter();
|
|
1320
|
+
values = {};
|
|
1321
|
+
agreed = false;
|
|
1322
|
+
t(key, vars) {
|
|
1323
|
+
return translate(this.atlas.localization, key, vars);
|
|
1324
|
+
}
|
|
1325
|
+
cls(key, base) {
|
|
1326
|
+
return classFor(key, this.atlas.appearance, base);
|
|
1327
|
+
}
|
|
1328
|
+
setValue(name, value) {
|
|
1329
|
+
this.values = { ...this.values, [name]: value };
|
|
1330
|
+
}
|
|
1331
|
+
fields() {
|
|
1332
|
+
return this.settings?.signUpFields ?? [];
|
|
1333
|
+
}
|
|
1334
|
+
label(f) {
|
|
1335
|
+
return f.required ? f.label : this.t('signUp.optionalField', { label: f.label });
|
|
1336
|
+
}
|
|
1337
|
+
inputType(f) {
|
|
1338
|
+
return ['email', 'tel', 'url', 'number', 'date'].includes(f.type) ? f.type : 'text';
|
|
1339
|
+
}
|
|
1340
|
+
/** Same encoding as React, the embed widget and the hosted page: one value,
|
|
1341
|
+
* or a comma-joined list for multiselect. */
|
|
1342
|
+
picked(key) {
|
|
1343
|
+
const value = this.values[key];
|
|
1344
|
+
return value ? value.split(', ') : [];
|
|
1345
|
+
}
|
|
1346
|
+
toggle(f, option, on) {
|
|
1347
|
+
if (f.type === 'radio') {
|
|
1348
|
+
this.setValue(f.key, option);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
const current = this.picked(f.key);
|
|
1352
|
+
this.setValue(f.key, (on ? [...current, option] : current.filter((p) => p !== option)).join(', '));
|
|
1353
|
+
}
|
|
1354
|
+
showLegal() {
|
|
1355
|
+
const legal = this.settings?.legal;
|
|
1356
|
+
return !!legal && (!!legal.termsUrl || !!legal.privacyUrl);
|
|
1357
|
+
}
|
|
1358
|
+
consentOk() {
|
|
1359
|
+
return !this.settings?.legal?.required || this.agreed;
|
|
1360
|
+
}
|
|
1361
|
+
formErrors() {
|
|
1362
|
+
return this.errors.filter((error) => !error.param);
|
|
1363
|
+
}
|
|
1364
|
+
fieldErrorsFor(name) {
|
|
1365
|
+
return this.errors.filter((error) => error.param === name);
|
|
1366
|
+
}
|
|
1367
|
+
onSubmit(event) {
|
|
1368
|
+
event.preventDefault();
|
|
1369
|
+
if (!this.consentOk())
|
|
1370
|
+
return;
|
|
1371
|
+
this.submitSignUp.emit({
|
|
1372
|
+
values: { ...this.values },
|
|
1373
|
+
consent: this.settings?.legal?.required ? this.agreed : true,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1377
|
+
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: `
|
|
1378
|
+
<div [class]="cls('card', 'atlas-card')">
|
|
1379
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
1380
|
+
|
|
1381
|
+
@for (error of formErrors(); track error.code) {
|
|
1382
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error.message }}</p>
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
<form (submit)="onSubmit($event)">
|
|
1386
|
+
@for (f of fields(); track f.key) {
|
|
1387
|
+
@if (f.type === 'radio' || f.type === 'multiselect') {
|
|
1388
|
+
<div [class]="cls('formField', 'atlas-field')">
|
|
1389
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1390
|
+
<span class="atlas-choices">
|
|
1391
|
+
@for (o of f.options; track o) {
|
|
1392
|
+
<label class="atlas-choice">
|
|
1393
|
+
<input
|
|
1394
|
+
[type]="f.type === 'radio' ? 'radio' : 'checkbox'"
|
|
1395
|
+
[attr.name]="f.key"
|
|
1396
|
+
[value]="o"
|
|
1397
|
+
[checked]="picked(f.key).includes(o)"
|
|
1398
|
+
(change)="toggle(f, o, $any($event.target).checked)"
|
|
1399
|
+
/>
|
|
1400
|
+
{{ o }}
|
|
1401
|
+
</label>
|
|
1402
|
+
}
|
|
1403
|
+
</span>
|
|
1404
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1405
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1406
|
+
}
|
|
1407
|
+
</div>
|
|
1408
|
+
} @else {
|
|
1409
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1410
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1411
|
+
@if (f.type === 'checkbox') {
|
|
1412
|
+
<input
|
|
1413
|
+
type="checkbox"
|
|
1414
|
+
[attr.name]="f.key"
|
|
1415
|
+
[checked]="values[f.key] === 'true'"
|
|
1416
|
+
(change)="setValue(f.key, $any($event.target).checked ? 'true' : '')"
|
|
1417
|
+
/>
|
|
1418
|
+
} @else if (f.type === 'select') {
|
|
1419
|
+
<select
|
|
1420
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1421
|
+
[attr.name]="f.key"
|
|
1422
|
+
[value]="values[f.key] ?? ''"
|
|
1423
|
+
[required]="f.required"
|
|
1424
|
+
(change)="setValue(f.key, $any($event.target).value)"
|
|
1425
|
+
>
|
|
1426
|
+
<option value="">{{ f.placeholder ?? t('signUp.selectPlaceholder') }}</option>
|
|
1427
|
+
@for (o of f.options; track o) {
|
|
1428
|
+
<option [value]="o">{{ o }}</option>
|
|
1429
|
+
}
|
|
1430
|
+
</select>
|
|
1431
|
+
} @else if (f.type === 'textarea') {
|
|
1432
|
+
<textarea
|
|
1433
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1434
|
+
[attr.name]="f.key"
|
|
1435
|
+
rows="3"
|
|
1436
|
+
[value]="values[f.key] ?? ''"
|
|
1437
|
+
[required]="f.required"
|
|
1438
|
+
[attr.placeholder]="f.placeholder"
|
|
1439
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1440
|
+
></textarea>
|
|
1441
|
+
} @else {
|
|
1442
|
+
<input
|
|
1443
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1444
|
+
[type]="inputType(f)"
|
|
1445
|
+
[attr.name]="f.key"
|
|
1446
|
+
[value]="values[f.key] ?? ''"
|
|
1447
|
+
[required]="f.required"
|
|
1448
|
+
[attr.placeholder]="f.placeholder"
|
|
1449
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1450
|
+
/>
|
|
1451
|
+
}
|
|
1452
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1453
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1454
|
+
}
|
|
1455
|
+
</label>
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1460
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.emailLabel') }}</span>
|
|
1461
|
+
<input
|
|
1462
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1463
|
+
type="email"
|
|
1464
|
+
name="email"
|
|
1465
|
+
autocomplete="email"
|
|
1466
|
+
required
|
|
1467
|
+
[value]="values['email'] ?? ''"
|
|
1468
|
+
(input)="setValue('email', $any($event.target).value)"
|
|
1469
|
+
/>
|
|
1470
|
+
@for (error of fieldErrorsFor('email'); track error.code) {
|
|
1471
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1472
|
+
}
|
|
1473
|
+
</label>
|
|
1474
|
+
|
|
1475
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1476
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.passwordLabel') }}</span>
|
|
1477
|
+
<input
|
|
1478
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1479
|
+
type="password"
|
|
1480
|
+
name="password"
|
|
1481
|
+
autocomplete="new-password"
|
|
1482
|
+
required
|
|
1483
|
+
[value]="values['password'] ?? ''"
|
|
1484
|
+
(input)="setValue('password', $any($event.target).value)"
|
|
1485
|
+
/>
|
|
1486
|
+
@for (error of fieldErrorsFor('password'); track error.code) {
|
|
1487
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1488
|
+
}
|
|
1489
|
+
</label>
|
|
1490
|
+
|
|
1491
|
+
@if (showLegal()) {
|
|
1492
|
+
<label [class]="cls('formField', 'atlas-consent')">
|
|
1493
|
+
@if (settings?.legal?.required) {
|
|
1494
|
+
<input
|
|
1495
|
+
type="checkbox"
|
|
1496
|
+
name="consent"
|
|
1497
|
+
[checked]="agreed"
|
|
1498
|
+
(change)="agreed = $any($event.target).checked"
|
|
1499
|
+
/>
|
|
1500
|
+
}
|
|
1501
|
+
<span class="atlas-text">
|
|
1502
|
+
{{ t(settings?.legal?.required ? 'signUp.consentAgree' : 'signUp.consentNotice') }}
|
|
1503
|
+
@if (settings?.legal?.termsUrl; as terms) {
|
|
1504
|
+
<a class="atlas-link" [href]="terms" target="_blank" rel="noopener noreferrer">{{ t('signUp.terms') }}</a>
|
|
1505
|
+
}
|
|
1506
|
+
@if (settings?.legal?.termsUrl && settings?.legal?.privacyUrl) {
|
|
1507
|
+
{{ t('signUp.and') }}
|
|
1508
|
+
}
|
|
1509
|
+
@if (settings?.legal?.privacyUrl; as privacy) {
|
|
1510
|
+
<a class="atlas-link" [href]="privacy" target="_blank" rel="noopener noreferrer">{{ t('signUp.privacy') }}</a>
|
|
1511
|
+
}.
|
|
1512
|
+
</span>
|
|
1513
|
+
@for (error of fieldErrorsFor('consent'); track error.code) {
|
|
1514
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1515
|
+
}
|
|
1516
|
+
</label>
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
@if (captcha) {
|
|
1520
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
<button
|
|
1524
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
1525
|
+
[disabled]="busy || !consentOk()"
|
|
1526
|
+
type="submit"
|
|
1527
|
+
>
|
|
1528
|
+
{{ t('signUp.submit') }}
|
|
1529
|
+
</button>
|
|
1530
|
+
</form>
|
|
1531
|
+
</div>
|
|
1532
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1533
|
+
}
|
|
1534
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpFormComponent, decorators: [{
|
|
1535
|
+
type: Component,
|
|
1536
|
+
args: [{
|
|
1537
|
+
selector: 'atlas-sign-up-form',
|
|
1538
|
+
standalone: true,
|
|
1539
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1540
|
+
imports: [AtlasCaptchaSlotDirective],
|
|
1541
|
+
template: `
|
|
1542
|
+
<div [class]="cls('card', 'atlas-card')">
|
|
1543
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
1544
|
+
|
|
1545
|
+
@for (error of formErrors(); track error.code) {
|
|
1546
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error.message }}</p>
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
<form (submit)="onSubmit($event)">
|
|
1550
|
+
@for (f of fields(); track f.key) {
|
|
1551
|
+
@if (f.type === 'radio' || f.type === 'multiselect') {
|
|
1552
|
+
<div [class]="cls('formField', 'atlas-field')">
|
|
1553
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1554
|
+
<span class="atlas-choices">
|
|
1555
|
+
@for (o of f.options; track o) {
|
|
1556
|
+
<label class="atlas-choice">
|
|
1557
|
+
<input
|
|
1558
|
+
[type]="f.type === 'radio' ? 'radio' : 'checkbox'"
|
|
1559
|
+
[attr.name]="f.key"
|
|
1560
|
+
[value]="o"
|
|
1561
|
+
[checked]="picked(f.key).includes(o)"
|
|
1562
|
+
(change)="toggle(f, o, $any($event.target).checked)"
|
|
1563
|
+
/>
|
|
1564
|
+
{{ o }}
|
|
1565
|
+
</label>
|
|
1566
|
+
}
|
|
1567
|
+
</span>
|
|
1568
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1569
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1570
|
+
}
|
|
1571
|
+
</div>
|
|
1572
|
+
} @else {
|
|
1573
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1574
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1575
|
+
@if (f.type === 'checkbox') {
|
|
1576
|
+
<input
|
|
1577
|
+
type="checkbox"
|
|
1578
|
+
[attr.name]="f.key"
|
|
1579
|
+
[checked]="values[f.key] === 'true'"
|
|
1580
|
+
(change)="setValue(f.key, $any($event.target).checked ? 'true' : '')"
|
|
1581
|
+
/>
|
|
1582
|
+
} @else if (f.type === 'select') {
|
|
1583
|
+
<select
|
|
1584
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1585
|
+
[attr.name]="f.key"
|
|
1586
|
+
[value]="values[f.key] ?? ''"
|
|
1587
|
+
[required]="f.required"
|
|
1588
|
+
(change)="setValue(f.key, $any($event.target).value)"
|
|
1589
|
+
>
|
|
1590
|
+
<option value="">{{ f.placeholder ?? t('signUp.selectPlaceholder') }}</option>
|
|
1591
|
+
@for (o of f.options; track o) {
|
|
1592
|
+
<option [value]="o">{{ o }}</option>
|
|
1593
|
+
}
|
|
1594
|
+
</select>
|
|
1595
|
+
} @else if (f.type === 'textarea') {
|
|
1596
|
+
<textarea
|
|
1597
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1598
|
+
[attr.name]="f.key"
|
|
1599
|
+
rows="3"
|
|
1600
|
+
[value]="values[f.key] ?? ''"
|
|
1601
|
+
[required]="f.required"
|
|
1602
|
+
[attr.placeholder]="f.placeholder"
|
|
1603
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1604
|
+
></textarea>
|
|
1605
|
+
} @else {
|
|
1606
|
+
<input
|
|
1607
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1608
|
+
[type]="inputType(f)"
|
|
1609
|
+
[attr.name]="f.key"
|
|
1610
|
+
[value]="values[f.key] ?? ''"
|
|
1611
|
+
[required]="f.required"
|
|
1612
|
+
[attr.placeholder]="f.placeholder"
|
|
1613
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1614
|
+
/>
|
|
1615
|
+
}
|
|
1616
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1617
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1618
|
+
}
|
|
1619
|
+
</label>
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1624
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.emailLabel') }}</span>
|
|
1625
|
+
<input
|
|
1626
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1627
|
+
type="email"
|
|
1628
|
+
name="email"
|
|
1629
|
+
autocomplete="email"
|
|
1630
|
+
required
|
|
1631
|
+
[value]="values['email'] ?? ''"
|
|
1632
|
+
(input)="setValue('email', $any($event.target).value)"
|
|
1633
|
+
/>
|
|
1634
|
+
@for (error of fieldErrorsFor('email'); track error.code) {
|
|
1635
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1636
|
+
}
|
|
1637
|
+
</label>
|
|
1638
|
+
|
|
1639
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1640
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.passwordLabel') }}</span>
|
|
1641
|
+
<input
|
|
1642
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1643
|
+
type="password"
|
|
1644
|
+
name="password"
|
|
1645
|
+
autocomplete="new-password"
|
|
1646
|
+
required
|
|
1647
|
+
[value]="values['password'] ?? ''"
|
|
1648
|
+
(input)="setValue('password', $any($event.target).value)"
|
|
1649
|
+
/>
|
|
1650
|
+
@for (error of fieldErrorsFor('password'); track error.code) {
|
|
1651
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1652
|
+
}
|
|
1653
|
+
</label>
|
|
1654
|
+
|
|
1655
|
+
@if (showLegal()) {
|
|
1656
|
+
<label [class]="cls('formField', 'atlas-consent')">
|
|
1657
|
+
@if (settings?.legal?.required) {
|
|
1658
|
+
<input
|
|
1659
|
+
type="checkbox"
|
|
1660
|
+
name="consent"
|
|
1661
|
+
[checked]="agreed"
|
|
1662
|
+
(change)="agreed = $any($event.target).checked"
|
|
1663
|
+
/>
|
|
1664
|
+
}
|
|
1665
|
+
<span class="atlas-text">
|
|
1666
|
+
{{ t(settings?.legal?.required ? 'signUp.consentAgree' : 'signUp.consentNotice') }}
|
|
1667
|
+
@if (settings?.legal?.termsUrl; as terms) {
|
|
1668
|
+
<a class="atlas-link" [href]="terms" target="_blank" rel="noopener noreferrer">{{ t('signUp.terms') }}</a>
|
|
1669
|
+
}
|
|
1670
|
+
@if (settings?.legal?.termsUrl && settings?.legal?.privacyUrl) {
|
|
1671
|
+
{{ t('signUp.and') }}
|
|
1672
|
+
}
|
|
1673
|
+
@if (settings?.legal?.privacyUrl; as privacy) {
|
|
1674
|
+
<a class="atlas-link" [href]="privacy" target="_blank" rel="noopener noreferrer">{{ t('signUp.privacy') }}</a>
|
|
1675
|
+
}.
|
|
1676
|
+
</span>
|
|
1677
|
+
@for (error of fieldErrorsFor('consent'); track error.code) {
|
|
1678
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1679
|
+
}
|
|
1680
|
+
</label>
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
@if (captcha) {
|
|
1684
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
<button
|
|
1688
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
1689
|
+
[disabled]="busy || !consentOk()"
|
|
1690
|
+
type="submit"
|
|
1691
|
+
>
|
|
1692
|
+
{{ t('signUp.submit') }}
|
|
1693
|
+
</button>
|
|
1694
|
+
</form>
|
|
1695
|
+
</div>
|
|
1696
|
+
`,
|
|
1697
|
+
}]
|
|
1698
|
+
}], propDecorators: { settings: [{
|
|
1699
|
+
type: Input
|
|
1700
|
+
}], busy: [{
|
|
1701
|
+
type: Input
|
|
1702
|
+
}], errors: [{
|
|
1703
|
+
type: Input
|
|
1704
|
+
}], captcha: [{
|
|
1705
|
+
type: Input
|
|
1706
|
+
}], submitSignUp: [{
|
|
1707
|
+
type: Output
|
|
1708
|
+
}] } });
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* `<atlas-sign-up>` — creates an account. The Angular peer of React's
|
|
1712
|
+
* `<SignUp/>`. The first step is the create-account form (the instance's
|
|
1713
|
+
* configured fields, email, password, legal consent and — when required — the
|
|
1714
|
+
* captcha), which POSTs `/v1/client/sign_ups`; from there it shares the flow
|
|
1715
|
+
* engine with `<atlas-sign-in>` (the emailed-code step, then the ticket
|
|
1716
|
+
* exchange), because the sequence of steps is decided by the server.
|
|
1140
1717
|
*
|
|
1141
1718
|
* ```html
|
|
1142
1719
|
* <atlas-sign-up *atlasSignedOut></atlas-sign-up>
|
|
@@ -1146,20 +1723,35 @@ class SignUpComponent {
|
|
|
1146
1723
|
atlas = inject(AtlasService);
|
|
1147
1724
|
/** Where to send the user once the flow completes. Reserved for host routing. */
|
|
1148
1725
|
afterUrl;
|
|
1149
|
-
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(), this.atlas.pendingAttempt);
|
|
1726
|
+
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(), this.atlas.pendingAttempt, (key) => translate(this.atlas.localization, key));
|
|
1727
|
+
ngOnInit() {
|
|
1728
|
+
// §5 the instance's sign-up fields, consent policy and captcha settings.
|
|
1729
|
+
if (typeof window !== 'undefined')
|
|
1730
|
+
void this.flow.loadSettings();
|
|
1731
|
+
}
|
|
1150
1732
|
ngOnDestroy() {
|
|
1151
1733
|
this.flow.destroy();
|
|
1152
1734
|
}
|
|
1153
1735
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1154
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1736
|
+
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: `
|
|
1737
|
+
@if (flow.state().attempt) {
|
|
1738
|
+
<atlas-attempt-flow
|
|
1739
|
+
[attempt]="flow.state().attempt"
|
|
1740
|
+
[busy]="flow.state().busy"
|
|
1741
|
+
[errors]="flow.state().errors"
|
|
1742
|
+
titleKey="signUp.title"
|
|
1743
|
+
(submitFlow)="flow.submit($event)"
|
|
1744
|
+
></atlas-attempt-flow>
|
|
1745
|
+
} @else {
|
|
1746
|
+
<atlas-sign-up-form
|
|
1747
|
+
[settings]="flow.settings()"
|
|
1748
|
+
[busy]="flow.state().busy"
|
|
1749
|
+
[errors]="flow.state().errors"
|
|
1750
|
+
[captcha]="flow.signUpCaptchaDrawn() ? flow.signUpCaptcha : null"
|
|
1751
|
+
(submitSignUp)="flow.create($event.values, $event.consent)"
|
|
1752
|
+
></atlas-sign-up-form>
|
|
1753
|
+
}
|
|
1754
|
+
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "titleKey", "captcha"], outputs: ["submitFlow"] }, { kind: "component", type: SignUpFormComponent, selector: "atlas-sign-up-form", inputs: ["settings", "busy", "errors", "captcha"], outputs: ["submitSignUp"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1163
1755
|
}
|
|
1164
1756
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, decorators: [{
|
|
1165
1757
|
type: Component,
|
|
@@ -1167,15 +1759,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1167
1759
|
selector: 'atlas-sign-up',
|
|
1168
1760
|
standalone: true,
|
|
1169
1761
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1170
|
-
imports: [AttemptFlowComponent],
|
|
1762
|
+
imports: [AttemptFlowComponent, SignUpFormComponent],
|
|
1171
1763
|
template: `
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1764
|
+
@if (flow.state().attempt) {
|
|
1765
|
+
<atlas-attempt-flow
|
|
1766
|
+
[attempt]="flow.state().attempt"
|
|
1767
|
+
[busy]="flow.state().busy"
|
|
1768
|
+
[errors]="flow.state().errors"
|
|
1769
|
+
titleKey="signUp.title"
|
|
1770
|
+
(submitFlow)="flow.submit($event)"
|
|
1771
|
+
></atlas-attempt-flow>
|
|
1772
|
+
} @else {
|
|
1773
|
+
<atlas-sign-up-form
|
|
1774
|
+
[settings]="flow.settings()"
|
|
1775
|
+
[busy]="flow.state().busy"
|
|
1776
|
+
[errors]="flow.state().errors"
|
|
1777
|
+
[captcha]="flow.signUpCaptchaDrawn() ? flow.signUpCaptcha : null"
|
|
1778
|
+
(submitSignUp)="flow.create($event.values, $event.consent)"
|
|
1779
|
+
></atlas-sign-up-form>
|
|
1780
|
+
}
|
|
1179
1781
|
`,
|
|
1180
1782
|
}]
|
|
1181
1783
|
}], propDecorators: { afterUrl: [{
|
|
@@ -1626,5 +2228,5 @@ function atlasChildGuard(options = {}) {
|
|
|
1626
2228
|
* Generated bundle index. Do not edit.
|
|
1627
2229
|
*/
|
|
1628
2230
|
|
|
1629
|
-
export { ATLAS_OPTIONS, ATLAS_UI, AtlasFlowController, AtlasLoadedDirective, AtlasLoadingDirective, AtlasModule, AtlasProtectDirective, AtlasRootDirective, AtlasService, AtlasSignedInDirective, AtlasSignedOutDirective, AttemptFlowComponent, DEFAULT_TOKENS, EN_US, OrganizationProfileComponent, OrganizationSwitcherComponent, SignInComponent, SignUpComponent, THEMES, UserButtonComponent, UserProfileComponent, atlasChildGuard, atlasGuard, classFor, cssVariables, explicitTokens, provideAtlas, renderAppearance, resolveTokens, translate, withFallback };
|
|
2231
|
+
export { 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 };
|
|
1630
2232
|
//# sourceMappingURL=atlasauth-angular.mjs.map
|