Skip to content

Commit 49c8247

Browse files
committed
chore: pr-comments-add-validation
1 parent 630f470 commit 49c8247

22 files changed

Lines changed: 691 additions & 311 deletions

.changeset/embed-password-policy-in-component.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
'@forgerock/davinci-client': minor
33
---
44

5-
Add PasswordVerifyCollector to support password policy embedded in PASSWORD_VERIFY field components. The DaVinci API now returns passwordPolicy inside the PASSWORD_VERIFY field (DV-16053) instead of at the response root. The new PasswordVerifyCollector exposes the policy via output.passwordPolicy, enabling consumers to render password requirements directly from the collector.
5+
Add `ValidatedPasswordCollector` alongside `PasswordCollector`. The new collector is emitted whenever a password field carries a `passwordPolicy` — the presence of the policy is the sole discriminator between the two types, regardless of the server-side field tag (`PASSWORD` vs `PASSWORD_VERIFY`). `ValidatedPasswordCollector.output.passwordPolicy` is required; consumers can render password requirements directly from the collector.
6+
7+
Both collectors now expose a `verify: boolean` on `output` (defaults to `false`), propagated from the field when the server sends `verify: true`.
8+
9+
`store.validate(collector)` accepts a `ValidatedPasswordCollector` and returns a validator that enforces the policy's length, unique-character, repeated-character, and per-charset minimum rules. Passing a `PasswordCollector` returns the standard "cannot be validated" error.

.nxignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.opensource/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 1e3f0d7de2572ae5a0433525c5af65c73c031e67

e2e/davinci-app/components/password.ts

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,65 @@
66
*/
77
import type {
88
PasswordCollector,
9-
PasswordVerifyCollector,
9+
ValidatedPasswordCollector,
1010
Updater,
11+
Validator,
1112
} from '@forgerock/davinci-client/types';
1213
import { dotToCamelCase } from '../helper.js';
1314

15+
const UPPERCASE_RE = /^[A-Z]+$/;
16+
const LOWERCASE_RE = /^[a-z]+$/;
17+
const DIGIT_RE = /^[0-9]+$/;
18+
1419
export default function passwordComponent(
1520
formEl: HTMLFormElement,
16-
collector: PasswordCollector | PasswordVerifyCollector,
17-
updater: Updater<PasswordCollector | PasswordVerifyCollector>,
21+
collector: PasswordCollector | ValidatedPasswordCollector,
22+
updater: Updater<PasswordCollector | ValidatedPasswordCollector>,
23+
validator?: Validator,
1824
) {
25+
const collectorKey = dotToCamelCase(collector.output.key);
1926
const label = document.createElement('label');
2027
const input = document.createElement('input');
2128

22-
label.htmlFor = dotToCamelCase(collector.output.key);
29+
label.htmlFor = collectorKey;
2330
label.innerText = collector.output.label;
2431
input.type = 'password';
25-
input.id = dotToCamelCase(collector.output.key);
26-
input.name = dotToCamelCase(collector.output.key);
32+
input.id = collectorKey;
33+
input.name = collectorKey;
2734

2835
formEl?.appendChild(label);
2936
formEl?.appendChild(input);
3037

31-
// Render password policy requirements if available
32-
if (collector.type === 'PasswordVerifyCollector' && collector.output.passwordPolicy) {
33-
const policy = collector.output.passwordPolicy;
38+
if (collector.type === 'ValidatedPasswordCollector') {
39+
const passwordPolicy = collector.output.passwordPolicy;
3440
const requirementsList = document.createElement('ul');
3541
requirementsList.className = 'password-requirements';
3642

37-
if (policy.length) {
38-
const li = document.createElement('li');
39-
li.textContent = `${policy.length.min}${policy.length.max} characters`;
40-
requirementsList.appendChild(li);
43+
if (passwordPolicy.length) {
44+
const { min, max } = passwordPolicy.length;
45+
let lengthMessage: string | null = null;
46+
if (min != null && max != null) {
47+
lengthMessage = `${min}${max} characters`;
48+
} else if (min != null) {
49+
lengthMessage = `At least ${min} characters`;
50+
} else if (max != null) {
51+
lengthMessage = `At most ${max} characters`;
52+
}
53+
if (lengthMessage) {
54+
const li = document.createElement('li');
55+
li.textContent = lengthMessage;
56+
requirementsList.appendChild(li);
57+
}
4158
}
4259

43-
if (policy.minCharacters) {
44-
for (const [charset, count] of Object.entries(policy.minCharacters)) {
60+
if (passwordPolicy.minCharacters) {
61+
for (const [charset, count] of Object.entries(passwordPolicy.minCharacters)) {
4562
const li = document.createElement('li');
46-
if (charset.match(/^[A-Z]+$/)) {
63+
if (UPPERCASE_RE.test(charset)) {
4764
li.textContent = `At least ${count} uppercase letter(s)`;
48-
} else if (charset.match(/^[a-z]+$/)) {
65+
} else if (LOWERCASE_RE.test(charset)) {
4966
li.textContent = `At least ${count} lowercase letter(s)`;
50-
} else if (charset.match(/^[0-9]+$/)) {
67+
} else if (DIGIT_RE.test(charset)) {
5168
li.textContent = `At least ${count} number(s)`;
5269
} else {
5370
li.textContent = `At least ${count} special character(s)`;
@@ -61,12 +78,35 @@ export default function passwordComponent(
6178
}
6279
}
6380

64-
formEl
65-
?.querySelector(`#${dotToCamelCase(collector.output.key)}`)
66-
?.addEventListener('blur', (event: Event) => {
67-
const error = updater((event.target as HTMLInputElement).value);
68-
if (error && 'error' in error) {
69-
console.error(error.error.message);
81+
const inputEl = formEl?.querySelector(`#${collectorKey}`);
82+
const shouldValidate = collector.type === 'ValidatedPasswordCollector' && !!validator;
83+
84+
inputEl?.addEventListener('input', (event: Event) => {
85+
const value = (event.target as HTMLInputElement).value;
86+
87+
if (shouldValidate) {
88+
const result = validator(value);
89+
if (Array.isArray(result) && result.length) {
90+
let errorEl = formEl?.querySelector<HTMLUListElement>(`.${collectorKey}-error`);
91+
if (!errorEl) {
92+
errorEl = document.createElement('ul');
93+
errorEl.className = `${collectorKey}-error`;
94+
inputEl.after(errorEl);
95+
}
96+
const items = result.map((msg) => {
97+
const li = document.createElement('li');
98+
li.textContent = msg;
99+
return li;
100+
});
101+
errorEl.replaceChildren(...items);
102+
return;
70103
}
71-
});
104+
formEl?.querySelector(`.${collectorKey}-error`)?.remove();
105+
}
106+
107+
const error = updater(value);
108+
if (error && 'error' in error) {
109+
console.error(error.error.message);
110+
}
111+
});
72112
}

e2e/davinci-app/main.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,15 @@ const urlParams = new URLSearchParams(window.location.search);
236236
);
237237
} else if (
238238
collector.type === 'PasswordCollector' ||
239-
collector.type === 'PasswordVerifyCollector'
239+
collector.type === 'ValidatedPasswordCollector'
240240
) {
241241
passwordComponent(
242242
formEl, // You can ignore this; it's just for rendering
243243
collector, // This is the plain object of the collector
244244
davinciClient.update(collector), // Returns an update function for this collector
245+
collector.type === 'ValidatedPasswordCollector'
246+
? davinciClient.validate(collector)
247+
: undefined,
245248
);
246249
} else if (collector.type === 'SubmitCollector') {
247250
submitButtonComponent(

packages/davinci-client/api-report/davinci-client.api.md

Lines changed: 72 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,13 @@ export interface CollectorErrors {
147147
}
148148

149149
// @public (undocumented)
150-
export type Collectors = FlowCollector | PasswordCollector | PasswordVerifyCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | UnknownCollector;
150+
export type Collectors = FlowCollector | PasswordCollector | ValidatedPasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | UnknownCollector;
151151

152152
// @public
153153
export type CollectorValueType<T> = T extends {
154154
type: 'PasswordCollector';
155155
} ? string : T extends {
156-
type: 'PasswordVerifyCollector';
156+
type: 'ValidatedPasswordCollector';
157157
} ? string : T extends {
158158
type: 'TextCollector';
159159
category: 'SingleValueCollector';
@@ -651,8 +651,6 @@ export interface DaVinciNextResponse extends DaVinciBaseResponse {
651651
};
652652
// (undocumented)
653653
_links?: Links;
654-
// (undocumented)
655-
passwordPolicy?: PasswordPolicy;
656654
}
657655

658656
// @public
@@ -1005,7 +1003,7 @@ export type InferMultiValueCollectorType<T extends MultiValueCollectorTypes> = T
10051003
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? NoValueCollectorBase<'ReadOnlyCollector'> : T extends 'QrCodeCollector' ? QrCodeCollectorBase : NoValueCollectorBase<'NoValueCollector'>;
10061004

10071005
// @public
1008-
export type InferSingleValueCollectorType<T extends SingleValueCollectorTypes> = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'PasswordVerifyCollector' ? PasswordVerifyCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>;
1006+
export type InferSingleValueCollectorType<T extends SingleValueCollectorTypes> = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'ValidatedPasswordCollector' ? ValidatedPasswordCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>;
10091007

10101008
// @public (undocumented)
10111009
export type InferValueObjectCollectorType<T extends ObjectValueCollectorTypes> = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>;
@@ -1140,12 +1138,11 @@ fields: DaVinciField[];
11401138
formData: {
11411139
value: Record<string, unknown>;
11421140
};
1143-
passwordPolicy?: PasswordPolicy;
11441141
}, string>;
11451142

11461143
// @public
1147-
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | PasswordVerifyCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
1148-
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | PasswordVerifyCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
1144+
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | ValidatedPasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
1145+
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | ValidatedPasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
11491146
};
11501147

11511148
// @public (undocumented)
@@ -1297,7 +1294,41 @@ export interface OutgoingQueryParams {
12971294
}
12981295

12991296
// @public (undocumented)
1300-
export type PasswordCollector = SingleValueCollectorNoValue<'PasswordCollector'>;
1297+
export interface PasswordCollector {
1298+
// (undocumented)
1299+
category: 'SingleValueCollector';
1300+
// (undocumented)
1301+
error: string | null;
1302+
// (undocumented)
1303+
id: string;
1304+
// (undocumented)
1305+
input: {
1306+
key: string;
1307+
value: string | number | boolean;
1308+
type: string;
1309+
};
1310+
// (undocumented)
1311+
name: string;
1312+
// (undocumented)
1313+
output: {
1314+
key: string;
1315+
label: string;
1316+
type: string;
1317+
verify: boolean;
1318+
};
1319+
// (undocumented)
1320+
type: 'PasswordCollector';
1321+
}
1322+
1323+
// @public
1324+
export type PasswordField = {
1325+
type: 'PASSWORD' | 'PASSWORD_VERIFY';
1326+
key: string;
1327+
label: string;
1328+
required?: boolean;
1329+
verify?: boolean;
1330+
passwordPolicy?: PasswordPolicy;
1331+
};
13011332

13021333
// @public (undocumented)
13031334
export interface PasswordPolicy {
@@ -1348,42 +1379,6 @@ export interface PasswordPolicy {
13481379
updatedAt?: string;
13491380
}
13501381

1351-
// @public (undocumented)
1352-
export interface PasswordVerifyCollector {
1353-
// (undocumented)
1354-
category: 'SingleValueCollector';
1355-
// (undocumented)
1356-
error: string | null;
1357-
// (undocumented)
1358-
id: string;
1359-
// (undocumented)
1360-
input: {
1361-
key: string;
1362-
value: string | number | boolean;
1363-
type: string;
1364-
};
1365-
// (undocumented)
1366-
name: string;
1367-
// (undocumented)
1368-
output: {
1369-
key: string;
1370-
label: string;
1371-
type: string;
1372-
passwordPolicy?: PasswordPolicy;
1373-
};
1374-
// (undocumented)
1375-
type: 'PasswordVerifyCollector';
1376-
}
1377-
1378-
// @public (undocumented)
1379-
export type PasswordVerifyField = {
1380-
type: 'PASSWORD_VERIFY';
1381-
key: string;
1382-
label: string;
1383-
required?: boolean;
1384-
passwordPolicy?: PasswordPolicy;
1385-
};
1386-
13871382
// @public (undocumented)
13881383
export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<'PhoneNumberCollector', PhoneNumberInputValue, PhoneNumberOutputValue>;
13891384

@@ -1647,10 +1642,10 @@ export interface SingleValueCollectorNoValue<T extends SingleValueCollectorTypes
16471642
}
16481643

16491644
// @public (undocumented)
1650-
export type SingleValueCollectors = SingleValueCollectorNoValue<'PasswordCollector'> | PasswordVerifyCollector | SingleSelectCollectorWithValue<'SingleSelectCollector'> | SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorWithValue<'TextCollector'> | ValidatedSingleValueCollectorWithValue<'TextCollector'>;
1645+
export type SingleValueCollectors = PasswordCollector | ValidatedPasswordCollector | SingleSelectCollectorWithValue<'SingleSelectCollector'> | SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorWithValue<'TextCollector'> | ValidatedSingleValueCollectorWithValue<'TextCollector'>;
16511646

16521647
// @public
1653-
export type SingleValueCollectorTypes = 'PasswordCollector' | 'PasswordVerifyCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector';
1648+
export type SingleValueCollectorTypes = 'PasswordCollector' | 'ValidatedPasswordCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector';
16541649

16551650
// @public (undocumented)
16561651
export interface SingleValueCollectorWithValue<T extends SingleValueCollectorTypes> {
@@ -1680,11 +1675,11 @@ export interface SingleValueCollectorWithValue<T extends SingleValueCollectorTyp
16801675
}
16811676

16821677
// @public (undocumented)
1683-
export type SingleValueFields = StandardField | PasswordVerifyField | ValidatedField | SingleSelectField | ProtectField;
1678+
export type SingleValueFields = StandardField | PasswordField | ValidatedField | SingleSelectField | ProtectField;
16841679

16851680
// @public (undocumented)
16861681
export type StandardField = {
1687-
type: 'PASSWORD' | 'TEXT' | 'SUBMIT_BUTTON' | 'FLOW_BUTTON' | 'FLOW_LINK' | 'BUTTON';
1682+
type: 'TEXT' | 'SUBMIT_BUTTON' | 'FLOW_BUTTON' | 'FLOW_LINK' | 'BUTTON';
16881683
key: string;
16891684
label: string;
16901685
required?: boolean;
@@ -1802,6 +1797,34 @@ export type ValidatedField = {
18021797
};
18031798
};
18041799

1800+
// @public (undocumented)
1801+
export interface ValidatedPasswordCollector {
1802+
// (undocumented)
1803+
category: 'SingleValueCollector';
1804+
// (undocumented)
1805+
error: string | null;
1806+
// (undocumented)
1807+
id: string;
1808+
// (undocumented)
1809+
input: {
1810+
key: string;
1811+
value: string | number | boolean;
1812+
type: string;
1813+
};
1814+
// (undocumented)
1815+
name: string;
1816+
// (undocumented)
1817+
output: {
1818+
key: string;
1819+
label: string;
1820+
type: string;
1821+
verify: boolean;
1822+
passwordPolicy: PasswordPolicy;
1823+
};
1824+
// (undocumented)
1825+
type: 'ValidatedPasswordCollector';
1826+
}
1827+
18051828
// @public (undocumented)
18061829
export interface ValidatedSingleValueCollectorWithValue<T extends SingleValueCollectorTypes> {
18071830
// (undocumented)

0 commit comments

Comments
 (0)