Skip to content
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ReferenceIdInterface } from '../../../reference/interfaces/reference-id.interface';
import { UserRolesInterface } from '../../user/interfaces/user-roles.interface';

export interface AuthenticatedUserInterface extends ReferenceIdInterface {}
export interface AuthenticatedUserInterface
extends ReferenceIdInterface,
Partial<UserRolesInterface> {}
4 changes: 4 additions & 0 deletions packages/nestjs-common/src/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { AuthorizationPayloadInterface } from './authorization/interfaces/author

export { PasswordPlainCurrentInterface } from './password/interfaces/password-plain-current.interface';
export { PasswordPlainInterface } from './password/interfaces/password-plain.interface';
export { PasswordStrengthTransformOptionsInterface } from './password/interfaces/password-strength-transform-options.interface';

export { OrgCreatableInterface } from './org/interfaces/org-creatable.interface';
export { OrgOwnableInterface } from './org/interfaces/org-ownable.interface';
Expand All @@ -28,6 +29,8 @@ export { UserOwnableInterface } from './user/interfaces/user-ownable.interface';
export { UserUpdatableInterface } from './user/interfaces/user-updatable.interface';
export { UserInterface } from './user/interfaces/user.interface';

export { UserRolesInterface } from './user/interfaces/user-roles.interface';

export { FederatedCreatableInterface } from './federated/interfaces/federated-creatable.interface';
export { FederatedUpdatableInterface } from './federated/interfaces/federated-updatable.interface';
export { FederatedInterface } from './federated/interfaces/federated.interface';
Expand All @@ -37,6 +40,7 @@ export { RoleAssignmentCreatableInterface } from './role/interfaces/role-assignm
export { RoleAssignmentInterface } from './role/interfaces/role-assignment.interface';
export { RoleCreatableInterface } from './role/interfaces/role-creatable.interface';
export { RoleUpdatableInterface } from './role/interfaces/role-updatable.interface';
export { RoleOwnableInterface } from './role/interfaces/role-ownable.interface';
export { RoleInterface } from './role/interfaces/role.interface';

export { OtpClearInterface } from './otp/interfaces/otp-clear.interface';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { RoleOwnableInterface } from '../../role/interfaces/role-ownable.interface';

export interface PasswordStrengthTransformOptionsInterface {
roles?: RoleOwnableInterface[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ReferenceId } from '../../../reference/interfaces/reference.types';
import { RoleInterface } from './role.interface';

export interface RoleOwnableInterface {
roleId: ReferenceId;
role?: Partial<RoleInterface>;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { UserInterface } from './user.interface';
import { PasswordPlainInterface } from '../../password/interfaces/password-plain.interface';
import { UserRolesInterface } from './user-roles.interface';

export interface UserCreatableInterface
extends Pick<UserInterface, 'username' | 'email'>,
Partial<Pick<UserInterface, 'active'>>,
Partial<PasswordPlainInterface> {}
Partial<PasswordPlainInterface>,
Partial<UserRolesInterface> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { RoleOwnableInterface } from '../../role/interfaces/role-ownable.interface';

export interface UserRolesInterface {
userRoles?: RoleOwnableInterface[];
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { UserCreatableInterface } from './user-creatable.interface';
import { PasswordPlainCurrentInterface } from '../../password/interfaces/password-plain-current.interface';
import { UserRolesInterface } from './user-roles.interface';

export interface UserUpdatableInterface
extends Partial<
Pick<UserCreatableInterface, 'email' | 'password' | 'active'>
>,
Partial<PasswordPlainCurrentInterface> {}
Partial<PasswordPlainCurrentInterface>,
Partial<UserRolesInterface> {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PasswordStrengthEnum } from '../enum/password-strength.enum';

export interface PasswordCreateObjectOptionsInterface {
/**
* Optional salt. If not provided, one will be generated.
Expand All @@ -8,4 +10,10 @@ export interface PasswordCreateObjectOptionsInterface {
* Set to true if password is required.
*/
required?: boolean;

/**
* Optional password strength requirement. If provided, will validate
* that password meets minimum strength requirements.
*/
passwordStrength?: PasswordStrengthEnum | undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PasswordStrengthEnum } from '../enum/password-strength.enum';

/**
* Password Strength Options Interface
*/
export interface PasswordStrengthOptionsInterface {
passwordStrength?: PasswordStrengthEnum | undefined;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PasswordStrengthOptionsInterface } from './password-strength-options.interface';

/**
* Password Strength Service Interface
*/
Expand All @@ -6,6 +8,10 @@ export interface PasswordStrengthServiceInterface {
* Check if Password is strong
*
* @param password - The plain text password
* @param options - The options
*/
isStrong(password: string): boolean;
isStrong(
password: string,
options?: PasswordStrengthOptionsInterface,
): boolean;
}
36 changes: 25 additions & 11 deletions packages/nestjs-password/src/services/password-creation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { PasswordHistoryPasswordInterface } from '../interfaces/password-history
import { PasswordNotStrongException } from '../exceptions/password-not-strong.exception';
import { PasswordCurrentRequiredException } from '../exceptions/password-current-required.exception';
import { PasswordUsedRecentlyException } from '../exceptions/password-used-recently.exception';
import { PasswordException } from '../exceptions/password.exception';

/**
* Service with functions related to password creation
Expand Down Expand Up @@ -74,19 +75,32 @@ export class PasswordCreationService
): Promise<
Omit<T, 'password'> | (Omit<T, 'password'> & PasswordStorageInterface)
> {
// extract properties
const { password } = object;

// is the password in the object?
if (typeof password === 'string') {
// check strength
if (!this.passwordStrengthService.isStrong(password)) {
throw new PasswordNotStrongException();
try {
// extract properties
const { password } = object;

// is the password in the object?
if (typeof password === 'string') {
// check strength
if (
!this.passwordStrengthService.isStrong(password, {
passwordStrength: options?.passwordStrength,
})
) {
throw new PasswordNotStrongException();
}
}
}

// finally hash it
return this.passwordStorageService.hashObject(object, options);
return this.passwordStorageService.hashObject(object, options);
} catch (err) {
if (err instanceof PasswordNotStrongException) {
throw err;
}
throw new PasswordException({
message: 'Failed to create password',
originalError: err,
});
}
}

public async validateCurrent(
Expand Down
26 changes: 20 additions & 6 deletions packages/nestjs-password/src/services/password-strength.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PASSWORD_MODULE_SETTINGS_TOKEN } from '../password.constants';
import { PasswordStrengthEnum } from '../enum/password-strength.enum';
import { PasswordStrengthServiceInterface } from '../interfaces/password-strength-service.interface';
import { PasswordSettingsInterface } from '../interfaces/password-settings.interface';
import { PasswordStrengthOptionsInterface } from '../interfaces/password-strength-options.interface';

/**
* Service to validate password strength
Expand All @@ -23,15 +24,28 @@ export class PasswordStrengthService
) {}

/**
* Method to check if password is strong
* Check if a password meets the minimum strength requirements.
* Uses zxcvbn to score password strength from 0-4:
*
* @param password - the plain text password
* @returns password strength
* The minimum required strength can be specified via:
* 1. The options.passwordStrength parameter - If defined it will be used as the minimum required strength
* 2. The module settings minPasswordStrength - Global minimum strength setting
* 3. Defaults to PasswordStrengthEnum.None (0) - If no other strength requirements specified
*
* @param password - The password to check
* @param options - Optional strength validation options
* @returns True if password meets minimum strength, false otherwise
*/
isStrong(password: string): boolean {
// Get min password Strength
isStrong(
password: string,
options?: PasswordStrengthOptionsInterface,
): boolean {
const { passwordStrength } = options || {};

const minStrength =
this.settings?.minPasswordStrength || PasswordStrengthEnum.None;
passwordStrength ??
this.settings?.minPasswordStrength ??
PasswordStrengthEnum.None;

// check strength of the password
const result = zxcvbn(password);
Expand Down
4 changes: 4 additions & 0 deletions packages/nestjs-user/src/config/user-default.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
USER_MODULE_DEFAULT_SETTINGS_TOKEN,
USER_MODULE_USER_PASSWORD_HISTORY_LIMIT_DAYS_DEFAULT,
} from '../user.constants';
import { defaultPasswordStrengthTransform } from '../user.utils';

/**
* Default configuration for User module.
Expand All @@ -24,6 +25,9 @@ export const userDefaultConfig = registerAs(
enabled,
limitDays: isNaN(limitDays) || limitDays < 1 ? undefined : limitDays,
},
passwordStrength: {
passwordStrengthTransform: defaultPasswordStrengthTransform,
},
};
},
);
2 changes: 2 additions & 0 deletions packages/nestjs-user/src/dto/user-create.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IntersectionType, PartialType, PickType } from '@nestjs/swagger';
import { UserCreatableInterface } from '@concepta/nestjs-common';
import { UserDto } from './user.dto';
import { UserPasswordDto } from './user-password.dto';
import { UserRolesDto } from './user-roles.dto';

/**
* User Create DTO
Expand All @@ -13,5 +14,6 @@ export class UserCreateDto
PickType(UserDto, ['username', 'email'] as const),
PartialType(PickType(UserDto, ['active'] as const)),
PartialType(UserPasswordDto),
PartialType(UserRolesDto),
)
implements UserCreatableInterface {}
17 changes: 17 additions & 0 deletions packages/nestjs-user/src/dto/user-roles.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
RoleOwnableInterface,
UserRolesInterface,
} from '@concepta/nestjs-common';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class UserRolesDto implements UserRolesInterface {
@Expose()
@ApiPropertyOptional({
type: 'array',
isArray: true,
description: 'User roles',
})
userRoles?: RoleOwnableInterface[];
}
2 changes: 2 additions & 0 deletions packages/nestjs-user/src/dto/user-update.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UserUpdatableInterface } from '@concepta/nestjs-common';
import { UserDto } from './user.dto';
import { UserPasswordDto } from './user-password.dto';
import { UserPasswordUpdateDto } from './user-password-update.dto';
import { UserRolesDto } from './user-roles.dto';

/**
* User Update DTO
Expand All @@ -14,5 +15,6 @@ export class UserUpdateDto
PartialType(PickType(UserDto, ['email', 'active'] as const)),
PartialType(UserPasswordDto),
PartialType(UserPasswordUpdateDto),
PartialType(UserRolesDto),
)
implements UserUpdatableInterface {}
3 changes: 3 additions & 0 deletions packages/nestjs-user/src/entities/user-postgres.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Column } from 'typeorm';
import { CommonPostgresEntity } from '@concepta/typeorm-common';
import { UserEntityInterface } from '../interfaces/user-entity.interface';
import { UserPasswordHistoryEntityInterface } from '../interfaces/user-password-history-entity.interface';
import { RoleOwnableInterface } from '@concepta/nestjs-common';

/**
* User Entity
Expand Down Expand Up @@ -40,5 +41,7 @@ export abstract class UserPostgresEntity
@Column({ type: 'text', nullable: true, default: null })
passwordSalt: string | null = null;

userRoles?: RoleOwnableInterface[];

userPasswordHistory?: UserPasswordHistoryEntityInterface;
}
3 changes: 3 additions & 0 deletions packages/nestjs-user/src/entities/user-sqlite.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Column } from 'typeorm';
import { CommonSqliteEntity } from '@concepta/typeorm-common';
import { UserEntityInterface } from '../interfaces/user-entity.interface';
import { UserPasswordHistoryEntityInterface } from '../interfaces/user-password-history-entity.interface';
import { RoleOwnableInterface } from '@concepta/nestjs-common';

export abstract class UserSqliteEntity
extends CommonSqliteEntity
Expand Down Expand Up @@ -37,5 +38,7 @@ export abstract class UserSqliteEntity
@Column({ type: 'text', nullable: true, default: null })
passwordSalt: string | null = null;

userRoles?: RoleOwnableInterface[];

userPasswordHistory?: UserPasswordHistoryEntityInterface;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { HttpStatus } from '@nestjs/common';
import { UserException } from './user-exception';

export class UserRolePasswordException extends UserException {
constructor(options?: RuntimeExceptionOptions) {
super({
message: 'Unable to get password strength for role',
httpStatus: HttpStatus.BAD_REQUEST,
...options,
});

this.errorCode = 'USER_ROLES_ERROR';
}
}
16 changes: 16 additions & 0 deletions packages/nestjs-user/src/exceptions/user-roles-exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { HttpStatus } from '@nestjs/common';
import { UserException } from './user-exception';

export class UserRolesException extends UserException {
constructor(userId: string, options?: RuntimeExceptionOptions) {
super({
message: 'Unable to get user roles for user ${userId}',
messageParams: [userId],
httpStatus: HttpStatus.BAD_REQUEST,
...options,
});

this.errorCode = 'USER_ROLES_ERROR';
}
}
1 change: 1 addition & 0 deletions packages/nestjs-user/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ export { UserResource } from './user.types';
export { UserException } from './exceptions/user-exception';
export { UserBadRequestException } from './exceptions/user-bad-request-exception';
export { UserNotFoundException } from './exceptions/user-not-found-exception';
export { UserRolesException } from './exceptions/user-roles-exception';
5 changes: 3 additions & 2 deletions packages/nestjs-user/src/interfaces/user-entity.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { UserInterface } from '@concepta/nestjs-common';
import { UserInterface, UserRolesInterface } from '@concepta/nestjs-common';
import { PasswordStorageInterface } from '@concepta/nestjs-password';

export interface UserEntityInterface
extends UserInterface,
PasswordStorageInterface {}
PasswordStorageInterface,
UserRolesInterface {}
2 changes: 2 additions & 0 deletions packages/nestjs-user/src/interfaces/user-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { UserLookupServiceInterface } from './user-lookup-service.interface';
import { UserMutateServiceInterface } from './user-mutate-service.interface';
import { UserPasswordServiceInterface } from './user-password-service.interface';
import { UserPasswordHistoryServiceInterface } from './user-password-history-service.interface';
import { UserRoleServiceInterface } from './user-role-service.interface';

export interface UserOptionsInterface {
settings?: UserSettingsInterface;
userLookupService?: UserLookupServiceInterface;
userMutateService?: UserMutateServiceInterface;
userPasswordService?: UserPasswordServiceInterface;
userRoleService?: UserRoleServiceInterface;
userPasswordHistoryService?: UserPasswordHistoryServiceInterface;
userAccessQueryService?: CanAccess;
}
Loading