Skip to content

Commit 1e8c728

Browse files
committed
fix(auth-ui): org-first resolution everywhere + org password-complexity policy
Extends the resolveOrg org-first/default-org fallback to every remaining org-scoped read so login/SSO/signup/reset use the ORG's IdPs, branding, login settings and password policy instead of the instance/default context - matching the old app's 'organization ?? getDefaultOrg()' invariant on every page. - SSO IdP-list flows (sso-link/action/management) route through the idp-providers getActiveIdPs choke point, which now resolves the org. - /login loader threads a resolved org into ?organization on direct entry so the whole ceremony inherits it. - All remaining per-screen + service settings/branding reads (login method/password, signup, password reset, mfa/otp/webauthn/session/authorize) resolve org-first. - Re-implement the org password-complexity policy (previously fetched nowhere): a live requirement checklist + client/server validation driven by getPasswordComplexity across signup/reset/change. findUser stays instance-wide (intentional); sso-callback left as-is (WIP-adjacent).
1 parent 14b94e1 commit 1e8c728

37 files changed

Lines changed: 1285 additions & 184 deletions
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { PasswordComplexity } from '@/modules/auth/types';
2+
import { passwordRules } from '@/resources/schemas/password-complexity';
3+
import { Form } from '@datum-cloud/datum-ui/form';
4+
5+
export interface PasswordRequirementsProps {
6+
/** The org's fetched complexity policy (from the loader). Undefined → default (min length only). */
7+
policy?: PasswordComplexity;
8+
/** The form field to watch. Defaults to `password`. */
9+
fieldName?: string;
10+
}
11+
12+
/**
13+
* Live password-requirement checklist. Reads the entered password reactively (Form.useWatch) and
14+
* ticks each requirement off as it is satisfied. The requirement set is POLICY-DRIVEN — derived
15+
* from the same `passwordRules(policy)` the Zod validator uses, so what the user sees and what the
16+
* form accepts never drift. Must be rendered inside a `<Form.Root>` (adapter context).
17+
*/
18+
export function PasswordRequirements({
19+
policy,
20+
fieldName = 'password',
21+
}: PasswordRequirementsProps): React.JSX.Element {
22+
const value = Form.useWatch<string>(fieldName) ?? '';
23+
const rules = passwordRules(policy);
24+
25+
return (
26+
<ul
27+
aria-label="Password requirements"
28+
// polite: announce a requirement becoming satisfied without interrupting typing.
29+
aria-live="polite"
30+
className="flex flex-col gap-1 text-sm">
31+
{rules.map((rule) => {
32+
const met = rule.test(value);
33+
return (
34+
<li
35+
key={rule.id}
36+
data-rule={rule.id}
37+
data-met={met}
38+
className={[
39+
'flex items-center gap-2',
40+
met ? 'text-foreground' : 'text-muted-foreground',
41+
].join(' ')}>
42+
<span
43+
aria-hidden="true"
44+
className={[
45+
'inline-flex size-4 items-center justify-center rounded-full text-xs',
46+
met ? 'bg-primary text-primary-foreground' : 'border-muted-foreground/40 border',
47+
].join(' ')}>
48+
{met ? '✓' : ''}
49+
</span>
50+
<span>{rule.requirement}</span>
51+
<span className="sr-only">{met ? ' — satisfied' : ' — not yet satisfied'}</span>
52+
</li>
53+
);
54+
})}
55+
</ul>
56+
);
57+
}

app/modules/auth/providers/fake/fake-provider.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ interface Seed {
8181
deviceAuths?: DeviceAuthSeed[]; // P6: device authorization requests keyed by userCode
8282
samlRequests?: SamlRequestSeed[]; // P6: SAML auth requests
8383
ldapUsers?: LdapUserSeed[]; // P6: LDAP credential fixtures
84+
85+
/**
86+
* The password-complexity policy getPasswordComplexity returns. Configurable so tests can drive
87+
* the policy-driven password rules (e.g. requiresSymbol=true). Defaults to min 8 / no required
88+
* character classes — the legacy behaviour the forms assumed before the policy was wired in.
89+
*/
90+
passwordComplexity?: PasswordComplexity;
8491
}
8592

8693
export class FakeAuthProvider implements AuthProvider {
@@ -117,6 +124,7 @@ export class FakeAuthProvider implements AuthProvider {
117124
private deviceAuthSeeds: DeviceAuthSeed[]; // P6
118125
private samlRequestSeeds: SamlRequestSeed[]; // P6
119126
private ldapUserSeeds: LdapUserSeed[]; // P6
127+
private passwordComplexity: PasswordComplexity; // configurable complexity policy fixture
120128
private authorizedDevices = new Set<string>(); // P6: deviceAuthId → authorized
121129
// Fake-only: per-session outcome scripts for getSession/createCallback (see FakeOutcomeScript).
122130
private sessionResults = new Map<string, FakeOutcomeScript>();
@@ -151,6 +159,13 @@ export class FakeAuthProvider implements AuthProvider {
151159
this.deviceAuthSeeds = seed.deviceAuths ?? []; // P6
152160
this.samlRequestSeeds = seed.samlRequests ?? []; // P6
153161
this.ldapUserSeeds = seed.ldapUsers ?? []; // P6
162+
this.passwordComplexity = seed.passwordComplexity ?? {
163+
minLength: 8,
164+
requiresUppercase: false,
165+
requiresLowercase: false,
166+
requiresNumber: false,
167+
requiresSymbol: false,
168+
};
154169
}
155170

156171
// ─── settings ────────────────────────────────────────────────────────────────
@@ -186,13 +201,8 @@ export class FakeAuthProvider implements AuthProvider {
186201
}
187202

188203
async getPasswordComplexity(_orgId?: string): Promise<PasswordComplexity | undefined> {
189-
return {
190-
minLength: 8,
191-
requiresUppercase: false,
192-
requiresLowercase: false,
193-
requiresNumber: false,
194-
requiresSymbol: false,
195-
};
204+
// Returns the configurable fixture (seed.passwordComplexity), defaulting to min 8 / no classes.
205+
return this.passwordComplexity;
196206
}
197207

198208
// getLegalSupport removed from the port (zero callers) — dropped here too.

0 commit comments

Comments
 (0)