Skip to content

Commit fd7d62d

Browse files
committed
chore: pr-comments-add-validation
1 parent 89159b3 commit fd7d62d

22 files changed

Lines changed: 711 additions & 331 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
@@ -239,12 +239,15 @@ const urlParams = new URLSearchParams(window.location.search);
239239
);
240240
} else if (
241241
collector.type === 'PasswordCollector' ||
242-
collector.type === 'PasswordVerifyCollector'
242+
collector.type === 'ValidatedPasswordCollector'
243243
) {
244244
passwordComponent(
245245
formEl, // You can ignore this; it's just for rendering
246246
collector, // This is the plain object of the collector
247247
davinciClient.update(collector), // Returns an update function for this collector
248+
collector.type === 'ValidatedPasswordCollector'
249+
? davinciClient.validate(collector)
250+
: undefined,
248251
);
249252
} else if (collector.type === 'SubmitCollector') {
250253
submitButtonComponent(

0 commit comments

Comments
 (0)