Unify password policy and fix conditional dynamic fields
This commit is contained in:
parent
52333b8601
commit
e6f6cda845
|
|
@ -12,7 +12,7 @@ import { AdminCreateUserRequest, UserRole } from '@models/user';
|
|||
import { Router } from '@angular/router';
|
||||
import { AdminService } from '@services/admin/admin';
|
||||
import { FormUtility } from '@utilities/form-utility';
|
||||
import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks } from '@utilities/password-validation';
|
||||
import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks, PASSWORD_MIN_LENGTH } from '@utilities/password-validation';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-create-user-page',
|
||||
|
|
@ -58,7 +58,7 @@ export class AdminCreateUserPage extends FormUtility {
|
|||
return { kind: 'invalidEmail', message: 'Invalid email address' };
|
||||
});
|
||||
required(model.password, { message: 'Password is required' });
|
||||
minLength(model.password, 8, { message: 'Password must be at least 8 characters long' });
|
||||
minLength(model.password, PASSWORD_MIN_LENGTH, { message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` });
|
||||
validate(model.password, ({ value }) => {
|
||||
const password = value();
|
||||
if (!password || hasValidPasswordComplexity(password)) return null;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { AdminService } from '@services/admin/admin';
|
|||
import { ConfirmDialogService } from '@services/dialogs/confirm-dialog';
|
||||
import { AdminResetPasswordDialogComponent } from '@shared/admin-reset-password-dialog/admin-reset-password-dialog';
|
||||
import { FormUtility } from '@utilities/form-utility';
|
||||
import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks } from '@utilities/password-validation';
|
||||
import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks, PASSWORD_MIN_LENGTH } from '@utilities/password-validation';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-users',
|
||||
|
|
@ -77,7 +77,7 @@ export class AdminUsersPage extends FormUtility {
|
|||
};
|
||||
});
|
||||
required(model.password, { message: 'Password is required' });
|
||||
minLength(model.password, 8, { message: 'Password must be at least 8 characters long' });
|
||||
minLength(model.password, PASSWORD_MIN_LENGTH, { message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` });
|
||||
validate(model.password, ({ value }) => {
|
||||
const password = value();
|
||||
if (!password || hasValidPasswordComplexity(password)) return null;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { environment } from '@environment';
|
|||
import { UserRegistration } from '@models/user';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
import { FormUtility } from '@utilities/form-utility';
|
||||
import { hasValidPasswordComplexity } from '@utilities/password-validation';
|
||||
import { hasValidPasswordComplexity, PASSWORD_MIN_LENGTH } from '@utilities/password-validation';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -91,7 +91,7 @@ export class Signup extends FormUtility implements AfterViewInit, OnDestroy {
|
|||
maxLength(model.username, 20, { message: 'Username cannot exceed 20 characters' }),
|
||||
email(model.email, { message: 'Invalid email address' }),
|
||||
required(model.email, { message: 'Email is required' }),
|
||||
minLength(model.password, 8, { message: 'Password must be at least 8 characters long' }),
|
||||
minLength(model.password, PASSWORD_MIN_LENGTH, { message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` }),
|
||||
validate(model.password, ({ value }) => {
|
||||
const password = value();
|
||||
if (!password || hasValidPasswordComplexity(password)) {
|
||||
|
|
|
|||
|
|
@ -6,14 +6,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
|||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { FormUtility } from '@utilities/form-utility';
|
||||
|
||||
function hasValidPasswordComplexity(value: string): boolean {
|
||||
return /^\S+$/.test(value)
|
||||
&& /[a-z]/.test(value)
|
||||
&& /[A-Z]/.test(value)
|
||||
&& /\d/.test(value)
|
||||
&& /[^A-Za-z0-9]/.test(value);
|
||||
}
|
||||
import { hasValidPasswordComplexity, evaluatePasswordChecks, PASSWORD_MIN_LENGTH } from '@utilities/password-validation';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-reset-password-dialog',
|
||||
|
|
@ -40,7 +33,7 @@ export class AdminResetPasswordDialogComponent extends FormUtility {
|
|||
|
||||
readonly passwordForm = form(this.model, (model) => {
|
||||
required(model.newPassword, { message: 'New password is required' });
|
||||
minLength(model.newPassword, 8, { message: 'Password must be at least 8 characters long' });
|
||||
minLength(model.newPassword, PASSWORD_MIN_LENGTH, { message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` });
|
||||
validate(model.newPassword, ({ value }) => {
|
||||
const password = value();
|
||||
if (!password || hasValidPasswordComplexity(password)) return null;
|
||||
|
|
@ -63,17 +56,7 @@ export class AdminResetPasswordDialogComponent extends FormUtility {
|
|||
});
|
||||
|
||||
readonly canSubmit = computed(() => !this.passwordForm().invalid() && !this.saving());
|
||||
readonly newPasswordChecks = computed(() => {
|
||||
const password = this.model().newPassword;
|
||||
return [
|
||||
{ label: 'At least 8 characters', satisfied: password.length >= 8 },
|
||||
{ label: 'At least one lowercase letter', satisfied: /[a-z]/.test(password) },
|
||||
{ label: 'At least one uppercase letter', satisfied: /[A-Z]/.test(password) },
|
||||
{ label: 'At least one number', satisfied: /\d/.test(password) },
|
||||
{ label: 'At least one special character', satisfied: /[^A-Za-z0-9]/.test(password) },
|
||||
{ label: 'No spaces', satisfied: /^\S*$/.test(password) }
|
||||
];
|
||||
});
|
||||
readonly newPasswordChecks = computed(() => evaluatePasswordChecks(this.model().newPassword));
|
||||
|
||||
close(event?: Event) {
|
||||
event?.preventDefault();
|
||||
|
|
|
|||
|
|
@ -7,14 +7,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ChangePasswordRequest } from '@models/user';
|
||||
import { FormUtility } from '@utilities/form-utility';
|
||||
|
||||
function hasValidPasswordComplexity(value: string): boolean {
|
||||
return /^\S+$/.test(value)
|
||||
&& /[a-z]/.test(value)
|
||||
&& /[A-Z]/.test(value)
|
||||
&& /\d/.test(value)
|
||||
&& /[^A-Za-z0-9]/.test(value);
|
||||
}
|
||||
import { hasValidPasswordComplexity, evaluatePasswordChecks, PASSWORD_MIN_LENGTH } from '@utilities/password-validation';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-password-dialog',
|
||||
|
|
@ -45,7 +38,7 @@ export class ChangePasswordDialogComponent extends FormUtility {
|
|||
required(model.oldPassword, { message: 'Current password is required' });
|
||||
|
||||
required(model.newPassword, { message: 'New password is required' });
|
||||
minLength(model.newPassword, 8, { message: 'Password must be at least 8 characters long' });
|
||||
minLength(model.newPassword, PASSWORD_MIN_LENGTH, { message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` });
|
||||
validate(model.newPassword, ({ value }) => {
|
||||
const password = value();
|
||||
if (!password || hasValidPasswordComplexity(password)) return null;
|
||||
|
|
@ -68,17 +61,7 @@ export class ChangePasswordDialogComponent extends FormUtility {
|
|||
});
|
||||
|
||||
readonly canSubmit = computed(() => !this.passwordForm().invalid() && !this.saving());
|
||||
readonly newPasswordChecks = computed(() => {
|
||||
const password = this.model().newPassword;
|
||||
return [
|
||||
{ label: 'At least 8 characters', satisfied: password.length >= 8 },
|
||||
{ label: 'At least one lowercase letter', satisfied: /[a-z]/.test(password) },
|
||||
{ label: 'At least one uppercase letter', satisfied: /[A-Z]/.test(password) },
|
||||
{ label: 'At least one number', satisfied: /\d/.test(password) },
|
||||
{ label: 'At least one special character', satisfied: /[^A-Za-z0-9]/.test(password) },
|
||||
{ label: 'No spaces', satisfied: /^\S*$/.test(password) }
|
||||
];
|
||||
});
|
||||
readonly newPasswordChecks = computed(() => evaluatePasswordChecks(this.model().newPassword));
|
||||
|
||||
close(event?: Event) {
|
||||
event?.preventDefault();
|
||||
|
|
|
|||
|
|
@ -1391,6 +1391,12 @@ export class GenericNodeComponent {
|
|||
const propertySchema = resolveSchemaRef(rawPropertySchema as Record<string, any>, schemaRoot);
|
||||
if (shouldSkipSchemaField(key, propertySchema)) continue;
|
||||
|
||||
const fieldUi = this.toFieldUiMeta(propertySchema);
|
||||
const visible = fieldUi.visibleWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, item, (fieldPath) => resolveSchemaPath(itemSchema, fieldPath))
|
||||
);
|
||||
if (!visible) continue;
|
||||
|
||||
if (this.hasDynamicSchema(propertySchema)) {
|
||||
const dynamicFields = await this.buildDynamicSchemaFields(key, propertySchema, item);
|
||||
fields.push(...dynamicFields.fields);
|
||||
|
|
@ -1415,7 +1421,10 @@ export class GenericNodeComponent {
|
|||
rows: fieldType === 'textarea' ? 8 : undefined,
|
||||
options,
|
||||
placeholder: typeof propertySchema?.['x-ui-placeholder'] === 'string' ? String(propertySchema['x-ui-placeholder']) : undefined,
|
||||
tip: schemaFieldDescription(propertySchema) ?? undefined
|
||||
tip: schemaFieldDescription(propertySchema) ?? undefined,
|
||||
readonly: !fieldUi.enabledWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, item, (fieldPath) => resolveSchemaPath(itemSchema, fieldPath))
|
||||
)
|
||||
});
|
||||
|
||||
if (fieldType === 'checkbox') {
|
||||
|
|
@ -1464,6 +1473,13 @@ export class GenericNodeComponent {
|
|||
for (const [key, rawPropertySchema] of Object.entries(properties)) {
|
||||
const propertySchema = resolveSchemaRef(rawPropertySchema as Record<string, any>, schemaRoot);
|
||||
if (shouldSkipSchemaField(key, propertySchema)) continue;
|
||||
|
||||
const fieldUi = this.toFieldUiMeta(propertySchema);
|
||||
const visible = fieldUi.visibleWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, result, (fieldPath) => resolveSchemaPath(itemSchema, fieldPath))
|
||||
);
|
||||
if (!visible) continue;
|
||||
|
||||
if (this.hasDynamicSchema(propertySchema)) {
|
||||
const dynamicValue = this.extractNestedDialogValues(result, key);
|
||||
nextItem[key] = Object.keys(dynamicValue).length ? dynamicValue : (previousItem[key] ?? {});
|
||||
|
|
@ -1608,7 +1624,8 @@ export class GenericNodeComponent {
|
|||
const walk = async (
|
||||
node: Record<string, any>,
|
||||
pathPrefix: string,
|
||||
titlePrefix: string
|
||||
titlePrefix: string,
|
||||
inheritedUi?: { visibleWhen: UiConditionRule[]; enabledWhen: UiConditionRule[]; group: string | null }
|
||||
) => {
|
||||
const resolved = resolveSchemaRef(node, schema);
|
||||
const properties = resolved?.['properties'] as Record<string, any> | undefined;
|
||||
|
|
@ -1617,13 +1634,27 @@ export class GenericNodeComponent {
|
|||
for (const [childKey, rawChildSchema] of Object.entries(properties)) {
|
||||
const childSchema = resolveSchemaRef(rawChildSchema as Record<string, any>, schema);
|
||||
if (shouldSkipSchemaField(childKey, childSchema)) continue;
|
||||
|
||||
const childUi = this.toFieldUiMeta(childSchema, inheritedUi);
|
||||
const fieldRelativePath = pathPrefix === keyPrefix
|
||||
? childKey
|
||||
: `${pathPrefix.slice(`${keyPrefix}.`.length)}.${childKey}`;
|
||||
const visible = childUi.visibleWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, currentRecord, (fieldPath) => resolveSchemaPath(schema, fieldPath))
|
||||
);
|
||||
if (!visible) continue;
|
||||
|
||||
const nextPath = `${pathPrefix}.${childKey}`;
|
||||
const nextLabel = `${titlePrefix} ${schemaFieldLabel(childKey, childSchema)}`;
|
||||
const currentNestedValue = getValueByPath(currentRecord, nextPath.slice(`${keyPrefix}.`.length));
|
||||
const currentNestedValue = getValueByPath(currentRecord, fieldRelativePath);
|
||||
|
||||
const hasChildren = !!childSchema?.['properties'] || childSchema?.type === 'object';
|
||||
if (hasChildren) {
|
||||
await walk(childSchema as Record<string, any>, nextPath, nextLabel);
|
||||
await walk(childSchema as Record<string, any>, nextPath, nextLabel, {
|
||||
visibleWhen: childUi.visibleWhen,
|
||||
enabledWhen: childUi.enabledWhen,
|
||||
group: childUi.group
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1646,7 +1677,10 @@ export class GenericNodeComponent {
|
|||
rows: fieldType === 'textarea' ? 8 : undefined,
|
||||
options,
|
||||
placeholder: typeof childSchema?.['x-ui-placeholder'] === 'string' ? String(childSchema['x-ui-placeholder']) : undefined,
|
||||
tip: schemaFieldDescription(childSchema) ?? undefined
|
||||
tip: schemaFieldDescription(childSchema) ?? undefined,
|
||||
readonly: !childUi.enabledWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, currentRecord, (fieldPath) => resolveSchemaPath(schema, fieldPath))
|
||||
)
|
||||
});
|
||||
|
||||
if (fieldType === 'checkbox') {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export const PASSWORD_MIN_LENGTH = 10;
|
||||
|
||||
export function hasValidPasswordComplexity(value: string): boolean {
|
||||
return /^\S+$/.test(value)
|
||||
&& /[a-z]/.test(value)
|
||||
|
|
@ -10,7 +12,7 @@ export type PasswordCheck = { label: string; satisfied: boolean };
|
|||
|
||||
export function evaluatePasswordChecks(password: string): PasswordCheck[] {
|
||||
return [
|
||||
{ label: 'At least 8 characters', satisfied: password.length >= 8 },
|
||||
{ label: `At least ${PASSWORD_MIN_LENGTH} characters`, satisfied: password.length >= PASSWORD_MIN_LENGTH },
|
||||
{ label: 'At least one lowercase letter', satisfied: /[a-z]/.test(password) },
|
||||
{ label: 'At least one uppercase letter', satisfied: /[A-Z]/.test(password) },
|
||||
{ label: 'At least one number', satisfied: /\d/.test(password) },
|
||||
|
|
@ -21,7 +23,7 @@ export function evaluatePasswordChecks(password: string): PasswordCheck[] {
|
|||
|
||||
export function initialPasswordChecks(): PasswordCheck[] {
|
||||
return [
|
||||
{ label: 'At least 8 characters', satisfied: false },
|
||||
{ label: `At least ${PASSWORD_MIN_LENGTH} characters`, satisfied: false },
|
||||
{ label: 'At least one lowercase letter', satisfied: false },
|
||||
{ label: 'At least one uppercase letter', satisfied: false },
|
||||
{ label: 'At least one number', satisfied: false },
|
||||
|
|
|
|||
Loading…
Reference in New Issue