Mobile Number is verified successfully.
Mobile Number:{mobileNumber}.
Reference:{reference}.
Support Email:{supportEmail}.
Visit us:{homeUrl}.
By:{homeName}.
--------------------------------------------------------------------------------
/src/modules/feature-flag/enums/feature-flag.status-code.enum.ts:
--------------------------------------------------------------------------------
1 | export enum EnumFeatureFlagStatusCodeError {
2 | notFound = 5080,
3 | serviceUnavailable = 5081,
4 | invalidMetadata = 5082,
5 | predefinedKeyLengthExceeded = 5083,
6 | predefinedKeyEmpty = 5084,
7 | predefinedKeyTypeInvalid = 5085,
8 | }
9 |
--------------------------------------------------------------------------------
/src/configs/session.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IConfigSession {
4 | keyPattern: string;
5 | }
6 |
7 | export default registerAs(
8 | 'session',
9 | (): IConfigSession => ({
10 | keyPattern: 'User:{userId}:Session:{sessionId}',
11 | })
12 | );
13 |
--------------------------------------------------------------------------------
/src/modules/hello/hello.module.ts:
--------------------------------------------------------------------------------
1 | import { HelloService } from '@modules/hello/services/hello.service';
2 | import { Module } from '@nestjs/common';
3 |
4 | @Module({
5 | controllers: [],
6 | providers: [HelloService],
7 | exports: [HelloService],
8 | imports: [],
9 | })
10 | export class HelloModule {}
11 |
--------------------------------------------------------------------------------
/src/modules/hello/interfaces/hello.service.interface.ts:
--------------------------------------------------------------------------------
1 | import { IResponseReturn } from '@common/response/interfaces/response.interface';
2 | import { HelloResponseDto } from '@modules/hello/dtos/response/hello.response.dto';
3 |
4 | export interface IHelloService {
5 | hello(): Promise>;
6 | }
7 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.generate-import.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { PickType } from '@nestjs/swagger';
2 | import { AwsS3PresignRequestDto } from '@common/aws/dtos/request/aws.s3-presign.request.dto';
3 |
4 | export class UserGenerateImportRequestDto extends PickType(
5 | AwsS3PresignRequestDto,
6 | ['size']
7 | ) {}
8 |
--------------------------------------------------------------------------------
/src/common/file/constants/file.constant.ts:
--------------------------------------------------------------------------------
1 | import bytes from 'bytes';
2 |
3 | /**
4 | * Maximum file size allowed in bytes (10MB).
5 | */
6 | export const FileSizeInBytes: number = bytes('10mb');
7 |
8 | /**
9 | * Maximum number of multiple files allowed for upload.
10 | */
11 | export const FileMaxMultiple: number = 3;
12 |
--------------------------------------------------------------------------------
/src/languages/en/health.json:
--------------------------------------------------------------------------------
1 | {
2 | "checkAws": "AWS health check completed successfully.",
3 | "checkDatabase": "Database health check completed successfully.",
4 | "checkInstance": "Instance health check completed successfully.",
5 | "checkThirdParty": "Third-party services health check completed successfully."
6 | }
7 |
--------------------------------------------------------------------------------
/src/modules/email/templates/email.forgot-password.template.hbs:
--------------------------------------------------------------------------------
1 | Hi{username},
You reset password link is here {link}
Expired until {expiredAt} ({expiredInMinutes} in minutes).
Reference:{reference}.
Support Email:{supportEmail}.Visit us:{homeUrl}.
By:{homeName}
2 |
--------------------------------------------------------------------------------
/src/modules/session/interfaces/session.interface.ts:
--------------------------------------------------------------------------------
1 | import { Session, User } from '@prisma/client';
2 |
3 | export interface ISession extends Session {
4 | user: User;
5 | }
6 |
7 | export interface ISessionCache {
8 | userId: string;
9 | sessionId: string;
10 | expiredAt: Date;
11 | jti: string;
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.forgot-password.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { UserLoginRequestDto } from '@modules/user/dtos/request/user.login.request.dto';
2 | import { PickType } from '@nestjs/swagger';
3 |
4 | export class UserForgotPasswordRequestDto extends PickType(
5 | UserLoginRequestDto,
6 | ['email'] as const
7 | ) {}
8 |
--------------------------------------------------------------------------------
/src/modules/email/templates/email.temp-password.template.hbs:
--------------------------------------------------------------------------------
1 | Hi{username},
Your password is {password}.
Expired At{passwordExpiredAt}.
Password Created At {passwordCreatedAt}
Support Email:{supportEmail}.
Visit us:{homeUrl}.
By:{homeName}.
--------------------------------------------------------------------------------
/src/modules/api-key/enums/api-key.status-code.enum.ts:
--------------------------------------------------------------------------------
1 | export enum EnumApiKeyStatusCodeError {
2 | xApiKeyRequired = 5100,
3 | xApiKeyNotFound = 5101,
4 | xApiKeyInvalid = 5102,
5 | xApiKeyForbidden = 5103,
6 | xApiKeyPredefinedNotFound = 5104,
7 | expired = 5105,
8 | notFound = 5107,
9 | inactive = 5108,
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/email/templates/email.verification.template.hbs:
--------------------------------------------------------------------------------
1 | Hi{username},
Here is your Link {link} for verification.
The Link will expire until {expiredAt} ({expiredInMinutes} in minutes).
Reference:{reference}.
Support Email:{supportEmail}.
Visit us:{homeUrl}.
By:{homeName}.
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.send-email-verification.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { UserLoginRequestDto } from '@modules/user/dtos/request/user.login.request.dto';
2 | import { PickType } from '@nestjs/swagger';
3 |
4 | export class UserSendEmailVerificationRequestDto extends PickType(
5 | UserLoginRequestDto,
6 | ['email'] as const
7 | ) {}
8 |
--------------------------------------------------------------------------------
/src/common/pagination/enums/pagination.enum.ts:
--------------------------------------------------------------------------------
1 | export enum EnumPaginationFilterDateBetweenType {
2 | start = 'start',
3 | end = 'end',
4 | }
5 |
6 | export enum EnumPaginationOrderDirectionType {
7 | asc = 'asc',
8 | desc = 'desc',
9 | }
10 |
11 | export enum EnumPaginationType {
12 | offset = 'offset',
13 | cursor = 'cursor',
14 | }
15 |
--------------------------------------------------------------------------------
/src/modules/term-policy/constants/term-policy.list.constant.ts:
--------------------------------------------------------------------------------
1 | import { EnumTermPolicyStatus, EnumTermPolicyType } from '@prisma/client';
2 |
3 | export const TermPolicyDefaultStatus = Object.values(EnumTermPolicyStatus);
4 | export const TermPolicyDefaultType = Object.values(EnumTermPolicyType);
5 | export const TermPolicyDefaultAvailableOrderBy = ['publishedAt', 'version'];
6 |
--------------------------------------------------------------------------------
/src/modules/email/templates/email.create-by-admin.template.hbs:
--------------------------------------------------------------------------------
1 | Hi {username},
Your account has created by admin and your password is {password}.
Expired At {passwordExpiredAt}.
Password Created At {passwordCreatedAt}
Support Email: {supportEmail}.
Visit us: {homeUrl}.
By: {homeName}.
--------------------------------------------------------------------------------
/src/modules/term-policy/enums/term-policy.status-code.enum.ts:
--------------------------------------------------------------------------------
1 | export enum EnumTermPolicyStatusCodeError {
2 | notFound = 6100,
3 | exist = 6101,
4 | languageDuplicate = 6102,
5 | alreadyAccepted = 6103,
6 | requiredInvalid = 6104,
7 | statusInvalid = 6105,
8 | contentNotFound = 6106,
9 | contentExist = 6107,
10 | contentEmpty = 6108,
11 | }
12 |
--------------------------------------------------------------------------------
/src/modules/term-policy/interfaces/term-policy.interface.ts:
--------------------------------------------------------------------------------
1 | import { TermPolicy, TermPolicyUserAcceptance, User } from '@prisma/client';
2 |
3 | export interface ITermPolicyUserAcceptance extends TermPolicyUserAcceptance {
4 | user: User;
5 | termPolicy: TermPolicy;
6 | }
7 |
8 | export interface ITermPolicyImportResult {
9 | key: string;
10 | size: number;
11 | }
12 |
--------------------------------------------------------------------------------
/src/configs/feature-flag.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IConfigFeatureFlag {
4 | cachePrefixKey: string;
5 | cacheTtlMs: number;
6 | }
7 |
8 | export default registerAs(
9 | 'featureFlag',
10 | (): IConfigFeatureFlag => ({
11 | cachePrefixKey: 'FeatureFlag',
12 | cacheTtlMs: 60 * 60,
13 | })
14 | );
15 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.two-factor-disable.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { UserLoginVerifyTwoFactorRequestDto } from '@modules/user/dtos/request/user.login-verify-two-factor.request.dto';
2 | import { OmitType } from '@nestjs/swagger';
3 |
4 | export class UserTwoFactorDisableRequestDto extends OmitType(
5 | UserLoginVerifyTwoFactorRequestDto,
6 | ['challengeToken'] as const
7 | ) {}
8 |
--------------------------------------------------------------------------------
/src/configs/home.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IConfigHome {
4 | name: string;
5 | url: string;
6 | }
7 |
8 | export default registerAs(
9 | 'home',
10 | (): IConfigHome => ({
11 | name: process.env.HOME_NAME ?? 'NestJs ACK Boilerplate',
12 | url: process.env.HOME_URL ?? 'https://example.com',
13 | })
14 | );
15 |
--------------------------------------------------------------------------------
/src/modules/email/dtos/email.mobile-number-verified.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { EmailVerifiedDto } from '@modules/email/dtos/email.verified.dto';
3 |
4 | export class EmailMobileNumberVerifiedDto extends EmailVerifiedDto {
5 | @ApiProperty({
6 | required: true,
7 | description: 'Mobile number',
8 | })
9 | mobileNumber: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/term-policy/dtos/request/term-policy.remove-content.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { TermPolicyContentPresignRequestDto } from '@modules/term-policy/dtos/request/term-policy.content-presign.request.dto';
2 | import { PickType } from '@nestjs/swagger';
3 |
4 | export class TermPolicyRemoveContentRequestDto extends PickType(
5 | TermPolicyContentPresignRequestDto,
6 | ['language'] as const
7 | ) {}
8 |
--------------------------------------------------------------------------------
/src/common/helper/interfaces/helper.interface.ts:
--------------------------------------------------------------------------------
1 | import { EnumHelperDateDayOf } from '@common/helper/enums/helper.enum';
2 |
3 | export interface IHelperPasswordOptions {
4 | length: number;
5 | }
6 |
7 | export interface IHelperDateCreateOptions {
8 | dayOf?: EnumHelperDateDayOf;
9 | }
10 |
11 | export interface IHelperEmailValidation {
12 | validated: boolean;
13 | messagePath?: string;
14 | }
15 |
--------------------------------------------------------------------------------
/src/configs/email.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IConfigEmail {
4 | noreply: string;
5 | support: string;
6 | admin: string;
7 | }
8 |
9 | export default registerAs(
10 | 'email',
11 | (): IConfigEmail => ({
12 | noreply: 'noreply@mail.com',
13 | support: 'support@mail.com',
14 | admin: 'admin@mail.com',
15 | })
16 | );
17 |
--------------------------------------------------------------------------------
/src/modules/session/constants/session.doc.constant.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiParamOptions } from '@nestjs/swagger';
3 |
4 | export const SessionDocParamsId: ApiParamOptions[] = [
5 | {
6 | name: 'sessionId',
7 | allowEmptyValue: false,
8 | required: true,
9 | type: 'string',
10 | example: faker.database.mongodbObjectId(),
11 | },
12 | ];
13 |
--------------------------------------------------------------------------------
/dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine
2 | LABEL maintainer="andrechristikan@gmail.com"
3 |
4 | WORKDIR /app
5 |
6 | RUN corepack enable && corepack prepare pnpm@latest --activate
7 | COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
8 |
9 | RUN set -x && pnpm install --frozen-lockfile
10 |
11 | RUN touch .env
12 |
13 | COPY . .
14 |
15 | RUN pnpm db:generate
16 |
17 | EXPOSE 3000
18 |
19 | CMD [ "pnpm", "start:dev" ]
20 |
--------------------------------------------------------------------------------
/src/modules/policy/interfaces/policy.interface.ts:
--------------------------------------------------------------------------------
1 | import { InferSubjects, MongoAbility } from '@casl/ability';
2 | import {
3 | EnumPolicyAction,
4 | EnumPolicySubject,
5 | } from '@modules/policy/enums/policy.enum';
6 |
7 | export type IPolicyAbilitySubject = InferSubjects | 'all';
8 |
9 | export type IPolicyAbilityRule = MongoAbility<
10 | [EnumPolicyAction, IPolicyAbilitySubject]
11 | >;
12 |
--------------------------------------------------------------------------------
/src/modules/policy/interfaces/policy.service.interface.ts:
--------------------------------------------------------------------------------
1 | import { IRequestApp } from '@common/request/interfaces/request.interface';
2 | import { RoleAbilityRequestDto } from '@modules/role/dtos/request/role.ability.request.dto';
3 |
4 | export interface IPolicyService {
5 | validatePolicyGuard(
6 | request: IRequestApp,
7 | requiredAbilities: RoleAbilityRequestDto[]
8 | ): Promise;
9 | }
10 |
--------------------------------------------------------------------------------
/src/modules/policy/policy.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { PolicyAbilityFactory } from '@modules/policy/factories/policy.factory';
3 | import { PolicyService } from '@modules/policy/services/policy.service';
4 |
5 | @Global()
6 | @Module({
7 | providers: [PolicyAbilityFactory, PolicyService],
8 | exports: [PolicyService],
9 | imports: [],
10 | })
11 | export class PolicyModule {}
12 |
--------------------------------------------------------------------------------
/src/modules/api-key/dtos/request/api-key.update-status.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsBoolean, IsNotEmpty } from 'class-validator';
3 |
4 | export class ApiKeyUpdateStatusRequestDto {
5 | @ApiProperty({
6 | example: true,
7 | required: true,
8 | description: 'API Key status',
9 | })
10 | @IsNotEmpty()
11 | @IsBoolean()
12 | isActive: boolean;
13 | }
14 |
--------------------------------------------------------------------------------
/src/configs/term-policy.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IConfigTermPolicy {
4 | uploadContentPath: string;
5 | contentPublicPath: string;
6 | }
7 |
8 | export default registerAs(
9 | 'termPolicy',
10 | (): IConfigTermPolicy => ({
11 | uploadContentPath: 'term-policies/{type}/v{version}',
12 | contentPublicPath: 'term-policies/{type}/v{version}',
13 | })
14 | );
15 |
--------------------------------------------------------------------------------
/src/app/interfaces/app.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IMessageProperties,
3 | IMessageValidationError,
4 | } from '@common/message/interfaces/message.interface';
5 |
6 | export interface IAppException {
7 | statusCode: number;
8 | message: string;
9 | messageProperties?: IMessageProperties;
10 | data?: T;
11 | metadata?: Record;
12 | errors?: IMessageValidationError[];
13 | _error?: unknown;
14 | }
15 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/response/user.two-factor-enable.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 |
3 | export class UserTwoFactorEnableResponseDto {
4 | @ApiProperty({
5 | required: true,
6 | description:
7 | 'List of newly generated backup codes. Each code can be used once.',
8 | type: [String],
9 | example: ['ABCD1234EF', 'ZXCV5678GH'],
10 | })
11 | backupCodes: string[];
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/term-policy/interfaces/term-policy.template-service.interface.ts:
--------------------------------------------------------------------------------
1 | import { ITermPolicyImportResult } from '@modules/term-policy/interfaces/term-policy.interface';
2 |
3 | export interface ITermPolicyTemplateService {
4 | importTermsOfService(): Promise;
5 | importPrivacy(): Promise;
6 | importCookie(): Promise;
7 | importMarketing(): Promise;
8 | }
9 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.verify-email.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty } from '@nestjs/swagger';
3 | import { IsNotEmpty, IsString } from 'class-validator';
4 |
5 | export class UserVerifyEmailRequestDto {
6 | @ApiProperty({
7 | description: 'Verification token',
8 | example: faker.string.alphanumeric(20),
9 | })
10 | @IsString()
11 | @IsNotEmpty()
12 | token: string;
13 | }
14 |
--------------------------------------------------------------------------------
/src/configs/user.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IUserConfig {
4 | usernamePrefix: string;
5 | usernamePattern: RegExp;
6 | uploadPhotoProfilePath: string;
7 | }
8 |
9 | export default registerAs(
10 | 'user',
11 | (): IUserConfig => ({
12 | usernamePrefix: 'user',
13 | usernamePattern: /^[a-zA-Z0-9-_]+$/,
14 | uploadPhotoProfilePath: 'users/{userId}/profile',
15 | })
16 | );
17 |
--------------------------------------------------------------------------------
/src/modules/email/enums/email.enum.ts:
--------------------------------------------------------------------------------
1 | export enum EnumSendEmailProcess {
2 | changePassword = 'changePassword',
3 | temporaryPassword = 'temporaryPassword',
4 | welcome = 'welcome',
5 | createByAdmin = 'createByAdmin',
6 | forgotPassword = 'forgotPassword',
7 | verification = 'verification',
8 | emailVerified = 'emailVerified',
9 | mobileNumberVerified = 'mobileNumberVerified',
10 | resetTwoFactorByAdmin = 'resetTwoFactorByAdmin',
11 | }
12 |
--------------------------------------------------------------------------------
/src/configs/database.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IConfigDatabase {
4 | url: string;
5 | debug: boolean;
6 | }
7 |
8 | export default registerAs(
9 | 'database',
10 | (): IConfigDatabase => ({
11 | url:
12 | process.env?.DATABASE_URL ??
13 | 'mongodb://localhost:27017,localhost:27018,localhost:27019',
14 | debug: process.env.DATABASE_DEBUG === 'true',
15 | })
16 | );
17 |
--------------------------------------------------------------------------------
/src/queues/queue.module.ts:
--------------------------------------------------------------------------------
1 | import { EmailModule } from '@modules/email/email.module';
2 | import { EmailProcessor } from '@modules/email/processors/email.processor';
3 | import { Module } from '@nestjs/common';
4 |
5 | /**
6 | * Module for managing queue processors
7 | * Imports EmailModule and provides EmailProcessor for handling email queues
8 | */
9 | @Module({
10 | imports: [EmailModule],
11 | providers: [EmailProcessor],
12 | })
13 | export class QueueModule {}
14 |
--------------------------------------------------------------------------------
/src/common/file/file.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { FileService } from '@common/file/services/file.service';
3 |
4 | /**
5 | * Global module providing file handling services.
6 | * Exports FileService for use in other modules requiring file operations.
7 | */
8 | @Global()
9 | @Module({
10 | providers: [FileService],
11 | exports: [FileService],
12 | imports: [],
13 | controllers: [],
14 | })
15 | export class FileModule {}
16 |
--------------------------------------------------------------------------------
/src/common/request/constants/request.constant.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Meta key for request custom timeout decorator.
3 | */
4 | export const RequestCustomTimeoutMetaKey = 'RequestCustomTimeoutMetaKey';
5 |
6 | /**
7 | * Meta key for request custom timeout value.
8 | */
9 | export const RequestCustomTimeoutValueMetaKey =
10 | 'RequestCustomTimeoutValueMetaKey';
11 |
12 | /**
13 | * Meta key for request environment.
14 | */
15 | export const RequestEnvMetaKey = 'RequestEnvMetaKey';
16 |
--------------------------------------------------------------------------------
/src/common/helper/helper.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { HelperService } from '@common/helper/services/helper.service';
3 |
4 | /**
5 | * Global module providing helper utility services.
6 | * Exports HelperService globally for use throughout the application.
7 | */
8 | @Global()
9 | @Module({
10 | providers: [HelperService],
11 | exports: [HelperService],
12 | controllers: [],
13 | imports: [],
14 | })
15 | export class HelperModule {}
16 |
--------------------------------------------------------------------------------
/src/common/file/dtos/file.single.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IFile } from '@common/file/interfaces/file.interface';
3 |
4 | /**
5 | * DTO class for handling single file upload.
6 | * This class defines the structure for API endpoints that accept a single file.
7 | */
8 | export class FileSingleDto {
9 | @ApiProperty({
10 | type: 'string',
11 | format: 'binary',
12 | description: 'Single file',
13 | })
14 | file: IFile;
15 | }
16 |
--------------------------------------------------------------------------------
/src/modules/role/role.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { RoleService } from '@modules/role/services/role.service';
3 | import { RoleUtil } from '@modules/role/utils/role.util';
4 | import { RoleRepository } from '@modules/role/repositories/role.repository';
5 |
6 | @Global()
7 | @Module({
8 | providers: [RoleService, RoleUtil, RoleRepository],
9 | exports: [RoleService, RoleUtil, RoleRepository],
10 | imports: [],
11 | })
12 | export class RoleModule {}
13 |
--------------------------------------------------------------------------------
/src/configs/message.config.ts:
--------------------------------------------------------------------------------
1 | import { EnumMessageLanguage } from '@common/message/enums/message.enum';
2 | import { registerAs } from '@nestjs/config';
3 |
4 | export interface IConfigMessage {
5 | availableLanguage: string[];
6 | language: string;
7 | }
8 |
9 | export default registerAs(
10 | 'message',
11 | (): IConfigMessage => ({
12 | availableLanguage: Object.values(EnumMessageLanguage),
13 | language: process.env.APP_LANGUAGE ?? EnumMessageLanguage.en,
14 | })
15 | );
16 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.check.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { PickType } from '@nestjs/swagger';
2 | import { UserCreateRequestDto } from '@modules/user/dtos/request/user.create.request.dto';
3 | import { UserClaimUsernameRequestDto } from '@modules/user/dtos/request/user.claim-username.request.dto';
4 |
5 | export class UserCheckUsernameRequestDto extends UserClaimUsernameRequestDto {}
6 |
7 | export class UserCheckEmailRequestDto extends PickType(UserCreateRequestDto, [
8 | 'email',
9 | ] as const) {}
10 |
--------------------------------------------------------------------------------
/src/configs/doc.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IConfigDoc {
4 | name: string;
5 | description: string;
6 | prefix: string;
7 | version: string;
8 | }
9 |
10 | export default registerAs(
11 | 'doc',
12 | (): IConfigDoc => ({
13 | name: `${process.env.APP_NAME ?? 'ACKNestJs'} APIs Specification`,
14 | description: 'Section for describe whole APIs',
15 | prefix: '/docs',
16 | version: '3.1.0',
17 | })
18 | );
19 |
--------------------------------------------------------------------------------
/src/modules/api-key/dtos/request/api-key.update.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty } from '@nestjs/swagger';
3 | import { IsOptional, IsString, MaxLength } from 'class-validator';
4 |
5 | export class ApiKeyUpdateRequestDto {
6 | @ApiProperty({
7 | description: 'Api Key name',
8 | example: faker.company.name(),
9 | required: false,
10 | })
11 | @IsOptional()
12 | @IsString()
13 | @MaxLength(100)
14 | name?: string;
15 | }
16 |
--------------------------------------------------------------------------------
/src/modules/country/interfaces/country.service.interface.ts:
--------------------------------------------------------------------------------
1 | import { IPaginationQueryOffsetParams } from '@common/pagination/interfaces/pagination.interface';
2 | import { IResponsePagingReturn } from '@common/response/interfaces/response.interface';
3 | import { CountryResponseDto } from '@modules/country/dtos/response/country.response.dto';
4 |
5 | export interface ICountryService {
6 | getList(
7 | pagination: IPaginationQueryOffsetParams
8 | ): Promise>;
9 | }
10 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.create-social.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { UserLoginRequestDto } from '@modules/user/dtos/request/user.login.request.dto';
2 | import { UserSignUpRequestDto } from '@modules/user/dtos/request/user.sign-up.request.dto';
3 | import { IntersectionType, OmitType, PickType } from '@nestjs/swagger';
4 |
5 | export class UserCreateSocialRequestDto extends IntersectionType(
6 | OmitType(UserSignUpRequestDto, ['email', 'from', 'password'] as const),
7 | PickType(UserLoginRequestDto, ['from'])
8 | ) {}
9 |
--------------------------------------------------------------------------------
/src/common/file/dtos/file.multiple.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IFile } from '@common/file/interfaces/file.interface';
3 |
4 | /**
5 | * DTO class for handling multiple file uploads.
6 | * This class defines the structure for API endpoints that accept multiple files.
7 | */
8 | export class FileMultipleDto {
9 | @ApiProperty({
10 | type: 'array',
11 | items: { type: 'string', format: 'binary', description: 'Multi file' },
12 | })
13 | files: IFile[];
14 | }
15 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/response/user.check.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 |
3 | export class UserCheckEmailResponseDto {
4 | @ApiProperty({
5 | required: true,
6 | })
7 | badWord: boolean;
8 |
9 | @ApiProperty({
10 | required: true,
11 | })
12 | exist: boolean;
13 | }
14 |
15 | export class UserCheckUsernameResponseDto extends UserCheckEmailResponseDto {
16 | @ApiProperty({
17 | required: true,
18 | })
19 | pattern: boolean;
20 | }
21 |
--------------------------------------------------------------------------------
/src/modules/policy/enums/policy.enum.ts:
--------------------------------------------------------------------------------
1 | export enum EnumPolicyAction {
2 | manage = 'manage',
3 | read = 'read',
4 | create = 'create',
5 | update = 'update',
6 | delete = 'delete',
7 | }
8 |
9 | export enum EnumPolicySubject {
10 | all = 'all',
11 | apiKey = 'apiKey',
12 | role = 'role',
13 | user = 'user',
14 | session = 'session',
15 | activityLog = 'activityLog',
16 | passwordHistory = 'passwordHistory',
17 | termPolicy = 'termPolicy',
18 | featureFlag = 'featureFlag',
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.update-status.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { EnumUserStatus } from '@prisma/client';
3 | import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
4 |
5 | export class UserUpdateStatusRequestDto {
6 | @ApiProperty({
7 | required: true,
8 | enum: EnumUserStatus,
9 | default: EnumUserStatus.active,
10 | })
11 | @IsString()
12 | @IsEnum(EnumUserStatus)
13 | @IsNotEmpty()
14 | status: EnumUserStatus;
15 | }
16 |
--------------------------------------------------------------------------------
/src/modules/api-key/api-key.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { ApiKeyService } from '@modules/api-key/services/api-key.service';
3 | import { ApiKeyUtil } from '@modules/api-key/utils/api-key.util';
4 | import { ApiKeyRepository } from '@modules/api-key/repositories/api-key.repository';
5 |
6 | @Global()
7 | @Module({
8 | providers: [ApiKeyService, ApiKeyUtil, ApiKeyRepository],
9 | exports: [ApiKeyService, ApiKeyUtil, ApiKeyRepository],
10 | imports: [],
11 | })
12 | export class ApiKeyModule {}
13 |
--------------------------------------------------------------------------------
/src/modules/term-policy/dtos/term-policy.content.dto.ts:
--------------------------------------------------------------------------------
1 | import { AwsS3Dto } from '@common/aws/dtos/aws.s3.dto';
2 | import { EnumMessageLanguage } from '@common/message/enums/message.enum';
3 | import { ApiProperty } from '@nestjs/swagger';
4 |
5 | export class TermContentDto extends AwsS3Dto {
6 | @ApiProperty({
7 | required: true,
8 | description: 'Language of the term document',
9 | example: EnumMessageLanguage.en,
10 | enum: EnumMessageLanguage,
11 | })
12 | readonly language: EnumMessageLanguage;
13 | }
14 |
--------------------------------------------------------------------------------
/src/modules/country/country.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { CountryService } from '@modules/country/services/country.service';
3 | import { CountryUtil } from '@modules/country/utils/country.util';
4 | import { CountryRepository } from '@modules/country/repositories/country.repository';
5 |
6 | @Module({
7 | imports: [],
8 | exports: [CountryService, CountryUtil, CountryRepository],
9 | providers: [CountryService, CountryUtil, CountryRepository],
10 | controllers: [],
11 | })
12 | export class CountryModule {}
13 |
--------------------------------------------------------------------------------
/src/modules/hello/docs/hello.public.doc.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import { Doc, DocResponse } from '@common/doc/decorators/doc.decorator';
3 | import { HelloResponseDto } from '@modules/hello/dtos/response/hello.response.dto';
4 |
5 | export function HelloPublicDoc(): MethodDecorator {
6 | return applyDecorators(
7 | Doc({
8 | summary: 'hello test api',
9 | }),
10 | DocResponse('app.hello', {
11 | dto: HelloResponseDto,
12 | })
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/common/pagination/constants/pagination.constant.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Default number of items per page for pagination.
3 | */
4 | export const PaginationDefaultPerPage = 20;
5 |
6 | /**
7 | * Maximum allowed number of items per page for pagination.
8 | */
9 | export const PaginationDefaultMaxPerPage = 100;
10 |
11 | /**
12 | * Maximum allowed page number for pagination.
13 | */
14 | export const PaginationDefaultMaxPage = 20;
15 |
16 | /**
17 | * Default field used for cursor-based pagination.
18 | */
19 | export const PaginationDefaultCursorField = 'id';
20 |
--------------------------------------------------------------------------------
/src/common/aws/aws.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AwsS3Service } from '@common/aws/services/aws.s3.service';
3 | import { AwsSESService } from '@common/aws/services/aws.ses.service';
4 |
5 | /**
6 | * AWS module that provides S3 and SES services.
7 | * Exports AWS services for use in other modules throughout the application.
8 | */
9 | @Module({
10 | exports: [AwsS3Service, AwsSESService],
11 | providers: [AwsS3Service, AwsSESService],
12 | imports: [],
13 | controllers: [],
14 | })
15 | export class AwsModule {}
16 |
--------------------------------------------------------------------------------
/src/modules/activity-log/utils/activity-log.util.ts:
--------------------------------------------------------------------------------
1 | import { ActivityLogResponseDto } from '@modules/activity-log/dtos/response/activity-log.response.dto';
2 | import { IActivityLog } from '@modules/activity-log/interfaces/activity-log.interface';
3 | import { Injectable } from '@nestjs/common';
4 | import { plainToInstance } from 'class-transformer';
5 |
6 | @Injectable()
7 | export class ActivityLogUtil {
8 | mapList(activityLogs: IActivityLog[]): ActivityLogResponseDto[] {
9 | return plainToInstance(ActivityLogResponseDto, activityLogs);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.login-enable-two-factor.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { UserLoginVerifyTwoFactorRequestDto } from '@modules/user/dtos/request/user.login-verify-two-factor.request.dto';
2 | import { UserTwoFactorEnableRequestDto } from '@modules/user/dtos/request/user.two-factor-enable.request.dto';
3 | import { IntersectionType, PickType } from '@nestjs/swagger';
4 |
5 | export class UserLoginEnableTwoFactorRequestDto extends IntersectionType(
6 | UserTwoFactorEnableRequestDto,
7 | PickType(UserLoginVerifyTwoFactorRequestDto, ['challengeToken'] as const)
8 | ) {}
9 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.two-factor-enable.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsTwoFactorCode } from '@modules/auth/validations/auth.two-factor-code.validation';
2 | import { ApiProperty } from '@nestjs/swagger';
3 | import { IsNotEmpty, IsString } from 'class-validator';
4 |
5 | export class UserTwoFactorEnableRequestDto {
6 | @ApiProperty({
7 | description: `digit code from authenticator app`,
8 | example: '654321',
9 | required: true,
10 | })
11 | @IsString()
12 | @IsNotEmpty()
13 | @IsTwoFactorCode()
14 | code: string;
15 | }
16 |
--------------------------------------------------------------------------------
/src/common/pagination/pagination.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { PaginationService } from '@common/pagination/services/pagination.service';
3 |
4 | /**
5 | * Global pagination module that provides pagination services across the application.
6 | * Configures and exports pagination functionality for handling paginated data requests.
7 | */
8 | @Global()
9 | @Module({
10 | providers: [PaginationService],
11 | exports: [PaginationService],
12 | imports: [],
13 | controllers: [],
14 | })
15 | export class PaginationModule {}
16 |
--------------------------------------------------------------------------------
/src/modules/email/email.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { EmailTemplateService } from '@modules/email/services/email.template.service';
3 | import { AwsModule } from '@common/aws/aws.module';
4 | import { EmailUtil } from '@modules/email/utils/email.util';
5 | import { EmailService } from '@modules/email/services/email.service';
6 |
7 | @Module({
8 | imports: [AwsModule],
9 | providers: [EmailUtil, EmailTemplateService, EmailService],
10 | exports: [EmailUtil, EmailTemplateService, EmailService],
11 | })
12 | export class EmailModule {}
13 |
--------------------------------------------------------------------------------
/src/migration.ts:
--------------------------------------------------------------------------------
1 | import { CommandFactory } from 'nest-commander';
2 | import { MigrationModule } from '@migration/migration.module';
3 | import { Logger as LoggerPino } from 'nestjs-pino';
4 |
5 | async function bootstrap(): Promise {
6 | const app = await CommandFactory.createWithoutRunning(MigrationModule, {
7 | abortOnError: true,
8 | bufferLogs: false,
9 | });
10 |
11 | app.useLogger(app.get(LoggerPino));
12 |
13 | await CommandFactory.runApplication(app);
14 |
15 | await app.close();
16 | process.exit(0);
17 | }
18 |
19 | bootstrap();
20 |
--------------------------------------------------------------------------------
/src/modules/auth/enums/auth.status-code.enum.ts:
--------------------------------------------------------------------------------
1 | export enum EnumAuthStatusCodeError {
2 | jwtAccessTokenInvalid = 5120,
3 | jwtRefreshTokenInvalid = 5121,
4 | socialGoogleRequired = 5122,
5 | socialGoogleInvalid = 5123,
6 | socialAppleRequired = 5124,
7 | socialAppleInvalid = 5125,
8 | twoFactorInvalid = 5126,
9 | twoFactorChallengeInvalid = 5127,
10 | twoFactorNotEnabled = 5128,
11 | twoFactorAlreadyEnabled = 5129,
12 | twoFactorRequiredSetup = 5132,
13 | twoFactorNotRequiredSetup = 5133,
14 | twoFactorAttemptTemporaryLock = 5134,
15 | }
16 |
--------------------------------------------------------------------------------
/src/router/routes/routes.user.module.ts:
--------------------------------------------------------------------------------
1 | import { UserUserController } from '@modules/user/controllers/user.user.controller';
2 | import { UserModule } from '@modules/user/user.module';
3 | import { Module } from '@nestjs/common';
4 |
5 | /**
6 | * User routes module that provides user-specific endpoints.
7 | * Contains controllers for user operations that require user-level authentication and authorization.
8 | */
9 | @Module({
10 | controllers: [UserUserController],
11 | providers: [],
12 | exports: [],
13 | imports: [UserModule],
14 | })
15 | export class RoutesUserModule {}
16 |
--------------------------------------------------------------------------------
/src/modules/country/utils/country.util.ts:
--------------------------------------------------------------------------------
1 | import { CountryResponseDto } from '@modules/country/dtos/response/country.response.dto';
2 | import { Injectable } from '@nestjs/common';
3 | import { Country } from '@prisma/client';
4 | import { plainToInstance } from 'class-transformer';
5 |
6 | @Injectable()
7 | export class CountryUtil {
8 | mapList(countries: Country[]): CountryResponseDto[] {
9 | return plainToInstance(CountryResponseDto, countries);
10 | }
11 |
12 | mapOne(country: Country): CountryResponseDto {
13 | return plainToInstance(CountryResponseDto, country);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/modules/term-policy/dtos/request/term-policy.accept.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { EnumTermPolicyType } from '@prisma/client';
3 | import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
4 |
5 | export class TermPolicyAcceptRequestDto {
6 | @ApiProperty({
7 | description: 'Type of the terms policy',
8 | example: EnumTermPolicyType.privacy,
9 | enum: EnumTermPolicyType,
10 | required: true,
11 | })
12 | @IsString()
13 | @IsEnum(EnumTermPolicyType)
14 | @IsNotEmpty()
15 | readonly type: EnumTermPolicyType;
16 | }
17 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.import.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { AwsS3PresignRequestDto } from '@common/aws/dtos/request/aws.s3-presign.request.dto';
2 | import { ApiProperty, PickType } from '@nestjs/swagger';
3 | import { IsNotEmpty, IsString } from 'class-validator';
4 |
5 | export class UserImportRequestDto extends PickType(AwsS3PresignRequestDto, [
6 | 'size',
7 | ]) {
8 | @ApiProperty({
9 | required: true,
10 | description: 'import path key',
11 | example: 'user/import/unique-import-key.csv',
12 | })
13 | @IsString()
14 | @IsNotEmpty()
15 | importKey: string;
16 | }
17 |
--------------------------------------------------------------------------------
/src/modules/role/dtos/response/role.list.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { RoleDto } from '@modules/role/dtos/role.dto';
2 | import { ApiHideProperty, ApiProperty, OmitType } from '@nestjs/swagger';
3 | import { Exclude, Transform } from 'class-transformer';
4 |
5 | export class RoleListResponseDto extends OmitType(RoleDto, [
6 | 'abilities',
7 | ] as const) {
8 | @ApiHideProperty()
9 | @Exclude()
10 | description?: string;
11 |
12 | @ApiProperty({
13 | description: 'count of abilities',
14 | required: true,
15 | })
16 | @Transform(({ value }) => value.length)
17 | abilities: number;
18 | }
19 |
--------------------------------------------------------------------------------
/src/modules/activity-log/activity-log.module.ts:
--------------------------------------------------------------------------------
1 | import { ActivityLogRepository } from '@modules/activity-log/repositories/activity-log.repository';
2 | import { ActivityLogService } from '@modules/activity-log/services/activity-log.service';
3 | import { ActivityLogUtil } from '@modules/activity-log/utils/activity-log.util';
4 | import { Module } from '@nestjs/common';
5 |
6 | @Module({
7 | controllers: [],
8 | providers: [ActivityLogService, ActivityLogUtil, ActivityLogRepository],
9 | exports: [ActivityLogService, ActivityLogUtil, ActivityLogRepository],
10 | imports: [],
11 | })
12 | export class ActivityLogModule {}
13 |
--------------------------------------------------------------------------------
/src/modules/feature-flag/feature-flag.module.ts:
--------------------------------------------------------------------------------
1 | import { FeatureFlagRepository } from '@modules/feature-flag/repositories/feature-flag.repository';
2 | import { FeatureFlagService } from '@modules/feature-flag/services/feature-flag.service';
3 | import { FeatureFlagUtil } from '@modules/feature-flag/utils/feature-flag.util';
4 | import { Global, Module } from '@nestjs/common';
5 |
6 | @Global()
7 | @Module({
8 | imports: [],
9 | exports: [FeatureFlagService, FeatureFlagRepository, FeatureFlagUtil],
10 | providers: [FeatureFlagService, FeatureFlagRepository, FeatureFlagUtil],
11 | })
12 | export class FeatureFlagModule {}
13 |
--------------------------------------------------------------------------------
/src/modules/user/docs/user.user.doc.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Doc,
3 | DocAuth,
4 | DocGuard,
5 | DocResponse,
6 | } from '@common/doc/decorators/doc.decorator';
7 | import { applyDecorators } from '@nestjs/common';
8 |
9 | export function UserUserDeleteSelfDoc(): MethodDecorator {
10 | return applyDecorators(
11 | Doc({
12 | summary: 'user delete their account',
13 | }),
14 | DocAuth({
15 | xApiKey: true,
16 | jwtAccessToken: true,
17 | }),
18 | DocGuard({ role: true, termPolicy: true }),
19 | DocResponse('user.delete')
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/modules/password-history/utils/password-history.util.ts:
--------------------------------------------------------------------------------
1 | import { PasswordHistoryResponseDto } from '@modules/password-history/dtos/response/password-history.response.dto';
2 | import { IPasswordHistory } from '@modules/password-history/interfaces/password-history.interface';
3 | import { Injectable } from '@nestjs/common';
4 | import { plainToInstance } from 'class-transformer';
5 |
6 | @Injectable()
7 | export class PasswordHistoryUtil {
8 | mapList(
9 | passwordHistories: IPasswordHistory[]
10 | ): PasswordHistoryResponseDto[] {
11 | return plainToInstance(PasswordHistoryResponseDto, passwordHistories);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/languages/en/role.json:
--------------------------------------------------------------------------------
1 | {
2 | "list": "Roles retrieved successfully.",
3 | "get": "Role details fetched successfully.",
4 | "create": "New role created successfully.",
5 | "update": "Role updated successfully.",
6 | "delete": "Role deleted successfully.",
7 | "error": {
8 | "notFound": "Sorry, we couldn't find the requested role.",
9 | "exist": "A role with this name already exists.",
10 | "used": "This role is currently in use and cannot be deleted.",
11 | "forbidden": "Sorry, your role doesn't grant access to this resource.",
12 | "predefinedNotFound": "Predefined roles not setted."
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/common/file/interfaces/file.service.interface.ts:
--------------------------------------------------------------------------------
1 | import { IFileRandomFilenameOptions } from '@common/file/interfaces/file.interface';
2 |
3 | export interface IFileService {
4 | writeCsv>(rows: T[]): string;
5 | readCsv>(file: string): T[];
6 | createRandomFilename({
7 | path,
8 | prefix,
9 | extension,
10 | randomLength,
11 | }: IFileRandomFilenameOptions): string;
12 | extractExtensionFromFilename(filename: string): string;
13 | extractMimeFromFilename(filename: string): string;
14 | extractFilenameFromPath(filePath: string): string;
15 | }
16 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "node",
6 | "request": "launch",
7 | "name": "Debug Nest Framework",
8 | "args": ["${workspaceFolder}/src/main.ts"],
9 | "runtimeArgs": [
10 | "--nolazy",
11 | "-r",
12 | "ts-node/register",
13 | "-r",
14 | "tsconfig-paths/register"
15 | ],
16 | "sourceMaps": true,
17 | "envFile": "${workspaceFolder}/.env",
18 | "cwd": "${workspaceRoot}",
19 | "console": "integratedTerminal"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/src/modules/role/dtos/role.ability.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import {
3 | EnumPolicyAction,
4 | EnumPolicySubject,
5 | } from '@modules/policy/enums/policy.enum';
6 |
7 | export class RoleAbilityDto {
8 | @ApiProperty({
9 | required: true,
10 | description: 'Ability subject',
11 | enum: EnumPolicySubject,
12 | })
13 | subject: EnumPolicySubject;
14 |
15 | @ApiProperty({
16 | required: true,
17 | description: 'Ability action base on subject',
18 | isArray: true,
19 | default: [EnumPolicyAction.manage],
20 | enum: EnumPolicyAction,
21 | })
22 | action: EnumPolicyAction[];
23 | }
24 |
--------------------------------------------------------------------------------
/src/common/pagination/interfaces/pagination.service.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IPaginationCursorReturn,
3 | IPaginationOffsetReturn,
4 | IPaginationQueryCursorParams,
5 | IPaginationQueryOffsetParams,
6 | IPaginationRepository,
7 | } from '@common/pagination/interfaces/pagination.interface';
8 |
9 | export interface IPaginationService {
10 | offset(
11 | repository: IPaginationRepository,
12 | args: IPaginationQueryOffsetParams
13 | ): Promise>;
14 | cursor(
15 | repository: IPaginationRepository,
16 | args: IPaginationQueryCursorParams
17 | ): Promise>;
18 | }
19 |
--------------------------------------------------------------------------------
/src/languages/en/featureFlag.json:
--------------------------------------------------------------------------------
1 | {
2 | "list": "Feature flags retrieved successfully.",
3 | "updateMetadata": "Feature flag metadata updated successfully.",
4 | "updateStatus": "Feature flag status updated successfully.",
5 | "error": {
6 | "notFound": "Feature flag not found.",
7 | "invalidMetadata": "Feature flag metadata is invalid.",
8 | "predefinedKeyEmpty": "Predefined key cannot be empty.",
9 | "predefinedKeyLengthExceeded": "Predefined key length exceeded the maximum allowed.",
10 | "predefinedKeyTypeInvalid": "Predefined key type is invalid.",
11 | "serviceUnavailable": "Feature flag service is currently unavailable."
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/modules/feature-flag/dtos/response/feature-flag.response.ts:
--------------------------------------------------------------------------------
1 | import { DatabaseDto } from '@common/database/dtos/database.dto';
2 | import { IFeatureFlagMetadata } from '@modules/feature-flag/interfaces/feature-flag.interface';
3 | import { ApiProperty } from '@nestjs/swagger';
4 |
5 | export class FeatureFlagResponseDto extends DatabaseDto {
6 | @ApiProperty({
7 | description: 'Feature flag key',
8 | })
9 | key: string;
10 |
11 | @ApiProperty({
12 | description: 'Feature flag status',
13 | })
14 | isEnable: boolean;
15 |
16 | @ApiProperty({
17 | description: 'Feature flag metadata in JSON format',
18 | })
19 | metadata: IFeatureFlagMetadata;
20 | }
21 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/response/user.two-factor-setup.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 |
3 | export class UserTwoFactorSetupResponseDto {
4 | @ApiProperty({
5 | required: true,
6 | description: 'Base32 encoded secret to be stored in authenticator apps',
7 | example: 'JBSWY3DPEHPK3PXP',
8 | })
9 | secret: string;
10 |
11 | @ApiProperty({
12 | required: true,
13 | description:
14 | 'otpauth URL compatible with Google Authenticator and similar apps',
15 | example:
16 | 'otpauth://totp/ACK%20Auth:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=ACK',
17 | })
18 | otpauthUrl: string;
19 | }
20 |
--------------------------------------------------------------------------------
/src/configs/forgot-password.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IConfigForgotPassword {
4 | expiredInMinutes: number;
5 | tokenLength: number;
6 | linkBaseUrl: string;
7 | resendInMinutes: number;
8 | reference: {
9 | prefix: string;
10 | length: number;
11 | };
12 | }
13 |
14 | export default registerAs(
15 | 'forgotPassword',
16 | (): IConfigForgotPassword => ({
17 | expiredInMinutes: 5,
18 | tokenLength: 100,
19 | linkBaseUrl: 'forgot-password',
20 | resendInMinutes: 2,
21 | reference: {
22 | prefix: 'FG',
23 | length: 25,
24 | },
25 | })
26 | );
27 |
--------------------------------------------------------------------------------
/src/modules/feature-flag/constants/feature-flag.doc.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiParamOptions, ApiQueryOptions } from '@nestjs/swagger';
3 |
4 | export const FeatureFlagDocParamsId: ApiParamOptions[] = [
5 | {
6 | name: 'featureFlagId',
7 | allowEmptyValue: false,
8 | required: true,
9 | type: 'string',
10 | example: faker.database.mongodbObjectId(),
11 | },
12 | ];
13 |
14 | export const FeatureFlagDocQueryList: ApiQueryOptions[] = [
15 | {
16 | name: 'key',
17 | allowEmptyValue: true,
18 | required: false,
19 | type: 'string',
20 | description: 'Filter by feature flag key',
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/src/modules/term-policy/dtos/request/term-policy.create.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { TermPolicyAcceptRequestDto } from '@modules/term-policy/dtos/request/term-policy.accept.request.dto';
2 | import { TermPolicyContentPresignRequestDto } from '@modules/term-policy/dtos/request/term-policy.content-presign.request.dto';
3 | import { TermPolicyContentsRequestDto } from '@modules/term-policy/dtos/request/term-policy.content.request.dto';
4 | import { IntersectionType, PickType } from '@nestjs/swagger';
5 |
6 | export class TermPolicyCreateRequestDto extends IntersectionType(
7 | TermPolicyAcceptRequestDto,
8 | TermPolicyContentsRequestDto,
9 | PickType(TermPolicyContentPresignRequestDto, ['version'] as const)
10 | ) {}
11 |
--------------------------------------------------------------------------------
/src/common/database/database.module.ts:
--------------------------------------------------------------------------------
1 | import { DatabaseService } from '@common/database/services/database.service';
2 | import { DatabaseUtil } from '@common/database/utils/database.util';
3 | import { DynamicModule, Global, Module } from '@nestjs/common';
4 |
5 | /**
6 | * Global database module that provides database services throughout the application.
7 | */
8 | @Global()
9 | @Module({})
10 | export class DatabaseModule {
11 | static forRoot(): DynamicModule {
12 | return {
13 | module: DatabaseModule,
14 | providers: [DatabaseService, DatabaseUtil],
15 | exports: [DatabaseService, DatabaseUtil],
16 | imports: [],
17 | controllers: [],
18 | };
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/configs/verification.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IConfigVerification {
4 | expiredInMinutes: number;
5 | otpLength: number;
6 | tokenLength: number;
7 | linkBaseUrl: string;
8 | resendInMinutes: number;
9 | reference: {
10 | prefix: string;
11 | length: number;
12 | };
13 | }
14 |
15 | export default registerAs(
16 | 'verification',
17 | (): IConfigVerification => ({
18 | expiredInMinutes: 5,
19 | otpLength: 6,
20 | tokenLength: 100,
21 | linkBaseUrl: 'verify-email',
22 | resendInMinutes: 2,
23 | reference: {
24 | prefix: 'VER',
25 | length: 25,
26 | },
27 | })
28 | );
29 |
--------------------------------------------------------------------------------
/src/configs/redis.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export interface IConfigRedis {
4 | cache: {
5 | url: string;
6 | namespace: string;
7 | ttlInMs: number;
8 | };
9 | queue: {
10 | url: string;
11 | namespace: string;
12 | };
13 | }
14 |
15 | export default registerAs(
16 | 'redis',
17 | (): IConfigRedis => ({
18 | cache: {
19 | url: process.env.CACHE_REDIS_URL ?? 'redis://localhost:6379',
20 | namespace: 'Cache',
21 | ttlInMs: 5 * 60 * 1000,
22 | },
23 | queue: {
24 | url: process.env.QUEUE_REDIS_URL ?? 'redis://localhost:6379',
25 | namespace: 'Queue',
26 | },
27 | })
28 | );
29 |
--------------------------------------------------------------------------------
/src/modules/email/dtos/email.temp-password.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty } from '@nestjs/swagger';
3 |
4 | export class EmailTempPasswordDto {
5 | @ApiProperty({
6 | required: true,
7 | example: faker.string.alphanumeric(10),
8 | description: 'Expired at by date',
9 | })
10 | password: string;
11 |
12 | @ApiProperty({
13 | required: true,
14 | example: faker.date.future(),
15 | description: 'Expired at by date',
16 | })
17 | passwordExpiredAt: Date;
18 |
19 | @ApiProperty({
20 | required: true,
21 | example: faker.date.recent(),
22 | description: 'Password created at by date',
23 | })
24 | passwordCreatedAt: Date;
25 | }
26 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.claim-username.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { Transform } from 'class-transformer';
3 | import {
4 | IsAlphanumeric,
5 | IsNotEmpty,
6 | IsString,
7 | MaxLength,
8 | MinLength,
9 | } from 'class-validator';
10 |
11 | export class UserClaimUsernameRequestDto {
12 | @IsNotEmpty()
13 | @IsString()
14 | @IsAlphanumeric()
15 | @MaxLength(50)
16 | @MinLength(3)
17 | @Transform(({ value }) => value.toLowerCase().trim())
18 | @ApiProperty({
19 | required: true,
20 | description: 'username to claim',
21 | example: 'john_doe123',
22 | maxLength: 50,
23 | minLength: 3,
24 | })
25 | username: Lowercase;
26 | }
27 |
--------------------------------------------------------------------------------
/src/migration/data/migration.country.data.ts:
--------------------------------------------------------------------------------
1 | import { EnumAppEnvironment } from '@app/enums/app.enum';
2 | import { CountryRequestDto } from '@modules/country/dtos/request/country.request.dto';
3 |
4 | const countryData = [
5 | {
6 | name: 'Indonesia',
7 | alpha2Code: 'ID',
8 | alpha3Code: 'IDN',
9 | phoneCode: ['62'],
10 | continent: 'Asia',
11 | timezone: 'Asia/Jakarta',
12 | },
13 | ];
14 |
15 | export const migrationCountryData: Record<
16 | EnumAppEnvironment,
17 | CountryRequestDto[]
18 | > = {
19 | [EnumAppEnvironment.local]: countryData,
20 | [EnumAppEnvironment.development]: countryData,
21 | [EnumAppEnvironment.staging]: countryData,
22 | [EnumAppEnvironment.production]: countryData,
23 | };
24 |
--------------------------------------------------------------------------------
/src/modules/feature-flag/dtos/request/feature-flag.update-status.request.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import {
3 | IsBoolean,
4 | IsInt,
5 | IsNotEmpty,
6 | IsNumber,
7 | Max,
8 | Min,
9 | } from 'class-validator';
10 |
11 | export class FeatureFlagUpdateStatusRequestDto {
12 | @ApiProperty({
13 | description: 'Status of the feature flag',
14 | example: true,
15 | })
16 | @IsBoolean()
17 | @IsNotEmpty()
18 | isEnable: boolean;
19 |
20 | @ApiProperty({
21 | description: 'Feature flag rollout percentage (0-100)',
22 | example: 50,
23 | })
24 | @IsNotEmpty()
25 | @IsNumber()
26 | @IsInt()
27 | @Min(0)
28 | @Max(100)
29 | rolloutPercent: number;
30 | }
31 |
--------------------------------------------------------------------------------
/src/common/response/dtos/response.error.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IMessageValidationError,
3 | IMessageValidationImportError,
4 | } from '@common/message/interfaces/message.interface';
5 | import { ResponseDto } from '@common/response/dtos/response.dto';
6 | import { ApiProperty } from '@nestjs/swagger';
7 |
8 | /**
9 | * Response DTO for error responses with validation error details.
10 | * Extends standard response structure to include specific validation error information.
11 | */
12 | export class ResponseErrorDto extends ResponseDto {
13 | @ApiProperty({
14 | type: 'array',
15 | description: 'List of validation errors',
16 | required: false,
17 | })
18 | errors?: IMessageValidationError[] | IMessageValidationImportError[];
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.generate-photo-profile.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty, PickType } from '@nestjs/swagger';
2 | import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
3 | import { EnumFileExtensionImage } from '@common/file/enums/file.enum';
4 | import { AwsS3PresignRequestDto } from '@common/aws/dtos/request/aws.s3-presign.request.dto';
5 |
6 | export class UserGeneratePhotoProfileRequestDto extends PickType(
7 | AwsS3PresignRequestDto,
8 | ['size']
9 | ) {
10 | @ApiProperty({
11 | type: 'string',
12 | enum: EnumFileExtensionImage,
13 | default: EnumFileExtensionImage.jpg,
14 | })
15 | @IsString()
16 | @IsEnum(EnumFileExtensionImage)
17 | @IsNotEmpty()
18 | extension: EnumFileExtensionImage;
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/email/dtos/email.send.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty } from '@nestjs/swagger';
3 |
4 | export class EmailSendDto {
5 | @ApiProperty({ required: true, example: faker.internet.username() })
6 | username: string;
7 |
8 | @ApiProperty({
9 | required: true,
10 | example: faker.internet.email(),
11 | })
12 | email: string;
13 |
14 | @ApiProperty({
15 | required: false,
16 | isArray: true,
17 | example: [faker.internet.email(), faker.internet.email()],
18 | })
19 | cc?: string[];
20 |
21 | @ApiProperty({
22 | required: false,
23 | isArray: true,
24 | example: [faker.internet.email(), faker.internet.email()],
25 | })
26 | bcc?: string[];
27 | }
28 |
--------------------------------------------------------------------------------
/src/modules/email/dtos/email.verification.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty } from '@nestjs/swagger';
3 |
4 | export class EmailVerificationDto {
5 | @ApiProperty({
6 | required: true,
7 | example: 'https://example.com/verify?token=abcdefg12345',
8 | })
9 | link: string;
10 |
11 | @ApiProperty({
12 | required: true,
13 | example: faker.date.future(),
14 | description: 'Expired at by date',
15 | })
16 | expiredAt: Date;
17 |
18 | @ApiProperty({
19 | required: true,
20 | example: 15,
21 | description: 'Expired in minutes',
22 | })
23 | expiredInMinutes: number;
24 |
25 | @ApiProperty({
26 | required: true,
27 | })
28 | reference: string;
29 | }
30 |
--------------------------------------------------------------------------------
/src/common/file/interfaces/file.interface.ts:
--------------------------------------------------------------------------------
1 | import { EnumFileExtension } from '@common/file/enums/file.enum';
2 |
3 | export type IFile = Express.Multer.File;
4 |
5 | export interface IFileUploadSingle {
6 | field: string;
7 | fileSize: number;
8 | }
9 |
10 | export interface IFileUploadMultiple extends IFileUploadSingle {
11 | maxFiles: number;
12 | }
13 |
14 | export type IFileUploadMultipleField = Omit;
15 |
16 | export type IFileUploadMultipleFieldOptions = Pick<
17 | IFileUploadSingle,
18 | 'fileSize'
19 | >;
20 |
21 | export type IFileInput = IFile | IFile[];
22 |
23 | export interface IFileRandomFilenameOptions {
24 | path?: string;
25 | prefix?: string;
26 | extension: EnumFileExtension;
27 | randomLength?: number;
28 | }
29 |
--------------------------------------------------------------------------------
/src/common/request/exceptions/request.validation.exception.ts:
--------------------------------------------------------------------------------
1 | import { HttpStatus } from '@nestjs/common';
2 | import { ValidationError } from 'class-validator';
3 | import { EnumRequestStatusCodeError } from '@common/request/enums/request.status-code.enum';
4 |
5 | /**
6 | * Custom exception class for request validation errors.
7 | * Thrown when request data fails validation rules.
8 | */
9 | export class RequestValidationException extends Error {
10 | readonly httpStatus: HttpStatus = HttpStatus.UNPROCESSABLE_ENTITY;
11 | readonly statusCode: number = EnumRequestStatusCodeError.validation;
12 | readonly errors: ValidationError[];
13 |
14 | constructor(errors: ValidationError[]) {
15 | super('request.error.validation');
16 |
17 | this.errors = errors;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/password-history/password-history.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PasswordHistoryService } from '@modules/password-history/services/password-history.service';
3 | import { PasswordHistoryRepository } from '@modules/password-history/repositories/password-history.repository';
4 | import { PasswordHistoryUtil } from '@modules/password-history/utils/password-history.util';
5 |
6 | @Module({
7 | imports: [],
8 | exports: [
9 | PasswordHistoryService,
10 | PasswordHistoryUtil,
11 | PasswordHistoryRepository,
12 | ],
13 | providers: [
14 | PasswordHistoryService,
15 | PasswordHistoryUtil,
16 | PasswordHistoryRepository,
17 | ],
18 | controllers: [],
19 | })
20 | export class PasswordHistoryModule {}
21 |
--------------------------------------------------------------------------------
/src/router/routes/routes.system.module.ts:
--------------------------------------------------------------------------------
1 | import { HealthSystemController } from '@modules/health/controllers/health.system.controller';
2 | import { HealthModule } from '@modules/health/health.module';
3 | import { UserSystemController } from '@modules/user/controllers/user.system.controller';
4 | import { UserModule } from '@modules/user/user.module';
5 | import { Module } from '@nestjs/common';
6 |
7 | /**
8 | * System routes module that provides system-level endpoints.
9 | * Contains controllers for user system operations and health checks for monitoring application status.
10 | */
11 | @Module({
12 | controllers: [UserSystemController, HealthSystemController],
13 | providers: [],
14 | exports: [],
15 | imports: [UserModule, HealthModule],
16 | })
17 | export class RoutesSystemModule {}
18 |
--------------------------------------------------------------------------------
/src/modules/role/decorators/role.decorator.ts:
--------------------------------------------------------------------------------
1 | import { RoleRequiredMetaKey } from '@modules/role/constants/role.constant';
2 | import { RoleGuard } from '@modules/role/guards/role.guard';
3 | import { SetMetadata, UseGuards, applyDecorators } from '@nestjs/common';
4 | import { EnumRoleType } from '@prisma/client';
5 |
6 | /**
7 | * Method decorator that applies role-based protection guards
8 | * @param {...RoleRequiredMetaKey[]} requiredRoles - List of role types required for access
9 | * @returns {MethodDecorator} Combined decorators for role validation
10 | */
11 | export function RoleProtected(
12 | ...requiredRoles: EnumRoleType[]
13 | ): MethodDecorator {
14 | return applyDecorators(
15 | UseGuards(RoleGuard),
16 | SetMetadata(RoleRequiredMetaKey, requiredRoles)
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/modules/activity-log/interfaces/activity-log.service.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IPaginationQueryCursorParams,
3 | IPaginationQueryOffsetParams,
4 | } from '@common/pagination/interfaces/pagination.interface';
5 | import { IResponsePagingReturn } from '@common/response/interfaces/response.interface';
6 | import { ActivityLogResponseDto } from '@modules/activity-log/dtos/response/activity-log.response.dto';
7 |
8 | export interface IActivityLogService {
9 | getListOffsetByUser(
10 | userId: string,
11 | pagination: IPaginationQueryOffsetParams
12 | ): Promise>;
13 | getListCursorByUser(
14 | userId: string,
15 | pagination: IPaginationQueryCursorParams
16 | ): Promise>;
17 | }
18 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'github-actions'
4 | directory: '/'
5 | schedule:
6 | interval: 'monthly'
7 | day: tuesday
8 | time: '00:00'
9 | open-pull-requests-limit: 3
10 | target-branch: 'development'
11 | commit-message:
12 | prefix: 'github-action'
13 | labels:
14 | - dependabot
15 |
16 | - package-ecosystem: 'npm'
17 | directory: '/'
18 | schedule:
19 | interval: 'monthly'
20 | day: monday
21 | time: '00:00'
22 | open-pull-requests-limit: 3
23 | target-branch: 'development'
24 | commit-message:
25 | prefix: 'deps'
26 | labels:
27 | - dependabot
28 | - dependencies
29 | versioning-strategy: increase
30 |
--------------------------------------------------------------------------------
/src/queues/decorators/queue.decorator.ts:
--------------------------------------------------------------------------------
1 | import { Processor } from '@nestjs/bullmq';
2 | import { NestWorkerOptions } from '@nestjs/bullmq/dist/interfaces/worker-options.interface';
3 | import { QueueProcessorConfigKey } from 'src/queues/constants/queue.constant';
4 | import { EnumQueue } from 'src/queues/enums/queue.enum';
5 |
6 | export function QueueProcessor(
7 | name: EnumQueue,
8 | options?: Omit
9 | ): ClassDecorator {
10 | // @note: currently there is no way to inject ConfigService into decorators
11 | return Processor(
12 | {
13 | name,
14 | configKey: QueueProcessorConfigKey,
15 | },
16 | {
17 | name: `${process.env.APP_NAME}-${process.env.APP_ENV}:${name}:consumer`,
18 | ...options,
19 | }
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/common/message/interfaces/message.interface.ts:
--------------------------------------------------------------------------------
1 | import { ValidationError } from '@nestjs/common';
2 |
3 | export type IMessageProperties = Record;
4 |
5 | export interface IMessageErrorOptions {
6 | readonly customLanguage?: string;
7 | }
8 |
9 | export interface IMessageSetOptions extends IMessageErrorOptions {
10 | readonly properties?: IMessageProperties;
11 | }
12 |
13 | export interface IMessageValidationError {
14 | key: string;
15 | property: string;
16 | message: string;
17 | }
18 |
19 | export interface IMessageValidationImportErrorParam {
20 | row: number;
21 | errors: ValidationError[];
22 | }
23 |
24 | export interface IMessageValidationImportError extends Omit<
25 | IMessageValidationImportErrorParam,
26 | 'errors'
27 | > {
28 | errors: IMessageValidationError[];
29 | }
30 |
--------------------------------------------------------------------------------
/src/modules/password-history/interfaces/password-history.service.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IPaginationQueryCursorParams,
3 | IPaginationQueryOffsetParams,
4 | } from '@common/pagination/interfaces/pagination.interface';
5 | import { IResponsePagingReturn } from '@common/response/interfaces/response.interface';
6 | import { PasswordHistoryResponseDto } from '@modules/password-history/dtos/response/password-history.response.dto';
7 |
8 | export interface IPasswordHistoryService {
9 | getListOffsetByUser(
10 | userId: string,
11 | pagination: IPaginationQueryOffsetParams
12 | ): Promise>;
13 | getListCursorByUser(
14 | userId: string,
15 | pagination: IPaginationQueryCursorParams
16 | ): Promise>;
17 | }
18 |
--------------------------------------------------------------------------------
/src/common/request/exceptions/request.too-many.exception.ts:
--------------------------------------------------------------------------------
1 | import { IAppException } from '@app/interfaces/app.interface';
2 | import { IMessageValidationError } from '@common/message/interfaces/message.interface';
3 | import { HttpException } from '@nestjs/common';
4 |
5 | export class RequestTooManyException extends HttpException {
6 | constructor({
7 | statusCode,
8 | message,
9 | messageProperties,
10 | errors,
11 | _error,
12 | }: IAppException) {
13 | super(
14 | {
15 | statusCode,
16 | message: message ?? 'request.error.tooManyRequests',
17 | messageProperties,
18 | errors: errors as IMessageValidationError[],
19 | _error,
20 | },
21 | 429
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/modules/country/docs/country.public.doc.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import {
3 | DocAuth,
4 | DocResponsePaging,
5 | } from '@common/doc/decorators/doc.decorator';
6 | import { Doc } from '@common/doc/decorators/doc.decorator';
7 | import { CountryResponseDto } from '@modules/country/dtos/response/country.response.dto';
8 | import { CountryDefaultAvailableSearch } from '@modules/country/constants/country.list.constant';
9 |
10 | export function CountryPublicListDoc(): MethodDecorator {
11 | return applyDecorators(
12 | Doc({ summary: 'get all list country' }),
13 | DocAuth({ xApiKey: true }),
14 | DocResponsePaging('country.list', {
15 | dto: CountryResponseDto,
16 | availableSearch: CountryDefaultAvailableSearch,
17 | })
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/feature-flag/decorators/feature-flag.decorator.ts:
--------------------------------------------------------------------------------
1 | import { FeatureFlagKeyPathMetaKey } from '@modules/feature-flag/constants/feature-flag.constant';
2 | import { FeatureFlagGuard } from '@modules/feature-flag/guards/feature-flag.guard';
3 | import { SetMetadata, UseGuards, applyDecorators } from '@nestjs/common';
4 |
5 | /**
6 | * Method decorator that applies feature flag protection to routes.
7 | * Validates if the specified feature flag is active before allowing access.
8 | *
9 | * @param {string} keyPath - The feature flag key path to check
10 | * @returns {MethodDecorator} Method decorator function
11 | */
12 | export function FeatureFlagProtected(keyPath: string): MethodDecorator {
13 | return applyDecorators(
14 | UseGuards(FeatureFlagGuard),
15 | SetMetadata(FeatureFlagKeyPathMetaKey, keyPath)
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/common/message/interfaces/message.service.interface.ts:
--------------------------------------------------------------------------------
1 | import { ValidationError } from '@nestjs/common';
2 | import {
3 | IMessageErrorOptions,
4 | IMessageSetOptions,
5 | IMessageValidationError,
6 | IMessageValidationImportError,
7 | IMessageValidationImportErrorParam,
8 | } from '@common/message/interfaces/message.interface';
9 |
10 | export interface IMessageService {
11 | filterLanguage(customLanguage: string): string;
12 | setMessage(path: string, options?: IMessageSetOptions): string;
13 | setValidationMessage(
14 | errors: ValidationError[],
15 | options?: IMessageErrorOptions
16 | ): IMessageValidationError[];
17 | setValidationImportMessage(
18 | errors: IMessageValidationImportErrorParam[],
19 | options?: IMessageErrorOptions
20 | ): IMessageValidationImportError[];
21 | }
22 |
--------------------------------------------------------------------------------
/src/modules/role/constants/role.doc.constant.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiParamOptions, ApiQueryOptions } from '@nestjs/swagger';
3 | import { EnumRoleType } from '@prisma/client';
4 |
5 | export const RoleDocParamsId: ApiParamOptions[] = [
6 | {
7 | name: 'roleId',
8 | allowEmptyValue: false,
9 | required: true,
10 | type: 'string',
11 | example: faker.database.mongodbObjectId(),
12 | },
13 | ];
14 |
15 | export const RoleDocQueryList: ApiQueryOptions[] = [
16 | {
17 | name: 'type',
18 | allowEmptyValue: true,
19 | required: false,
20 | type: 'string',
21 | example: Object.values(EnumRoleType).join(','),
22 | description: `enum value with ',' delimiter. Available values: ${Object.values(EnumRoleType).join(',')}`,
23 | },
24 | ];
25 |
--------------------------------------------------------------------------------
/src/modules/role/dtos/request/role.create.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty } from '@nestjs/swagger';
3 | import {
4 | IsAlphanumeric,
5 | IsNotEmpty,
6 | IsString,
7 | MaxLength,
8 | MinLength,
9 | } from 'class-validator';
10 | import { RoleUpdateRequestDto } from '@modules/role/dtos/request/role.update.request.dto';
11 | import { Transform } from 'class-transformer';
12 |
13 | export class RoleCreateRequestDto extends RoleUpdateRequestDto {
14 | @ApiProperty({
15 | description: 'Name of role',
16 | example: faker.person.jobTitle(),
17 | required: true,
18 | })
19 | @IsString()
20 | @IsNotEmpty()
21 | @IsAlphanumeric()
22 | @MinLength(3)
23 | @MaxLength(30)
24 | @Transform(({ value }) => value.toLowerCase().trim())
25 | name: Lowercase;
26 | }
27 |
--------------------------------------------------------------------------------
/src/common/request/middlewares/request.helmet.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NestMiddleware } from '@nestjs/common';
2 | import { NextFunction, Request, Response } from 'express';
3 | import helmet from 'helmet';
4 |
5 | /**
6 | * Security headers middleware powered by Helmet for enhanced application protection.
7 | * Applies comprehensive security headers to protect against common web vulnerabilities.
8 | */
9 | @Injectable()
10 | export class RequestHelmetMiddleware implements NestMiddleware {
11 | /**
12 | * Applies Helmet security middleware to set protective HTTP headers.
13 | *
14 | * @param req - The Express request object
15 | * @param res - The Express response object
16 | * @param next - The next middleware function
17 | */
18 | use(req: Request, res: Response, next: NextFunction): void {
19 | helmet()(req, res, next);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/modules/feature-flag/dtos/request/feature-flag.update-metadata.request.ts:
--------------------------------------------------------------------------------
1 | import { IFeatureFlagMetadata } from '@modules/feature-flag/interfaces/feature-flag.interface';
2 | import { IsFeatureFlagMetadata } from '@modules/feature-flag/validations/feature-flag.metadata.validation';
3 | import { ApiProperty } from '@nestjs/swagger';
4 | import { IsNotEmpty, IsObject } from 'class-validator';
5 |
6 | export class FeatureFlagUpdateMetadataRequestDto {
7 | @ApiProperty({
8 | description: 'Feature flag metadata in JSON format',
9 | example: {
10 | newFeature: true,
11 | betaUserAccess: false,
12 | maxRetries: 3,
13 | apiEndpoint: 'https://api.example.com',
14 | },
15 | required: false,
16 | })
17 | @IsNotEmpty()
18 | @IsObject()
19 | @IsFeatureFlagMetadata()
20 | metadata: IFeatureFlagMetadata;
21 | }
22 |
--------------------------------------------------------------------------------
/src/common/request/middlewares/request.response-time.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NestMiddleware } from '@nestjs/common';
2 | import { NextFunction, Request, Response } from 'express';
3 | import responseTime from 'response-time';
4 |
5 | /**
6 | * Response time measurement middleware for performance monitoring.
7 | * Automatically measures request processing time and adds 'X-Response-Time' header.
8 | */
9 | @Injectable()
10 | export class RequestResponseTimeMiddleware implements NestMiddleware {
11 | /**
12 | * Applies response time measurement to HTTP requests.
13 | *
14 | * @param req - The Express request object
15 | * @param res - The Express response object
16 | * @param next - The next middleware function
17 | */
18 | async use(req: Request, res: Response, next: NextFunction): Promise {
19 | responseTime()(req, res, next);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/common/file/exceptions/file.import.exception.ts:
--------------------------------------------------------------------------------
1 | import { HttpStatus } from '@nestjs/common';
2 | import { IMessageValidationImportErrorParam } from '@common/message/interfaces/message.interface';
3 | import { EnumRequestStatusCodeError } from '@common/request/enums/request.status-code.enum';
4 |
5 | /**
6 | * Custom exception for file import validation errors.
7 | * Extends Error to provide structured validation error information for file import operations.
8 | */
9 | export class FileImportException extends Error {
10 | readonly httpStatus: HttpStatus = HttpStatus.UNPROCESSABLE_ENTITY;
11 | readonly statusCode: number = EnumRequestStatusCodeError.validation;
12 | readonly errors: IMessageValidationImportErrorParam[];
13 |
14 | constructor(errors: IMessageValidationImportErrorParam[]) {
15 | super('file.error.validationDto');
16 |
17 | this.errors = errors;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/email/dtos/email.worker.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty, getSchemaPath } from '@nestjs/swagger';
2 | import { Type } from 'class-transformer';
3 | import {
4 | IsNotEmpty,
5 | IsNotEmptyObject,
6 | IsObject,
7 | IsOptional,
8 | ValidateNested,
9 | } from 'class-validator';
10 | import { EmailSendDto } from '@modules/email/dtos/email.send.dto';
11 |
12 | export class EmailWorkerDto {
13 | @ApiProperty({
14 | required: true,
15 | type: EmailSendDto,
16 | oneOf: [{ $ref: getSchemaPath(EmailSendDto) }],
17 | })
18 | @IsObject()
19 | @IsNotEmpty()
20 | @IsNotEmptyObject()
21 | @ValidateNested()
22 | @Type(() => EmailSendDto)
23 | send: EmailSendDto;
24 |
25 | @ApiProperty({
26 | required: false,
27 | })
28 | @IsObject()
29 | @IsOptional()
30 | @IsNotEmptyObject()
31 | @ValidateNested()
32 | data?: T;
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/linter.yml:
--------------------------------------------------------------------------------
1 | name: Linter
2 | on:
3 | workflow_dispatch:
4 | pull_request:
5 | branches:
6 | - main
7 | - staging
8 | - development
9 |
10 | env:
11 | NODE_VERSION: lts/*
12 |
13 | jobs:
14 | linter:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Git checkout
19 | uses: actions/checkout@v6
20 |
21 | - name: Setup node
22 | uses: actions/setup-node@v6
23 | with:
24 | node-version: ${{ env.NODE_VERSION }}
25 |
26 | - name: Setup pnpm
27 | uses: pnpm/action-setup@v4
28 |
29 | - name: Install dependencies
30 | run: pnpm install --frozen-lockfile
31 |
32 | - name: Generate Client
33 | run: pnpm db:generate
34 |
35 | - name: Linter
36 | run: pnpm lint
37 |
--------------------------------------------------------------------------------
/.commitlintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@commitlint/config-conventional"
4 | ],
5 | "rules": {
6 | "body-max-line-length": [
7 | 0,
8 | "always"
9 | ],
10 | "subject-case": [
11 | 2,
12 | "always",
13 | [
14 | "sentence-case",
15 | "start-case",
16 | "pascal-case",
17 | "upper-case",
18 | "lower-case",
19 | "camel-case"
20 | ]
21 | ],
22 | "type-enum": [
23 | 2,
24 | "always",
25 | [
26 | "ci",
27 | "doc",
28 | "feat",
29 | "fix",
30 | "hotfix",
31 | "chore",
32 | "refactor",
33 | "revert",
34 | "test"
35 | ]
36 | ]
37 | }
38 | }
--------------------------------------------------------------------------------
/src/modules/user/enums/user.status-code.enum.ts:
--------------------------------------------------------------------------------
1 | export enum EnumUserStatus_CODE_ERROR {
2 | notFound = 5150,
3 | notSelf = 5151,
4 | emailExist = 5152,
5 | usernameExist = 5153,
6 | mobileNumberNotFound = 5154,
7 | statusInvalid = 5155,
8 | blockedInvalid = 5156,
9 | inactiveForbidden = 5157,
10 | deletedForbidden = 5158,
11 | blockedForbidden = 5159,
12 | passwordNotMatch = 5160,
13 | passwordMustNew = 5161,
14 | passwordExpired = 5162,
15 | passwordAttemptMax = 5163,
16 | mobileNumberInvalid = 5164,
17 | usernameNotAllowed = 5165,
18 | usernameContainBadWord = 5166,
19 | emailNotVerified = 5167,
20 | loginWithNotSupported = 5168,
21 | passwordNotSet = 5169,
22 | tokenInvalid = 5170,
23 | emailAlreadyVerified = 5171,
24 | mobileNumberExist = 5172,
25 | verificationEmailResendLimitExceeded = 5173,
26 | forgotPasswordRequestLimitExceeded = 5174,
27 | }
28 |
--------------------------------------------------------------------------------
/src/configs/logger.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 | import ms from 'ms';
3 |
4 | export interface IConfigDebug {
5 | enable: boolean;
6 | level: string;
7 | intoFile: boolean;
8 | filePath: string;
9 | auto: boolean;
10 | prettier: boolean;
11 | sentry: {
12 | dsn?: string;
13 | timeout: number; // in milliseconds
14 | };
15 | }
16 |
17 | export default registerAs(
18 | 'logger',
19 | (): IConfigDebug => ({
20 | enable: process.env.LOGGER_ENABLE === 'true',
21 | level: process.env.LOGGER_LEVEL ?? 'debug',
22 | intoFile: process.env.LOGGER_INTO_FILE === 'true',
23 | filePath: '/logs',
24 | auto: process.env.LOGGER_AUTO === 'true',
25 | prettier: process.env.LOGGER_PRETTIER === 'true',
26 | sentry: {
27 | dsn: process.env.SENTRY_DSN,
28 | timeout: ms('10s'),
29 | },
30 | })
31 | );
32 |
--------------------------------------------------------------------------------
/src/common/file/enums/file.enum.ts:
--------------------------------------------------------------------------------
1 | export enum EnumFileExtensionImage {
2 | jpg = 'jpg',
3 | jpeg = 'jpeg',
4 | png = 'png',
5 | }
6 |
7 | export enum EnumFileExtensionDocument {
8 | pdf = 'pdf',
9 | csv = 'csv',
10 | }
11 |
12 | export enum EnumFileExtensionTemplate {
13 | hbs = 'hbs',
14 | }
15 |
16 | export enum EnumFileExtensionAudio {
17 | mpeg = 'mpeg',
18 | m4a = 'm4a',
19 | mp3 = 'mp3',
20 | }
21 |
22 | export enum EnumFileExtensionVideo {
23 | mp4 = 'mp4',
24 | }
25 |
26 | export const EnumFileExtension = {
27 | ...EnumFileExtensionImage,
28 | ...EnumFileExtensionDocument,
29 | ...EnumFileExtensionAudio,
30 | ...EnumFileExtensionVideo,
31 | ...EnumFileExtensionTemplate,
32 | };
33 |
34 | export type EnumFileExtension =
35 | | EnumFileExtensionImage
36 | | EnumFileExtensionDocument
37 | | EnumFileExtensionAudio
38 | | EnumFileExtensionVideo
39 | | EnumFileExtensionTemplate;
40 |
--------------------------------------------------------------------------------
/src/modules/term-policy/term-policy.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { TermPolicyService } from '@modules/term-policy/services/term-policy.service';
3 | import { TermPolicyRepository } from '@modules/term-policy/repositories/term-policy.repository';
4 | import { TermPolicyUtil } from '@modules/term-policy/utils/term-policy.util';
5 | import { AwsModule } from '@common/aws/aws.module';
6 | import { TermPolicyTemplateService } from '@modules/term-policy/services/term-policy.template.service';
7 |
8 | @Global()
9 | @Module({
10 | imports: [AwsModule],
11 | providers: [
12 | TermPolicyService,
13 | TermPolicyTemplateService,
14 | TermPolicyRepository,
15 | TermPolicyUtil,
16 | ],
17 | exports: [
18 | TermPolicyService,
19 | TermPolicyTemplateService,
20 | TermPolicyRepository,
21 | TermPolicyUtil,
22 | ],
23 | })
24 | export class TermPolicyModule {}
25 |
--------------------------------------------------------------------------------
/src/modules/policy/decorators/policy.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata, UseGuards, applyDecorators } from '@nestjs/common';
2 | import { PolicyRequiredAbilityMetaKey } from '@modules/policy/constants/policy.constant';
3 | import { PolicyAbilityGuard } from '@modules/policy/guards/policy.ability.guard';
4 | import { RoleAbilityRequestDto } from '@modules/role/dtos/request/role.ability.request.dto';
5 |
6 | /**
7 | * Method decorator that applies policy ability-based protection guards
8 | * @param {...RoleAbilityRequestDto[]} requiredAbilities - List of policy abilities required for access
9 | * @returns {MethodDecorator} Combined decorators for policy ability validation
10 | */
11 | export function PolicyAbilityProtected(
12 | ...requiredAbilities: RoleAbilityRequestDto[]
13 | ): MethodDecorator {
14 | return applyDecorators(
15 | UseGuards(PolicyAbilityGuard),
16 | SetMetadata(PolicyRequiredAbilityMetaKey, requiredAbilities)
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/modules/activity-log/docs/activity-log.shared.doc.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Doc,
3 | DocAuth,
4 | DocGuard,
5 | DocResponsePaging,
6 | } from '@common/doc/decorators/doc.decorator';
7 | import { EnumPaginationType } from '@common/pagination/enums/pagination.enum';
8 | import { ActivityLogResponseDto } from '@modules/activity-log/dtos/response/activity-log.response.dto';
9 | import { applyDecorators } from '@nestjs/common';
10 |
11 | export function ActivityLogSharedListDoc(): MethodDecorator {
12 | return applyDecorators(
13 | Doc({
14 | summary: 'get all activity logs',
15 | }),
16 | DocAuth({
17 | xApiKey: true,
18 | jwtAccessToken: true,
19 | }),
20 | DocGuard({ termPolicy: true }),
21 | DocResponsePaging('activityLog.list', {
22 | dto: ActivityLogResponseDto,
23 | type: EnumPaginationType.cursor,
24 | })
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | dist/
3 | node_modules/
4 | .warmup/
5 | generated/
6 | swagger.json
7 | /src/metadata.ts
8 |
9 | # Logs
10 | logs
11 | *.log
12 | npm-debug.log*
13 | pnpm-debug.log*
14 | lerna-debug.log*
15 |
16 | # OS
17 | .DS_Store
18 |
19 | # Tests
20 | /coverage
21 | /.nyc_output
22 |
23 | # IDEs and editors
24 | /.idea
25 | .project
26 | .classpath
27 | .c9/
28 | *.launch
29 | .settings/
30 | *.sublime-workspace
31 |
32 | # IDE - VSCode
33 | .vscode/*
34 | !.vscode/settings.json
35 | !.vscode/tasks.json
36 | !.vscode/launch.json
37 | !.vscode/extensions.json
38 |
39 | # pnpm
40 | .pnpm-store/
41 | pnpm-debug.log*
42 |
43 | # environment
44 | .env*
45 | !.env.example
46 | config.yaml
47 | config.yml
48 |
49 | # Runtime generated files
50 | keys/
51 | *.pem
52 | *.pub
53 | jwks.json
54 |
55 | # Temporary and cache files
56 | .cache/
57 | .temp/
58 | .tmp/
59 | temp/
60 | tmp/
61 |
62 | # Build and development artifacts
63 | .swc/
64 | .turbo/
65 | .parcel-cache/
--------------------------------------------------------------------------------
/src/modules/user/dtos/user.term-policy.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { EnumTermPolicyType } from '@prisma/client';
3 |
4 | export class UserTermPolicyDto {
5 | @ApiProperty({
6 | required: true,
7 | description: 'Terms of Service acceptance',
8 | example: true,
9 | })
10 | [EnumTermPolicyType.termsOfService]: boolean;
11 |
12 | @ApiProperty({
13 | required: true,
14 | description: 'Privacy Policy acceptance',
15 | example: true,
16 | })
17 | [EnumTermPolicyType.privacy]: boolean;
18 |
19 | @ApiProperty({
20 | required: true,
21 | description: 'Cookie Policy acceptance',
22 | example: true,
23 | })
24 | [EnumTermPolicyType.cookies]: boolean;
25 |
26 | @ApiProperty({
27 | required: true,
28 | description: 'Marketing Policy acceptance',
29 | example: false,
30 | })
31 | [EnumTermPolicyType.marketing]: boolean;
32 | }
33 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | workflow_dispatch:
4 | pull_request:
5 | branches:
6 | - main
7 | - staging
8 | - development
9 |
10 | env:
11 | NODE_VERSION: lts/*
12 |
13 | jobs:
14 | test:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Git checkout
19 | uses: actions/checkout@v6
20 |
21 | - name: Setup node
22 | uses: actions/setup-node@v6
23 | with:
24 | node-version: ${{ env.NODE_VERSION }}
25 |
26 | - name: Setup pnpm
27 | uses: pnpm/action-setup@v4
28 |
29 | - name: Install dependencies
30 | run: pnpm install --frozen-lockfile
31 |
32 | - name: Generate Client
33 | run: pnpm db:generate
34 |
35 | - name: Unit Test
36 | run: NODE_ENV=test pnpm test
37 | env:
38 | CI: true
39 |
--------------------------------------------------------------------------------
/src/common/request/interfaces/request.interface.ts:
--------------------------------------------------------------------------------
1 | import { Request } from 'express';
2 | import { IAuthJwtAccessTokenPayload } from '@modules/auth/interfaces/auth.interface';
3 | import { IPaginationQuery } from '@common/pagination/interfaces/pagination.interface';
4 | import { ApiKey } from '@prisma/client';
5 | import { RoleAbilityDto } from '@modules/role/dtos/role.ability.dto';
6 | import { IUser } from '@modules/user/interfaces/user.interface';
7 | import { RequestUserAgentDto } from '@common/request/dtos/request.user-agent.dto';
8 |
9 | export interface IRequestApp extends Request {
10 | correlationId: string;
11 | user?: T;
12 |
13 | __apiKey?: ApiKey;
14 | __user?: IUser;
15 | __abilities?: RoleAbilityDto[];
16 |
17 | __pagination?: IPaginationQuery;
18 |
19 | __language: string;
20 | __version: string;
21 | }
22 |
23 | export interface IRequestLog {
24 | userAgent: RequestUserAgentDto;
25 | ipAddress: string;
26 | }
27 |
--------------------------------------------------------------------------------
/src/modules/auth/dtos/response/auth.token.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { EnumRoleType } from '@prisma/client';
3 |
4 | /**
5 | * Response DTO for authentication token containing token information and user role
6 | */
7 | export class AuthTokenResponseDto {
8 | @ApiProperty({
9 | example: 'Bearer',
10 | required: true,
11 | })
12 | tokenType: string;
13 |
14 | @ApiProperty({
15 | example: EnumRoleType.user,
16 | enum: EnumRoleType,
17 | type: String,
18 | required: true,
19 | })
20 | roleType: EnumRoleType;
21 |
22 | @ApiProperty({
23 | example: 3600,
24 | description: 'timestamp in minutes',
25 | required: true,
26 | })
27 | expiresIn: number;
28 |
29 | @ApiProperty({
30 | required: true,
31 | })
32 | accessToken: string;
33 |
34 | @ApiProperty({
35 | required: true,
36 | })
37 | refreshToken: string;
38 | }
39 |
--------------------------------------------------------------------------------
/src/modules/api-key/dtos/request/api-key.update-date.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty } from '@nestjs/swagger';
3 | import { IsISO8601, IsNotEmpty } from 'class-validator';
4 | import { GreaterThanEqualOtherProperty } from '@common/request/validations/request.greater-than-other-property.validation';
5 | import { IsAfterNow } from '@common/request/validations/request.is-after-now.validation';
6 |
7 | export class ApiKeyUpdateDateRequestDto {
8 | @ApiProperty({
9 | description: 'Api Key start date',
10 | example: faker.date.recent(),
11 | required: false,
12 | })
13 | @IsNotEmpty()
14 | @IsISO8601()
15 | @IsAfterNow()
16 | startAt: Date;
17 |
18 | @ApiProperty({
19 | description: 'Api Key end date',
20 | example: faker.date.recent(),
21 | required: false,
22 | })
23 | @IsNotEmpty()
24 | @IsISO8601()
25 | @GreaterThanEqualOtherProperty('startDate')
26 | endAt: Date;
27 | }
28 |
--------------------------------------------------------------------------------
/src/modules/role/utils/role.util.ts:
--------------------------------------------------------------------------------
1 | import { IActivityLogMetadata } from '@modules/activity-log/interfaces/activity-log.interface';
2 | import { RoleListResponseDto } from '@modules/role/dtos/response/role.list.response.dto';
3 | import { RoleDto } from '@modules/role/dtos/role.dto';
4 | import { Injectable } from '@nestjs/common';
5 | import { Role } from '@prisma/client';
6 | import { plainToInstance } from 'class-transformer';
7 |
8 | @Injectable()
9 | export class RoleUtil {
10 | mapList(roles: Role[]): RoleListResponseDto[] {
11 | return plainToInstance(RoleListResponseDto, roles);
12 | }
13 |
14 | mapOne(role: Role): RoleDto {
15 | return plainToInstance(RoleDto, role);
16 | }
17 |
18 | mapActivityLogMetadata(role: Role): IActivityLogMetadata {
19 | return {
20 | roleId: role.id,
21 | roleName: role.name,
22 | roleType: role.type,
23 | timestamp: role.updatedAt ?? role.createdAt,
24 | };
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/modules/api-key/dtos/response/api-key.create.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiKeyDto } from '@modules/api-key/dtos/api-key.dto';
2 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
3 | import { EnumApiKeyType } from '@prisma/client';
4 | import { Exclude } from 'class-transformer';
5 |
6 | export class ApiKeyCreateResponseDto extends ApiKeyDto {
7 | @ApiProperty({
8 | description: 'Secret key of ApiKey, only show at once',
9 | example: true,
10 | required: true,
11 | })
12 | secret: string;
13 |
14 | @ApiHideProperty()
15 | @Exclude()
16 | isActive: boolean;
17 |
18 | @ApiHideProperty()
19 | @Exclude()
20 | startDate?: Date;
21 |
22 | @ApiHideProperty()
23 | @Exclude()
24 | endDate?: Date;
25 |
26 | @ApiHideProperty()
27 | @Exclude()
28 | name?: string;
29 |
30 | @ApiHideProperty()
31 | @Exclude()
32 | type: EnumApiKeyType;
33 |
34 | @ApiHideProperty()
35 | @Exclude()
36 | key: string;
37 | }
38 |
--------------------------------------------------------------------------------
/src/modules/password-history/docs/password-history.shared.doc.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import {
3 | Doc,
4 | DocAuth,
5 | DocGuard,
6 | DocResponsePaging,
7 | } from '@common/doc/decorators/doc.decorator';
8 | import { PasswordHistoryResponseDto } from '@modules/password-history/dtos/response/password-history.response.dto';
9 | import { EnumPaginationType } from '@common/pagination/enums/pagination.enum';
10 |
11 | export function PasswordHistorySharedListDoc(): MethodDecorator {
12 | return applyDecorators(
13 | Doc({
14 | summary: 'get all user password Histories',
15 | }),
16 | DocAuth({
17 | xApiKey: true,
18 | jwtAccessToken: true,
19 | }),
20 | DocGuard({ termPolicy: true }),
21 | DocResponsePaging('passwordHistory.list', {
22 | dto: PasswordHistoryResponseDto,
23 | type: EnumPaginationType.cursor,
24 | })
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/modules/term-policy/decorators/term-policy.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata, UseGuards, applyDecorators } from '@nestjs/common';
2 | import { TermPolicyRequiredGuardMetaKey } from '@modules/term-policy/constants/term-policy.constant';
3 | import { TermPolicyGuard } from '@modules/term-policy/guards/term-policy.guard';
4 | import { EnumTermPolicyType } from '@prisma/client';
5 |
6 | /**
7 | * Method decorator that applies term policy acceptance protection to routes.
8 | * Validates if the user has accepted the required term policies before allowing access.
9 | *
10 | * @param {...EnumTermPolicyType[]} requiredTermPolicies - List of term policy types that must be accepted
11 | * @returns {MethodDecorator} Method decorator function
12 | */
13 | export function TermPolicyAcceptanceProtected(
14 | ...requiredTermPolicies: EnumTermPolicyType[]
15 | ): MethodDecorator {
16 | return applyDecorators(
17 | UseGuards(TermPolicyGuard),
18 | SetMetadata(TermPolicyRequiredGuardMetaKey, requiredTermPolicies)
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/modules/activity-log/docs/activity-log.admin.doc.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Doc,
3 | DocAuth,
4 | DocGuard,
5 | DocRequest,
6 | DocResponsePaging,
7 | } from '@common/doc/decorators/doc.decorator';
8 | import { ActivityLogResponseDto } from '@modules/activity-log/dtos/response/activity-log.response.dto';
9 | import { UserDocParamsId } from '@modules/user/constants/user.doc.constant';
10 | import { applyDecorators } from '@nestjs/common';
11 |
12 | export function ActivityLogAdminListDoc(): MethodDecorator {
13 | return applyDecorators(
14 | Doc({
15 | summary: 'get all activity logs',
16 | }),
17 | DocRequest({
18 | params: UserDocParamsId,
19 | }),
20 | DocAuth({
21 | xApiKey: true,
22 | jwtAccessToken: true,
23 | }),
24 | DocGuard({ role: true, policy: true, termPolicy: true }),
25 | DocResponsePaging('activityLog.list', {
26 | dto: ActivityLogResponseDto,
27 | })
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/modules/hello/controllers/hello.public.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';
2 | import { ApiTags } from '@nestjs/swagger';
3 | import { Response } from '@common/response/decorators/response.decorator';
4 | import { IResponseReturn } from '@common/response/interfaces/response.interface';
5 | import { HelloService } from '@modules/hello/services/hello.service';
6 | import { HelloResponseDto } from '@modules/hello/dtos/response/hello.response.dto';
7 | import { HelloPublicDoc } from '@modules/hello/docs/hello.public.doc';
8 |
9 | @ApiTags('modules.public.hello')
10 | @Controller({
11 | version: VERSION_NEUTRAL,
12 | path: '/hello',
13 | })
14 | export class HelloPublicController {
15 | constructor(private readonly helloService: HelloService) {}
16 |
17 | @HelloPublicDoc()
18 | @Response('hello.hello', {
19 | cache: true,
20 | })
21 | @Get('/')
22 | async hello(): Promise> {
23 | return this.helloService.hello();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/migration/data/migration.api-key.data.ts:
--------------------------------------------------------------------------------
1 | import { EnumAppEnvironment } from '@app/enums/app.enum';
2 | import { ApiKeyCreateRawRequestDto } from '@modules/api-key/dtos/request/api-key.create.request.dto';
3 | import { EnumApiKeyType } from '@prisma/client';
4 |
5 | export const migrationApiKeyData: Record<
6 | EnumAppEnvironment,
7 | ApiKeyCreateRawRequestDto[]
8 | > = {
9 | [EnumAppEnvironment.local]: [
10 | {
11 | name: 'Api Key Default',
12 | type: EnumApiKeyType.default,
13 | key: 'fyFGb7ywyM37TqDY8nuhAmGW5',
14 | secret: 'qbp7LmCxYUTHFwKvHnxGW1aTyjSNU6ytN21etK89MaP2Dj2KZP',
15 | },
16 | {
17 | name: 'Api Key System',
18 | type: EnumApiKeyType.system,
19 | key: 'UTDH0fuDMAbd1ZVnwnyrQJd8Q',
20 | secret: 'qbp7LmCxYUTHFwKvHnxGW1aTyjSNU6ytN21etK89MaP2Dj2KZP',
21 | },
22 | ],
23 | [EnumAppEnvironment.development]: [],
24 | [EnumAppEnvironment.staging]: [],
25 | [EnumAppEnvironment.production]: [],
26 | };
27 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.forgot-password-reset.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { UserChangePasswordRequestDto } from '@modules/user/dtos/request/user.change-password.request.dto';
3 | import { UserLoginVerifyTwoFactorRequestDto } from '@modules/user/dtos/request/user.login-verify-two-factor.request.dto';
4 | import {
5 | ApiProperty,
6 | IntersectionType,
7 | OmitType,
8 | PartialType,
9 | PickType,
10 | } from '@nestjs/swagger';
11 | import { IsNotEmpty, IsString } from 'class-validator';
12 |
13 | export class UserForgotPasswordResetRequestDto extends IntersectionType(
14 | PickType(UserChangePasswordRequestDto, ['newPassword'] as const),
15 | PartialType(
16 | OmitType(UserLoginVerifyTwoFactorRequestDto, ['challengeToken'])
17 | )
18 | ) {
19 | @ApiProperty({
20 | required: true,
21 | description: 'Forgot password token',
22 | example: faker.string.alphanumeric(20),
23 | })
24 | @IsString()
25 | @IsNotEmpty()
26 | token: string;
27 | }
28 |
--------------------------------------------------------------------------------
/src/common/request/middlewares/request.compression.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NestMiddleware } from '@nestjs/common';
2 | import { NextFunction, Response } from 'express';
3 | import { IRequestApp } from '@common/request/interfaces/request.interface';
4 | import compression from 'compression';
5 |
6 | /**
7 | * HTTP response compression middleware for improved performance and bandwidth optimization.
8 | * Applies gzip/deflate compression to responses based on client capabilities.
9 | */
10 | @Injectable()
11 | export class RequestCompressionMiddleware implements NestMiddleware {
12 | constructor() {}
13 |
14 | /**
15 | * Applies compression middleware to HTTP responses.
16 | *
17 | * @param _req - The Express request object
18 | * @param _res - The Express response object
19 | * @param next - The next middleware function
20 | */
21 | async use(
22 | _req: IRequestApp,
23 | _res: Response,
24 | next: NextFunction
25 | ): Promise {
26 | compression();
27 | next();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/modules/health/dtos/response/health.sentry.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { HealthCheckStatus, HealthIndicatorResult } from '@nestjs/terminus';
3 |
4 | export class HealthThirdPartyResponseDto {
5 | @ApiProperty({
6 | required: true,
7 | examples: ['error', 'ok', 'shutting_down'],
8 | })
9 | status: HealthCheckStatus;
10 |
11 | @ApiProperty({
12 | required: true,
13 | example: {
14 | sentry: {
15 | status: 'up',
16 | },
17 | },
18 | })
19 | info?: HealthIndicatorResult;
20 |
21 | @ApiProperty({
22 | required: true,
23 | example: {
24 | sentry: {
25 | status: 'down',
26 | },
27 | },
28 | })
29 | error?: HealthIndicatorResult;
30 |
31 | @ApiProperty({
32 | required: true,
33 | example: {
34 | sentry: {
35 | status: 'up',
36 | },
37 | },
38 | })
39 | details: HealthIndicatorResult;
40 | }
41 |
--------------------------------------------------------------------------------
/src/modules/user/user.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UserService } from '@modules/user/services/user.service';
3 | import { UserUtil } from '@modules/user/utils/user.util';
4 | import { UserRepository } from '@modules/user/repositories/user.repository';
5 | import { CountryModule } from '@modules/country/country.module';
6 | import { RoleModule } from '@modules/role/role.module';
7 | import { AwsModule } from '@common/aws/aws.module';
8 | import { SessionModule } from '@modules/session/session.module';
9 | import { PasswordHistoryModule } from '@modules/password-history/password-history.module';
10 | import { EmailModule } from '@modules/email/email.module';
11 |
12 | @Module({
13 | imports: [
14 | PasswordHistoryModule,
15 | CountryModule,
16 | RoleModule,
17 | AwsModule,
18 | SessionModule,
19 | EmailModule,
20 | ],
21 | exports: [UserService, UserUtil, UserRepository],
22 | providers: [UserService, UserUtil, UserRepository],
23 | controllers: [],
24 | })
25 | export class UserModule {}
26 |
--------------------------------------------------------------------------------
/src/modules/password-history/docs/password-history.admin.doc.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import {
3 | Doc,
4 | DocAuth,
5 | DocGuard,
6 | DocRequest,
7 | DocResponsePaging,
8 | } from '@common/doc/decorators/doc.decorator';
9 | import { UserDocParamsId } from '@modules/user/constants/user.doc.constant';
10 | import { PasswordHistoryResponseDto } from '@modules/password-history/dtos/response/password-history.response.dto';
11 |
12 | export function PasswordHistoryAdminListDoc(): MethodDecorator {
13 | return applyDecorators(
14 | Doc({
15 | summary: 'get all user password histories',
16 | }),
17 | DocRequest({
18 | params: UserDocParamsId,
19 | }),
20 | DocAuth({
21 | xApiKey: true,
22 | jwtAccessToken: true,
23 | }),
24 | DocGuard({ role: true, policy: true, termPolicy: true }),
25 | DocResponsePaging('passwordHistory.list', {
26 | dto: PasswordHistoryResponseDto,
27 | })
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/modules/role/dtos/request/role.ability.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import {
3 | ArrayNotEmpty,
4 | IsArray,
5 | IsEnum,
6 | IsNotEmpty,
7 | IsString,
8 | } from 'class-validator';
9 | import {
10 | EnumPolicyAction,
11 | EnumPolicySubject,
12 | } from '@modules/policy/enums/policy.enum';
13 |
14 | export class RoleAbilityRequestDto {
15 | @ApiProperty({
16 | required: true,
17 | description: 'Ability subject',
18 | enum: EnumPolicySubject,
19 | })
20 | @IsNotEmpty()
21 | @IsString()
22 | @IsEnum(EnumPolicySubject)
23 | subject: EnumPolicySubject;
24 |
25 | @ApiProperty({
26 | required: true,
27 | description: 'Ability action base on subject',
28 | isArray: true,
29 | default: [EnumPolicyAction.manage],
30 | enum: EnumPolicyAction,
31 | })
32 | @IsString({ each: true })
33 | @IsEnum(EnumPolicyAction, { each: true })
34 | @IsArray()
35 | @IsNotEmpty()
36 | @ArrayNotEmpty()
37 | action: EnumPolicyAction[];
38 | }
39 |
--------------------------------------------------------------------------------
/src/modules/auth/decorators/auth.social.decorator.ts:
--------------------------------------------------------------------------------
1 | import { UseGuards, applyDecorators } from '@nestjs/common';
2 | import { AuthSocialAppleGuard } from '@modules/auth/guards/social/auth.social.apple.guard';
3 | import { AuthSocialGoogleGuard } from '@modules/auth/guards/social/auth.social.google.guard';
4 |
5 | /**
6 | * Decorator that applies Google social authentication protection to a route.
7 | * Validates Google authentication token from request headers.
8 | * @returns MethodDecorator - Decorator function that applies Google authentication guard
9 | */
10 | export function AuthSocialGoogleProtected(): MethodDecorator {
11 | return applyDecorators(UseGuards(AuthSocialGoogleGuard));
12 | }
13 |
14 | /**
15 | * Decorator that applies Apple social authentication protection to a route.
16 | * Validates Apple authentication token from request headers.
17 | * @returns MethodDecorator - Decorator function that applies Apple authentication guard
18 | */
19 | export function AuthSocialAppleProtected(): MethodDecorator {
20 | return applyDecorators(UseGuards(AuthSocialAppleGuard));
21 | }
22 |
--------------------------------------------------------------------------------
/src/modules/term-policy/docs/term-policy.public.doc.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import {
3 | Doc,
4 | DocAuth,
5 | DocRequest,
6 | DocResponsePaging,
7 | } from '@common/doc/decorators/doc.decorator';
8 | import { TermPolicyResponseDto } from '@modules/term-policy/dtos/response/term-policy.response.dto';
9 | import { TermPolicyListPublicDocQuery } from '@modules/term-policy/constants/term-policy.doc.constant';
10 | import { EnumPaginationType } from '@common/pagination/enums/pagination.enum';
11 |
12 | export function TermPolicyPublicListDoc(): MethodDecorator {
13 | return applyDecorators(
14 | Doc({
15 | summary: 'Retrieve list of publish terms and policies',
16 | }),
17 | DocRequest({
18 | queries: TermPolicyListPublicDocQuery,
19 | }),
20 | DocAuth({
21 | xApiKey: true,
22 | }),
23 | DocResponsePaging('termPolicy.list', {
24 | dto: TermPolicyResponseDto,
25 | type: EnumPaginationType.cursor,
26 | })
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/modules/health/indicators/health.redis.indicator.ts:
--------------------------------------------------------------------------------
1 | import { CacheMainProvider } from '@common/cache/constants/cache.constant';
2 | import { Inject, Injectable } from '@nestjs/common';
3 | import {
4 | HealthIndicatorResult,
5 | HealthIndicatorService,
6 | } from '@nestjs/terminus';
7 | import { Cache } from 'cache-manager';
8 |
9 | @Injectable()
10 | export class HealthRedisIndicator {
11 | constructor(
12 | @Inject(CacheMainProvider) private cacheManager: Cache,
13 | private readonly healthIndicatorService: HealthIndicatorService
14 | ) {}
15 |
16 | async isHealthy(key: string): Promise {
17 | const indicator = this.healthIndicatorService.check(key);
18 |
19 | try {
20 | await this.cacheManager.get('health-check');
21 |
22 | return indicator.up();
23 | } catch (err: unknown) {
24 | const message =
25 | err instanceof Error ? err.message : 'Unknown error';
26 |
27 | return indicator.down(`HealthRedisIndicator Failed - ${message}`);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint", // ESLint for linting
4 | "esbenp.prettier-vscode", // Prettier for code formatting
5 | "firsttris.vscode-jest-runner", // Jest test runner
6 | "ms-vscode.vscode-typescript-next", // TypeScript support
7 | "mikestead.dotenv", // .env file support
8 | "streetsidesoftware.code-spell-checker", // Spell checking
9 | "pkief.material-icon-theme", // Nice icons for better project navigation
10 | "yzhang.markdown-all-in-one", // Markdown support
11 | "gruntfuggly.todo-tree", // TODO comments highlighting
12 | "aaron-bond.better-comments", // Better comments highlighting
13 | "redhat.vscode-yaml", // YAML support for Docker Compose
14 | "ms-azuretools.vscode-docker", // Docker support
15 | "mhutchie.git-graph", // Git visualization
16 | "eamodio.gitlens", // Git integration
17 | "christian-kohler.path-intellisense", // Path autocompletion
18 | "christian-kohler.npm-intellisense" // NPM package autocompletion
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/modules/health/indicators/health.database.indicator.ts:
--------------------------------------------------------------------------------
1 | import { DatabaseService } from '@common/database/services/database.service';
2 | import { Injectable } from '@nestjs/common';
3 | import {
4 | HealthIndicatorResult,
5 | HealthIndicatorService,
6 | } from '@nestjs/terminus';
7 |
8 | @Injectable()
9 | export class HealthDatabaseIndicator {
10 | constructor(
11 | private readonly databaseService: DatabaseService,
12 | private readonly healthIndicatorService: HealthIndicatorService
13 | ) {}
14 |
15 | async isHealthy(key: string): Promise {
16 | const indicator = this.healthIndicatorService.check(key);
17 |
18 | try {
19 | await this.databaseService.$runCommandRaw({ ping: 1 });
20 |
21 | return indicator.up();
22 | } catch (err: unknown) {
23 | const message =
24 | err instanceof Error ? err.message : 'Unknown error';
25 |
26 | return indicator.down(
27 | `HealthDatabaseIndicator Failed - ${message}`
28 | );
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/modules/api-key/constants/api-key.doc.constant.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiParamOptions, ApiQueryOptions } from '@nestjs/swagger';
3 | import { EnumApiKeyType } from '@prisma/client';
4 |
5 | export const ApiKeyDocParamsId: ApiParamOptions[] = [
6 | {
7 | name: 'apiKeyId',
8 | allowEmptyValue: false,
9 | required: true,
10 | type: 'string',
11 | example: faker.database.mongodbObjectId(),
12 | },
13 | ];
14 |
15 | export const ApiKeyDocQueryList: ApiQueryOptions[] = [
16 | {
17 | name: 'isActive',
18 | allowEmptyValue: true,
19 | required: false,
20 | type: 'boolean',
21 | example: true,
22 | description: 'boolean value. Available values: true, false.',
23 | },
24 | {
25 | name: 'type',
26 | allowEmptyValue: true,
27 | required: false,
28 | type: 'string',
29 | example: Object.values(EnumApiKeyType).join(','),
30 | description: `enum value with ',' delimiter. Available values: ${Object.values(EnumApiKeyType).join(', ')}`,
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) [2020] [Andrechristikan]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.profile.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty, PickType } from '@nestjs/swagger';
2 | import { UserCreateRequestDto } from '@modules/user/dtos/request/user.create.request.dto';
3 | import { EnumUserGender } from '@prisma/client';
4 | import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
5 | import { AwsS3PresignRequestDto } from '@common/aws/dtos/request/aws.s3-presign.request.dto';
6 |
7 | export class UserUpdateProfileRequestDto extends PickType(
8 | UserCreateRequestDto,
9 | ['name', 'countryId'] as const
10 | ) {
11 | @ApiProperty({
12 | required: true,
13 | enum: EnumUserGender,
14 | example: EnumUserGender.male,
15 | })
16 | @IsEnum(EnumUserGender)
17 | @IsNotEmpty()
18 | gender: EnumUserGender;
19 | }
20 |
21 | export class UserUpdateProfilePhotoRequestDto extends PickType(
22 | AwsS3PresignRequestDto,
23 | ['size']
24 | ) {
25 | @ApiProperty({
26 | required: true,
27 | description: 'photo path key',
28 | example: 'user/profile/unique-photo-key.jpg',
29 | })
30 | @IsString()
31 | @IsNotEmpty()
32 | photoKey: string;
33 | }
34 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/user.mobile-number.dto.ts:
--------------------------------------------------------------------------------
1 | import { DatabaseDto } from '@common/database/dtos/database.dto';
2 | import { faker } from '@faker-js/faker';
3 | import { CountryResponseDto } from '@modules/country/dtos/response/country.response.dto';
4 | import { ApiProperty, getSchemaPath } from '@nestjs/swagger';
5 | import { Type } from 'class-transformer';
6 |
7 | export class UserMobileNumberResponseDto extends DatabaseDto {
8 | @ApiProperty({
9 | example: `8${faker.string.fromCharacters('1234567890', {
10 | min: 7,
11 | max: 11,
12 | })}`,
13 | required: true,
14 | maxLength: 20,
15 | minLength: 8,
16 | })
17 | number: string;
18 |
19 | @ApiProperty({
20 | example: faker.location.countryCode('alpha-2'),
21 | required: true,
22 | maxLength: 6,
23 | minLength: 1,
24 | })
25 | phoneCode: string;
26 |
27 | @ApiProperty({
28 | required: true,
29 | type: CountryResponseDto,
30 | oneOf: [{ $ref: getSchemaPath(CountryResponseDto) }],
31 | })
32 | @Type(() => CountryResponseDto)
33 | country: CountryResponseDto;
34 | }
35 |
--------------------------------------------------------------------------------
/src/languages/en/apiKey.json:
--------------------------------------------------------------------------------
1 | {
2 | "list": "Here's your complete list of API keys.",
3 | "create": "Great! Your new API key has been created successfully.",
4 | "reset": "Perfect! Your API key has been reset and is ready to use.",
5 | "update": "Your API key has been updated successfully.",
6 | "updateDate": "API key dates have been refreshed successfully.",
7 | "updateStatus": "API key status has been updated successfully.",
8 | "delete": "Your API key has been safely removed.",
9 | "error": {
10 | "notFound": "We couldn't locate this API key. Please check and try again.",
11 | "inactive": "This API key is currently inactive.",
12 | "expired": "This API key has expired. Would you like to create a new one?",
13 | "xApiKey": {
14 | "required": "Please provide your API key to continue.",
15 | "notFound": "We couldn't find this API key in our system.",
16 | "invalid": "Sorry, this API key appears to be invalid.",
17 | "forbidden": "You don't have permission to use this API key.",
18 | "predefinedNotFound": "Predefined API key not found in the system."
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src",
5 | "compilerOptions": {
6 | "deleteOutDir": true,
7 | "watchAssets": true,
8 | "typeCheck": true,
9 | "builder": {
10 | "type": "swc",
11 | "options": {
12 | "swcrcPath": ".swcrc",
13 | "outDir": "dist"
14 | }
15 | },
16 | "plugins": [],
17 | "assets": [
18 | {
19 | "include": "languages/**/*",
20 | "outDir": "dist"
21 | },
22 | {
23 | "include": "**/**/templates/*",
24 | "outDir": "dist"
25 | }
26 | ]
27 | },
28 | "projects": {
29 | "app": {
30 | "type": "application",
31 | "root": "src",
32 | "entryFile": "main",
33 | "sourceRoot": "src"
34 | },
35 | "migration": {
36 | "type": "application",
37 | "root": "src",
38 | "entryFile": "migration",
39 | "sourceRoot": "src"
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.mobile-number.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty, PickType } from '@nestjs/swagger';
3 | import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
4 | import { UserCreateRequestDto } from '@modules/user/dtos/request/user.create.request.dto';
5 |
6 | export class UserAddMobileNumberRequestDto extends PickType(
7 | UserCreateRequestDto,
8 | ['countryId'] as const
9 | ) {
10 | @ApiProperty({
11 | example: `8${faker.string.fromCharacters('1234567890', {
12 | min: 7,
13 | max: 11,
14 | })}`,
15 | required: true,
16 | maxLength: 20,
17 | minLength: 8,
18 | })
19 | @IsString()
20 | @IsNotEmpty()
21 | @MinLength(8)
22 | @MaxLength(22)
23 | number: string;
24 |
25 | @ApiProperty({
26 | example: '62',
27 | required: true,
28 | maxLength: 6,
29 | minLength: 1,
30 | })
31 | @IsString()
32 | @IsNotEmpty()
33 | @MinLength(1)
34 | @MaxLength(6)
35 | phoneCode: string;
36 | }
37 |
38 | export class UserUpdateMobileNumberRequestDto extends UserAddMobileNumberRequestDto {}
39 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/response/user.login.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { UserTwoFactorResponseDto } from '@modules/user/dtos/response/user.two-factor.response.dto';
3 | import { Type } from 'class-transformer';
4 | import { AuthTokenResponseDto } from '@modules/auth/dtos/response/auth.token.response.dto';
5 |
6 | export class UserLoginResponseDto {
7 | @ApiProperty({
8 | description:
9 | 'Indicates whether an additional 2FA verification step is enable',
10 | example: false,
11 | required: true,
12 | })
13 | isTwoFactorEnable: boolean;
14 |
15 | @ApiProperty({
16 | required: false,
17 | type: AuthTokenResponseDto,
18 | description: 'Provides access and refresh tokens upon successful login',
19 | })
20 | @Type(() => AuthTokenResponseDto)
21 | tokens?: AuthTokenResponseDto;
22 |
23 | @ApiProperty({
24 | required: false,
25 | type: UserTwoFactorResponseDto,
26 | description:
27 | 'Provides details for completing the 2FA verification step',
28 | })
29 | @Type(() => UserTwoFactorResponseDto)
30 | twoFactor?: UserTwoFactorResponseDto;
31 | }
32 |
--------------------------------------------------------------------------------
/ci/dockerfile:
--------------------------------------------------------------------------------
1 | # Build stage base
2 | FROM node:lts-alpine AS base
3 | LABEL maintainer="andrechristikan@gmail.com"
4 | ARG NODE_ENV
5 | ENV NODE_ENV=${NODE_ENV}
6 | RUN corepack enable && corepack prepare pnpm@latest --activate
7 | WORKDIR /app
8 |
9 | # Stage to install all dependencies and build the app
10 | FROM base AS builder
11 | COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
12 | RUN set -x && pnpm install --frozen-lockfile --ignore-scripts
13 |
14 | COPY . .
15 |
16 | RUN pnpm db:generate
17 | RUN ls -la
18 | RUN pnpm build
19 | RUN ls -la
20 |
21 | # Main Stage
22 | FROM base AS main
23 | COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
24 | RUN set -x && pnpm install --frozen-lockfile --prod --ignore-scripts
25 |
26 | # Copy only necessary files from previous stages
27 | COPY --from=builder /app/generated/prisma-client ./generated/prisma-client
28 | COPY --from=builder /app/dist ./dist
29 | COPY --from=builder /app/package.json ./package.json
30 |
31 | # Create non-root user for security
32 | RUN addgroup -S ec2-user && adduser -S ec2-user -G ec2-user && \
33 | chown -R ec2-user:ec2-user /app
34 | USER ec2-user
35 |
36 | RUN touch .env
37 | EXPOSE 3000
38 |
39 | CMD ["pnpm", "start:prod"]
40 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/response/user.two-factor.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { UserTwoFactorSetupResponseDto } from '@modules/user/dtos/response/user.two-factor-setup.response.dto';
2 | import { ApiProperty, PartialType } from '@nestjs/swagger';
3 |
4 | export class UserTwoFactorResponseDto extends PartialType(
5 | UserTwoFactorSetupResponseDto
6 | ) {
7 | @ApiProperty({
8 | description:
9 | 'Indicates whether the user is required to set up 2FA upon next login',
10 | example: false,
11 | required: true,
12 | })
13 | isRequiredSetup: boolean;
14 |
15 | @ApiProperty({
16 | required: true,
17 | description: 'Challenge token to be used for completing 2FA login',
18 | example: '2b5b8933f0a44a94b3e1a96f8d2e2f21',
19 | })
20 | challengeToken: string;
21 |
22 | @ApiProperty({
23 | required: true,
24 | description: 'Challenge token TTL in milliseconds',
25 | example: 300,
26 | })
27 | challengeExpiresInMs: number;
28 |
29 | @ApiProperty({
30 | required: false,
31 | description: 'Remaining backup codes count for the account',
32 | example: 8,
33 | })
34 | backupCodesRemaining?: number;
35 | }
36 |
--------------------------------------------------------------------------------
/src/modules/session/interfaces/session.service.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IPaginationQueryCursorParams,
3 | IPaginationQueryOffsetParams,
4 | } from '@common/pagination/interfaces/pagination.interface';
5 | import { IRequestLog } from '@common/request/interfaces/request.interface';
6 | import {
7 | IResponsePagingReturn,
8 | IResponseReturn,
9 | } from '@common/response/interfaces/response.interface';
10 | import { SessionResponseDto } from '@modules/session/dtos/response/session.response.dto';
11 |
12 | export interface ISessionService {
13 | getListOffsetByUser(
14 | userId: string,
15 | pagination: IPaginationQueryOffsetParams
16 | ): Promise>;
17 | getListCursorByUser(
18 | userId: string,
19 | pagination: IPaginationQueryCursorParams
20 | ): Promise>;
21 | revoke(
22 | userId: string,
23 | sessionId: string,
24 | requestLog: IRequestLog
25 | ): Promise>;
26 | revokeByAdmin(
27 | userId: string,
28 | sessionId: string,
29 | requestLog: IRequestLog,
30 | revokeBy: string
31 | ): Promise>;
32 | }
33 |
--------------------------------------------------------------------------------
/src/modules/api-key/guards/x-api-key/api-key.x-api-key.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2 | import { IRequestApp } from '@common/request/interfaces/request.interface';
3 | import { ApiKeyService } from '@modules/api-key/services/api-key.service';
4 |
5 | /**
6 | * Guard that validates X-API-Key header authentication.
7 | * Extracts and validates the API key from request headers and attaches it to the request object.
8 | */
9 | @Injectable()
10 | export class ApiKeyXApiKeyGuard implements CanActivate {
11 | constructor(private readonly apiKeyService: ApiKeyService) {}
12 |
13 | /**
14 | * Validates the X-API-Key header and attaches the API key to the request.
15 | *
16 | * @param {ExecutionContext} context - The execution context containing request information
17 | * @returns {Promise} Promise that resolves to true if API key is valid
18 | */
19 | async canActivate(context: ExecutionContext): Promise {
20 | const request = context.switchToHttp().getRequest();
21 | const apiKey = await this.apiKeyService.validateXApiKeyGuard(request);
22 |
23 | request.__apiKey = apiKey;
24 |
25 | return true;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/migration/data/migration.user.data.ts:
--------------------------------------------------------------------------------
1 | import { EnumAppEnvironment } from '@app/enums/app.enum';
2 |
3 | const userData: {
4 | country: string;
5 | email: Lowercase;
6 | name: string;
7 | role: string;
8 | password: string;
9 | }[] = [
10 | {
11 | country: 'ID',
12 | email: 'superadmin@mail.com',
13 | name: 'Super Admin',
14 | role: 'superadmin',
15 | password: 'aaAA@123',
16 | },
17 | {
18 | country: 'ID',
19 | email: 'admin@mail.com',
20 | name: 'Admin',
21 | role: 'admin',
22 | password: 'aaAA@123',
23 | },
24 | {
25 | country: 'ID',
26 | email: 'user@mail.com',
27 | name: 'User',
28 | role: 'user',
29 | password: 'aaAA@123',
30 | },
31 | ];
32 |
33 | export const migrationUserData: Record<
34 | EnumAppEnvironment,
35 | {
36 | country: string;
37 | email: string;
38 | name: string;
39 | role: string;
40 | password: string;
41 | }[]
42 | > = {
43 | [EnumAppEnvironment.local]: userData,
44 | [EnumAppEnvironment.development]: userData,
45 | [EnumAppEnvironment.staging]: userData,
46 | [EnumAppEnvironment.production]: userData,
47 | };
48 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/response/user.profile.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty, getSchemaPath } from '@nestjs/swagger';
2 | import { Type } from 'class-transformer';
3 | import { RoleDto } from '@modules/role/dtos/role.dto';
4 | import { CountryResponseDto } from '@modules/country/dtos/response/country.response.dto';
5 | import { UserDto } from '@modules/user/dtos/user.dto';
6 | import { UserMobileNumberResponseDto } from '@modules/user/dtos/user.mobile-number.dto';
7 |
8 | export class UserProfileResponseDto extends UserDto {
9 | @ApiProperty({
10 | required: true,
11 | type: RoleDto,
12 | oneOf: [{ $ref: getSchemaPath(RoleDto) }],
13 | })
14 | @Type(() => RoleDto)
15 | role: RoleDto;
16 |
17 | @ApiProperty({
18 | required: true,
19 | type: CountryResponseDto,
20 | oneOf: [{ $ref: getSchemaPath(CountryResponseDto) }],
21 | })
22 | @Type(() => CountryResponseDto)
23 | country: CountryResponseDto;
24 |
25 | @ApiProperty({
26 | required: false,
27 | type: UserMobileNumberResponseDto,
28 | oneOf: [{ $ref: getSchemaPath(UserMobileNumberResponseDto) }],
29 | })
30 | @Type(() => UserMobileNumberResponseDto)
31 | mobileNumber?: UserMobileNumberResponseDto;
32 | }
33 |
--------------------------------------------------------------------------------
/src/modules/health/indicators/health.aws-s3.indicator.ts:
--------------------------------------------------------------------------------
1 | import { EnumAwsS3Accessibility } from '@common/aws/enums/aws.enum';
2 | import { AwsS3Service } from '@common/aws/services/aws.s3.service';
3 | import { Injectable } from '@nestjs/common';
4 | import {
5 | HealthIndicatorResult,
6 | HealthIndicatorService,
7 | } from '@nestjs/terminus';
8 |
9 | @Injectable()
10 | export class HealthAwsS3BucketIndicator {
11 | constructor(
12 | private readonly awsS3Service: AwsS3Service,
13 | private readonly healthIndicatorService: HealthIndicatorService
14 | ) {}
15 |
16 | async isHealthy(
17 | key: string,
18 | access?: EnumAwsS3Accessibility
19 | ): Promise {
20 | const indicator = this.healthIndicatorService.check(key);
21 |
22 | try {
23 | await this.awsS3Service.checkBucket({
24 | access,
25 | });
26 |
27 | return indicator.up();
28 | } catch (err: unknown) {
29 | const message =
30 | err instanceof Error ? err.message : 'Unknown error';
31 |
32 | return indicator.down(
33 | `HealthAwsS3BucketIndicator Failed - ${message}`
34 | );
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.login.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsCustomEmail } from '@common/request/validations/request.custom-email.validation';
2 | import { faker } from '@faker-js/faker';
3 | import { ApiProperty } from '@nestjs/swagger';
4 | import { EnumUserLoginFrom } from '@prisma/client';
5 | import { Transform } from 'class-transformer';
6 | import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
7 |
8 | export class UserLoginRequestDto {
9 | @ApiProperty({
10 | required: true,
11 | example: faker.internet.email(),
12 | })
13 | @IsString()
14 | @IsNotEmpty()
15 | @IsCustomEmail()
16 | @Transform(({ value }) => value.toLowerCase().trim())
17 | email: Lowercase;
18 |
19 | @ApiProperty({
20 | description: 'string password',
21 | required: true,
22 | example: faker.string.alphanumeric(10),
23 | })
24 | @IsString()
25 | @IsNotEmpty()
26 | password: string;
27 |
28 | @ApiProperty({
29 | description: 'from where the user is logging in',
30 | enum: EnumUserLoginFrom,
31 | example: EnumUserLoginFrom.website,
32 | required: true,
33 | })
34 | @IsNotEmpty()
35 | @IsEnum(EnumUserLoginFrom)
36 | from: EnumUserLoginFrom;
37 | }
38 |
--------------------------------------------------------------------------------
/src/common/database/dtos/database.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty } from '@nestjs/swagger';
3 |
4 | export class DatabaseDto {
5 | @ApiProperty({
6 | description: 'Database document identifier',
7 | example: faker.database.mongodbObjectId(),
8 | required: true,
9 | })
10 | id: string;
11 |
12 | @ApiProperty({
13 | description: 'Date created at',
14 | example: faker.date.recent(),
15 | required: true,
16 | })
17 | createdAt: Date;
18 |
19 | @ApiProperty({
20 | description: 'created by',
21 | required: false,
22 | })
23 | createdBy?: string;
24 |
25 | @ApiProperty({
26 | description: 'Date updated at',
27 | example: faker.date.recent(),
28 | required: true,
29 | })
30 | updatedAt: Date;
31 |
32 | @ApiProperty({
33 | description: 'updated by',
34 | required: false,
35 | })
36 | updatedBy?: string;
37 |
38 | @ApiProperty({
39 | description: 'Date delete at',
40 | required: false,
41 | })
42 | deletedAt?: Date;
43 |
44 | @ApiProperty({
45 | description: 'Delete by',
46 | required: false,
47 | })
48 | deletedBy?: string;
49 | }
50 |
--------------------------------------------------------------------------------
/src/modules/health/indicators/health.aws-ses.indicator.ts:
--------------------------------------------------------------------------------
1 | import { AwsSESService } from '@common/aws/services/aws.ses.service';
2 | import { Injectable } from '@nestjs/common';
3 | import {
4 | HealthIndicatorResult,
5 | HealthIndicatorService,
6 | } from '@nestjs/terminus';
7 |
8 | @Injectable()
9 | export class HealthAwsSESIndicator {
10 | constructor(
11 | private readonly awsSESService: AwsSESService,
12 | private readonly healthIndicatorService: HealthIndicatorService
13 | ) {}
14 |
15 | async isHealthy(key: string): Promise {
16 | const indicator = this.healthIndicatorService.check(key);
17 |
18 | try {
19 | const connection = await this.awsSESService.checkConnection();
20 |
21 | if (!connection) {
22 | return indicator.down(
23 | `HealthAwsSESIndicator Failed - Connection to AWS SES failed`
24 | );
25 | }
26 |
27 | return indicator.up();
28 | } catch (err: unknown) {
29 | const message =
30 | err instanceof Error ? err.message : 'Unknown error';
31 |
32 | return indicator.down(`HealthAwsSESIndicator Failed - ${message}`);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/modules/password-history/dtos/response/password-history.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { DatabaseDto } from '@common/database/dtos/database.dto';
2 | import { faker } from '@faker-js/faker';
3 | import { UserListResponseDto } from '@modules/user/dtos/response/user.list.response.dto';
4 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
5 | import { EnumPasswordHistoryType } from '@prisma/client';
6 | import { Exclude, Type } from 'class-transformer';
7 |
8 | export class PasswordHistoryResponseDto extends DatabaseDto {
9 | @ApiProperty({
10 | required: true,
11 | example: faker.database.mongodbObjectId(),
12 | })
13 | userId: string;
14 |
15 | @ApiProperty({
16 | required: true,
17 | type: UserListResponseDto,
18 | })
19 | @Type(() => UserListResponseDto)
20 | user: UserListResponseDto;
21 |
22 | @ApiHideProperty()
23 | @Exclude()
24 | password: string;
25 |
26 | @ApiProperty({
27 | required: true,
28 | example: EnumPasswordHistoryType.admin,
29 | enum: EnumPasswordHistoryType,
30 | })
31 | type: EnumPasswordHistoryType;
32 |
33 | @ApiProperty({
34 | required: true,
35 | example: faker.date.future(),
36 | })
37 | expiredAt: Date;
38 | }
39 |
--------------------------------------------------------------------------------
/src/modules/user/interfaces/user.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Country,
3 | EnumUserLoginFrom,
4 | EnumUserLoginWith,
5 | EnumVerificationType,
6 | Role,
7 | TwoFactor,
8 | User,
9 | UserMobileNumber,
10 | } from '@prisma/client';
11 |
12 | export interface IUser extends User {
13 | role: Role;
14 | twoFactor: TwoFactor;
15 | }
16 |
17 | export interface IUserMobileNumber extends UserMobileNumber {
18 | country: Country;
19 | }
20 |
21 | export interface IUserProfile extends IUser {
22 | mobileNumbers: IUserMobileNumber[];
23 | country: Country;
24 | }
25 |
26 | export interface IUserLogin {
27 | loginFrom: EnumUserLoginFrom;
28 | loginWith: EnumUserLoginWith;
29 | expiredAt: Date;
30 | jti: string;
31 | sessionId: string;
32 | }
33 |
34 | export interface IUserForgotPasswordCreate {
35 | expiredAt: Date;
36 | expiredInMinutes: number;
37 | resendInMinutes: number;
38 | reference: string;
39 | token: string;
40 | link: string;
41 | }
42 |
43 | export interface IUserVerificationCreate {
44 | type: EnumVerificationType;
45 | expiredAt: Date;
46 | expiredInMinutes: number;
47 | resendInMinutes: number;
48 | reference: string;
49 | token: string;
50 | link?: string;
51 | }
52 |
--------------------------------------------------------------------------------
/src/modules/term-policy/dtos/request/term-policy.content-presign.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { AwsS3PresignRequestDto } from '@common/aws/dtos/request/aws.s3-presign.request.dto';
2 | import { EnumMessageLanguage } from '@common/message/enums/message.enum';
3 | import { TermPolicyAcceptRequestDto } from '@modules/term-policy/dtos/request/term-policy.accept.request.dto';
4 | import { ApiProperty, IntersectionType, PickType } from '@nestjs/swagger';
5 | import { IsEnum, IsNotEmpty, IsNumber, IsString } from 'class-validator';
6 |
7 | export class TermPolicyContentPresignRequestDto extends IntersectionType(
8 | TermPolicyAcceptRequestDto,
9 | PickType(AwsS3PresignRequestDto, ['size'])
10 | ) {
11 | @ApiProperty({
12 | required: true,
13 | description: 'Language of the term document',
14 | example: EnumMessageLanguage.en,
15 | enum: EnumMessageLanguage,
16 | })
17 | @IsString()
18 | @IsEnum(EnumMessageLanguage)
19 | @IsNotEmpty()
20 | readonly language: EnumMessageLanguage;
21 |
22 | @ApiProperty({
23 | description: 'Version of the terms policy',
24 | example: 1,
25 | required: true,
26 | })
27 | @IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0 })
28 | @IsNotEmpty()
29 | readonly version: number;
30 | }
31 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/user.two-factor.dto.ts:
--------------------------------------------------------------------------------
1 | import { DatabaseDto } from '@common/database/dtos/database.dto';
2 | import { faker } from '@faker-js/faker';
3 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
4 | import { Exclude } from 'class-transformer';
5 |
6 | export class UserTwoFactorDto extends DatabaseDto {
7 | @ApiProperty({
8 | required: true,
9 | example: faker.database.mongodbObjectId(),
10 | })
11 | userId: string;
12 |
13 | @ApiHideProperty()
14 | @Exclude()
15 | secret?: string;
16 |
17 | @ApiHideProperty()
18 | @Exclude()
19 | iv?: string;
20 |
21 | @ApiHideProperty()
22 | @Exclude()
23 | backupCodes?: string[];
24 |
25 | @ApiProperty({
26 | required: true,
27 | description: 'Whether the user has 2FA enabled',
28 | example: false,
29 | })
30 | enabled: boolean;
31 |
32 | @ApiProperty({
33 | required: true,
34 | description: 'Whether the user is required to set up 2FA',
35 | example: false,
36 | })
37 | requiredSetup: boolean;
38 |
39 | @ApiProperty({
40 | required: false,
41 | example: faker.date.past(),
42 | })
43 | confirmedAt?: Date;
44 |
45 | @ApiHideProperty()
46 | @Exclude()
47 | lastUsedAt?: Date;
48 | }
49 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/response/user.two-factor-status.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 |
3 | export class UserTwoFactorStatusResponseDto {
4 | @ApiProperty({
5 | required: true,
6 | description:
7 | 'Whether two-factor authentication is enabled for the account',
8 | example: false,
9 | })
10 | isEnabled: boolean;
11 |
12 | @ApiProperty({
13 | required: true,
14 | description:
15 | 'True when 2FA setup has been started but not yet confirmed',
16 | example: false,
17 | })
18 | isPendingConfirmation: boolean;
19 |
20 | @ApiProperty({
21 | required: true,
22 | description: 'Remaining backup codes count for the account',
23 | example: 8,
24 | minimum: 0,
25 | })
26 | backupCodesRemaining: number;
27 |
28 | @ApiProperty({
29 | required: false,
30 | description: 'When two-factor authentication was confirmed/enabled',
31 | example: '2025-01-01T00:00:00.000Z',
32 | })
33 | confirmedAt?: Date;
34 |
35 | @ApiProperty({
36 | required: false,
37 | description: 'Last time two-factor authentication was used/verified',
38 | example: '2025-01-01T00:00:00.000Z',
39 | })
40 | lastUsedAt?: Date;
41 | }
42 |
--------------------------------------------------------------------------------
/src/modules/health/dtos/response/health.database.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { HealthCheckStatus, HealthIndicatorResult } from '@nestjs/terminus';
3 |
4 | export class HealthDatabaseResponseDto {
5 | @ApiProperty({
6 | required: true,
7 | examples: ['error', 'ok', 'shutting_down'],
8 | })
9 | status: HealthCheckStatus;
10 |
11 | @ApiProperty({
12 | required: true,
13 | example: {
14 | database: {
15 | status: 'up',
16 | },
17 | redis: {
18 | status: 'up',
19 | },
20 | },
21 | })
22 | info?: HealthIndicatorResult;
23 |
24 | @ApiProperty({
25 | required: true,
26 | example: {
27 | database: {
28 | status: 'down',
29 | },
30 | redis: {
31 | status: 'down',
32 | },
33 | },
34 | })
35 | error?: HealthIndicatorResult;
36 |
37 | @ApiProperty({
38 | required: true,
39 | example: {
40 | database: {
41 | status: 'up',
42 | },
43 | redis: {
44 | status: 'up',
45 | },
46 | },
47 | })
48 | details: HealthIndicatorResult;
49 | }
50 |
--------------------------------------------------------------------------------
/src/modules/country/services/country.service.ts:
--------------------------------------------------------------------------------
1 | import { IPaginationQueryOffsetParams } from '@common/pagination/interfaces/pagination.interface';
2 | import { IResponsePagingReturn } from '@common/response/interfaces/response.interface';
3 | import { CountryResponseDto } from '@modules/country/dtos/response/country.response.dto';
4 | import { ICountryService } from '@modules/country/interfaces/country.service.interface';
5 | import { CountryRepository } from '@modules/country/repositories/country.repository';
6 | import { CountryUtil } from '@modules/country/utils/country.util';
7 | import { Injectable } from '@nestjs/common';
8 |
9 | @Injectable()
10 | export class CountryService implements ICountryService {
11 | constructor(
12 | private readonly countryRepository: CountryRepository,
13 | private readonly countryUtil: CountryUtil
14 | ) {}
15 |
16 | async getList(
17 | pagination: IPaginationQueryOffsetParams
18 | ): Promise> {
19 | const { data, ...others } =
20 | await this.countryRepository.findWithPagination(pagination);
21 | const countries: CountryResponseDto[] = this.countryUtil.mapList(data);
22 |
23 | return {
24 | data: countries,
25 | ...others,
26 | };
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/common/logger/logger.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Module } from '@nestjs/common';
2 | import { LoggerModule as PinoLoggerModule } from 'nestjs-pino';
3 | import { LoggerOptionService } from '@common/logger/services/logger.option.service';
4 |
5 | /**
6 | * Module providing logger configuration options service.
7 | * Exports LoggerService for use in other modules requiring logger configuration.
8 | */
9 | @Module({})
10 | export class LoggerModule {
11 | /**
12 | * Creates and configures the logger module with Pino logger integration.
13 | * Sets up async configuration using LoggerOptionService.
14 | *
15 | * @returns {DynamicModule} Configured dynamic module with Pino logger
16 | */
17 | static forRoot(): DynamicModule {
18 | return {
19 | module: LoggerModule,
20 | imports: [
21 | PinoLoggerModule.forRootAsync({
22 | providers: [LoggerOptionService],
23 | inject: [LoggerOptionService],
24 | useFactory: async (
25 | loggerOptionService: LoggerOptionService
26 | ) => {
27 | return loggerOptionService.createOptions();
28 | },
29 | }),
30 | ],
31 | };
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/modules/auth/guards/social/auth.social.google.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2 | import { IRequestApp } from '@common/request/interfaces/request.interface';
3 | import { IAuthSocialPayload } from '@modules/auth/interfaces/auth.interface';
4 | import { AuthService } from '@modules/auth/services/auth.service';
5 |
6 | /**
7 | * Guard for validating Google social authentication tokens.
8 | * Implements CanActivate interface to protect routes that require Google authentication.
9 | */
10 | @Injectable()
11 | export class AuthSocialGoogleGuard implements CanActivate {
12 | constructor(private readonly authService: AuthService) {}
13 |
14 | /**
15 | * Validates Google authentication token and attaches user payload to request.
16 | * @param context - The execution context containing the HTTP request
17 | * @returns Promise - True if authentication is successful
18 | * @throws UnauthorizedException - When token is missing, malformed, or invalid
19 | */
20 | async canActivate(context: ExecutionContext): Promise {
21 | const request = context
22 | .switchToHttp()
23 | .getRequest>();
24 |
25 | return this.authService.validateOAuthGoogleGuard(request);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/modules/health/health.module.ts:
--------------------------------------------------------------------------------
1 | import { AwsModule } from '@common/aws/aws.module';
2 | import { HealthAwsS3BucketIndicator } from '@modules/health/indicators/health.aws-s3.indicator';
3 | import { HealthAwsSESIndicator } from '@modules/health/indicators/health.aws-ses.indicator';
4 | import { HealthDatabaseIndicator } from '@modules/health/indicators/health.database.indicator';
5 | import { HealthRedisIndicator } from '@modules/health/indicators/health.redis.indicator';
6 | import { HealthSentryIndicator } from '@modules/health/indicators/health.sentry.indicator';
7 | import { Module } from '@nestjs/common';
8 | import { TerminusModule } from '@nestjs/terminus';
9 |
10 | @Module({
11 | providers: [
12 | HealthAwsS3BucketIndicator,
13 | HealthAwsSESIndicator,
14 | HealthDatabaseIndicator,
15 | HealthRedisIndicator,
16 | HealthSentryIndicator,
17 | ],
18 | exports: [
19 | HealthAwsS3BucketIndicator,
20 | HealthAwsSESIndicator,
21 | HealthDatabaseIndicator,
22 | HealthRedisIndicator,
23 | HealthSentryIndicator,
24 | TerminusModule,
25 | ],
26 | imports: [
27 | AwsModule,
28 | TerminusModule.forRoot({
29 | gracefulShutdownTimeoutMs: 30000,
30 | }),
31 | ],
32 | })
33 | export class HealthModule {}
34 |
--------------------------------------------------------------------------------
/src/modules/role/dtos/role.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty, getSchemaPath } from '@nestjs/swagger';
3 | import { Type } from 'class-transformer';
4 | import { DatabaseDto } from '@common/database/dtos/database.dto';
5 | import { EnumRoleType } from '@prisma/client';
6 | import { RoleAbilityDto } from '@modules/role/dtos/role.ability.dto';
7 |
8 | export class RoleDto extends DatabaseDto {
9 | @ApiProperty({
10 | description: 'Name of role',
11 | example: faker.person.jobTitle(),
12 | required: true,
13 | })
14 | name: string;
15 |
16 | @ApiProperty({
17 | description: 'Description of role',
18 | example: faker.lorem.sentence(),
19 | required: false,
20 | maxLength: 500,
21 | })
22 | description?: string;
23 |
24 | @ApiProperty({
25 | description: 'Representative for role type',
26 | example: EnumRoleType.admin,
27 | required: true,
28 | enum: EnumRoleType,
29 | })
30 | type: EnumRoleType;
31 |
32 | @ApiProperty({
33 | type: RoleAbilityDto,
34 | oneOf: [{ $ref: getSchemaPath(RoleAbilityDto) }],
35 | required: true,
36 | isArray: true,
37 | default: [],
38 | })
39 | @Type(() => RoleAbilityDto)
40 | abilities: RoleAbilityDto[];
41 | }
42 |
--------------------------------------------------------------------------------
/src/modules/activity-log/decorators/activity-log.decorator.ts:
--------------------------------------------------------------------------------
1 | import { ActivityLogMetadataMetaKey } from '@modules/activity-log/constants/activity-log.constant';
2 | import { ActivityLogInterceptor } from '@modules/activity-log/interceptors/activity-log.interceptor';
3 | import { IActivityLogMetadata } from '@modules/activity-log/interfaces/activity-log.interface';
4 | import { SetMetadata, UseInterceptors, applyDecorators } from '@nestjs/common';
5 | import { EnumActivityLogAction } from '@prisma/client';
6 |
7 | /**
8 | * Decorator that enables activity logging for controller methods.
9 | * Automatically tracks user actions with optional metadata for audit trail purposes.
10 | *
11 | * @param {string} action - The activity action to be logged
12 | * @param {IActivityLogMetadata} metadata - Optional metadata to include with the activity log
13 | * @returns {MethodDecorator} Method decorator function
14 | */
15 | export function ActivityLog(
16 | action: string,
17 | metadata?: IActivityLogMetadata
18 | ): MethodDecorator {
19 | // TODO: Implement bidirectional logging support, and record when failed attempts occur
20 | return applyDecorators(
21 | UseInterceptors(ActivityLogInterceptor),
22 | SetMetadata(EnumActivityLogAction, action),
23 | SetMetadata(ActivityLogMetadataMetaKey, metadata)
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/modules/auth/guards/social/auth.social.apple.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2 | import { IRequestApp } from '@common/request/interfaces/request.interface';
3 | import { IAuthSocialPayload } from '@modules/auth/interfaces/auth.interface';
4 | import { AuthService } from '@modules/auth/services/auth.service';
5 |
6 | /**
7 | * Guard for validating Apple social authentication tokens.
8 | * Implements CanActivate interface to protect routes that require Apple authentication.
9 | */
10 | @Injectable()
11 | export class AuthSocialAppleGuard implements CanActivate {
12 | constructor(private readonly authService: AuthService) {}
13 |
14 | /**
15 | * Validates Apple authentication token and attaches user payload to request.
16 | *
17 | * @param {ExecutionContext} context - The execution context containing the HTTP request
18 | * @returns {Promise} True if authentication is successful
19 | * @throws {UnauthorizedException} When token is missing, malformed, or invalid
20 | */
21 | async canActivate(context: ExecutionContext): Promise {
22 | const request = context
23 | .switchToHttp()
24 | .getRequest>();
25 | return this.authService.validateOAuthAppleGuard(request);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/migration/data/migration.role.data.ts:
--------------------------------------------------------------------------------
1 | import { EnumAppEnvironment } from '@app/enums/app.enum';
2 | import {
3 | EnumPolicyAction,
4 | EnumPolicySubject,
5 | } from '@modules/policy/enums/policy.enum';
6 | import { RoleCreateRequestDto } from '@modules/role/dtos/request/role.create.request.dto';
7 | import { EnumRoleType } from '@prisma/client';
8 |
9 | const roleData: RoleCreateRequestDto[] = [
10 | {
11 | name: 'superadmin',
12 | description: 'Super Admin Role',
13 | abilities: [],
14 | type: EnumRoleType.superAdmin,
15 | },
16 | {
17 | name: 'admin',
18 | description: 'Admin Role',
19 | abilities: Object.values(EnumPolicySubject).map(role => ({
20 | subject: role,
21 | action: Object.values(EnumPolicyAction),
22 | })),
23 | type: EnumRoleType.admin,
24 | },
25 | {
26 | name: 'user',
27 | description: 'User Role',
28 | abilities: [],
29 | type: EnumRoleType.user,
30 | },
31 | ];
32 |
33 | export const migrationRoleData: Record<
34 | EnumAppEnvironment,
35 | RoleCreateRequestDto[]
36 | > = {
37 | [EnumAppEnvironment.local]: roleData,
38 | [EnumAppEnvironment.development]: roleData,
39 | [EnumAppEnvironment.staging]: roleData,
40 | [EnumAppEnvironment.production]: roleData,
41 | };
42 |
--------------------------------------------------------------------------------
/src/router/routes/routes.public.module.ts:
--------------------------------------------------------------------------------
1 | import { CountryPublicController } from '@modules/country/controllers/country.public.controller';
2 | import { CountryModule } from '@modules/country/country.module';
3 | import { HelloPublicController } from '@modules/hello/controllers/hello.public.controller';
4 | import { HelloModule } from '@modules/hello/hello.module';
5 | import { RolePublicController } from '@modules/role/controllers/role.public.controller';
6 | import { TermPolicyPublicController } from '@modules/term-policy/controllers/term-policy.public.controller';
7 | import { UserPublicController } from '@modules/user/controllers/user.public.controller';
8 | import { UserModule } from '@modules/user/user.module';
9 | import { Module } from '@nestjs/common';
10 |
11 | /**
12 | * Public routes module that provides publicly accessible endpoints.
13 | * Contains controllers for countries, roles, hello world, users, term policies, and verification that don't require authentication.
14 | */
15 | @Module({
16 | controllers: [
17 | CountryPublicController,
18 | RolePublicController,
19 | HelloPublicController,
20 | UserPublicController,
21 | TermPolicyPublicController,
22 | ],
23 | providers: [],
24 | exports: [],
25 | imports: [CountryModule, HelloModule, UserModule],
26 | })
27 | export class RoutesPublicModule {}
28 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.validate": [
3 | "javascript",
4 | "javascriptreact",
5 | "typescript",
6 | "typescriptreact"
7 | ],
8 | "search.exclude": {
9 | "package-lock.json": true,
10 | "pnpm-lock.yaml": true,
11 | "pnpm-workspace.yaml": true,
12 | "node_modules": true,
13 | "dist": true,
14 | "coverage": true
15 | },
16 |
17 | "editor.tabSize": 4,
18 | "eslint.useFlatConfig": true,
19 | "eslint.debug": true,
20 | "eslint.options": {
21 | "overrideConfigFile": "eslint.config.mjs"
22 | },
23 |
24 | "editor.defaultFormatter": "dbaeumer.vscode-eslint",
25 | "editor.formatOnSave": false,
26 | "editor.codeActionsOnSave": {
27 | "source.fixAll.eslint": "explicit"
28 | },
29 |
30 | "typescript.preferences.importModuleSpecifier": "non-relative",
31 | "typescript.preferences.quoteStyle": "single",
32 |
33 | "[typescript][json][jsonc][yaml][dockercompose][handlebars][javascript][dotenv][shellscript][dockerfile]": {
34 | "editor.formatOnSave": true,
35 | "editor.defaultFormatter": "esbenp.prettier-vscode"
36 | },
37 |
38 | "editor.bracketPairColorization.enabled": true,
39 | "better-comments.multilineComments": true,
40 | "better-comments.highlightPlainText": true,
41 |
42 | "prisma.pinToPrisma6": true
43 | }
44 |
--------------------------------------------------------------------------------
/src/modules/health/indicators/health.sentry.indicator.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | HealthIndicatorResult,
4 | HealthIndicatorService,
5 | } from '@nestjs/terminus';
6 | import * as Sentry from '@sentry/nestjs';
7 |
8 | @Injectable()
9 | export class HealthSentryIndicator {
10 | constructor(
11 | private readonly healthIndicatorService: HealthIndicatorService
12 | ) {}
13 |
14 | async isHealthy(key: string): Promise {
15 | const indicator = this.healthIndicatorService.check(key);
16 |
17 | try {
18 | const client = Sentry.getClient();
19 | if (!client) {
20 | return indicator.down('Sentry client not initialized');
21 | }
22 |
23 | const options = client.getOptions();
24 | if (!options.dsn) {
25 | return indicator.down('Sentry DSN not configured');
26 | }
27 |
28 | if (options.enabled === false) {
29 | return indicator.down('Sentry is disabled');
30 | }
31 |
32 | return indicator.up();
33 | } catch (err: unknown) {
34 | const message =
35 | err instanceof Error ? err.message : 'Unknown error';
36 |
37 | return indicator.down(`HealthSentryIndicator Failed - ${message}`);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.github/workflows/release-version.yml:
--------------------------------------------------------------------------------
1 | name: ReleaseVersion
2 |
3 | permissions:
4 | contents: write
5 |
6 | on:
7 | workflow_dispatch:
8 |
9 | jobs:
10 | release:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Git checkout
15 | uses: actions/checkout@v6
16 |
17 | - name: Get short sha commit
18 | id: git
19 | run: |
20 | echo "short_sha=$(git rev-parse --short $GITHUB_SHA)" >> "$GITHUB_OUTPUT"
21 |
22 | - name: Get latest version
23 | id: version
24 | uses: martinbeentjes/npm-get-version-action@main
25 |
26 | - name: Log info
27 | run: |
28 | echo Branch name is: ${{ github.ref_name }}
29 | echo Short sha: ${{ steps.git.outputs.short_sha }}
30 | echo Version is: ${{ steps.version.outputs.current-version }}
31 |
32 | - name: Release
33 | uses: softprops/action-gh-release@master
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 | with:
37 | tag_name: v${{ steps.version.outputs.current-version }}
38 | name: v${{ steps.version.outputs.current-version }}
39 | generate_release_notes: true
40 | draft: false
41 | prerelease: false
42 |
--------------------------------------------------------------------------------
/src/common/request/pipes/request.required.pipe.ts:
--------------------------------------------------------------------------------
1 | import { EnumRequestStatusCodeError } from '@common/request/enums/request.status-code.enum';
2 | import {
3 | ArgumentMetadata,
4 | BadRequestException,
5 | Injectable,
6 | } from '@nestjs/common';
7 | import { PipeTransform } from '@nestjs/common';
8 |
9 | /**
10 | * Validation pipe that ensures required parameters are present.
11 | * Throws BadRequestException when value is missing or empty.
12 | */
13 | @Injectable()
14 | export class RequestRequiredPipe implements PipeTransform {
15 | /**
16 | * Validates that the input value is not empty or undefined.
17 | *
18 | * @param value - The input value to validate
19 | * @param metadata - Argument metadata containing parameter information
20 | * @returns The validated value if present
21 | * @throws {BadRequestException} When value is missing or empty
22 | */
23 | async transform(
24 | value: string,
25 | metadata: ArgumentMetadata
26 | ): Promise {
27 | if (!value) {
28 | throw new BadRequestException({
29 | statusCode: EnumRequestStatusCodeError.paramRequired,
30 | message: 'request.error.paramRequired',
31 | messageProperties: {
32 | property: metadata.data,
33 | },
34 | });
35 | }
36 |
37 | return value;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/modules/role/docs/role.public.doc.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Doc,
3 | DocAuth,
4 | DocRequest,
5 | DocResponse,
6 | DocResponsePaging,
7 | } from '@common/doc/decorators/doc.decorator';
8 | import {
9 | RoleDocParamsId,
10 | RoleDocQueryList,
11 | } from '@modules/role/constants/role.doc.constant';
12 | import { RoleListResponseDto } from '@modules/role/dtos/response/role.list.response.dto';
13 | import { RoleDto } from '@modules/role/dtos/role.dto';
14 | import { applyDecorators } from '@nestjs/common';
15 |
16 | export function RolePublicListDoc(): MethodDecorator {
17 | return applyDecorators(
18 | Doc({
19 | summary: 'get all of roles',
20 | }),
21 | DocRequest({
22 | queries: RoleDocQueryList,
23 | }),
24 | DocAuth({
25 | xApiKey: true,
26 | }),
27 | DocResponsePaging('role.list', {
28 | dto: RoleListResponseDto,
29 | })
30 | );
31 | }
32 |
33 | export function RolePublicGetDoc(): MethodDecorator {
34 | return applyDecorators(
35 | Doc({
36 | summary: 'get detail a role',
37 | }),
38 | DocRequest({
39 | params: RoleDocParamsId,
40 | }),
41 | DocAuth({
42 | xApiKey: true,
43 | }),
44 | DocResponse('role.get', {
45 | dto: RoleDto,
46 | })
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/modules/term-policy/dtos/request/term-policy.content.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { TermPolicyContentPresignRequestDto } from '@modules/term-policy/dtos/request/term-policy.content-presign.request.dto';
2 | import { ApiProperty, PickType } from '@nestjs/swagger';
3 | import { Type } from 'class-transformer';
4 | import {
5 | ArrayNotEmpty,
6 | ArrayUnique,
7 | IsNotEmpty,
8 | IsObject,
9 | IsString,
10 | ValidateNested,
11 | } from 'class-validator';
12 |
13 | export class TermPolicyContentRequestDto extends PickType(
14 | TermPolicyContentPresignRequestDto,
15 | ['language', 'size'] as const
16 | ) {
17 | @ApiProperty({
18 | required: true,
19 | description: 'Key of the term document in storage',
20 | example: 'terms/privacy/en/terms_privacy_en_v1.hbs',
21 | })
22 | @IsString()
23 | @IsNotEmpty()
24 | readonly key: string;
25 | }
26 |
27 | export class TermPolicyContentsRequestDto {
28 | @ApiProperty({
29 | description: 'Contents of the terms policy',
30 | type: [TermPolicyContentRequestDto],
31 | isArray: true,
32 | })
33 | @Type(() => TermPolicyContentRequestDto)
34 | @IsNotEmpty({ each: true })
35 | @ValidateNested({ each: true })
36 | @ArrayUnique((content: TermPolicyContentRequestDto) => content.language)
37 | @ArrayNotEmpty()
38 | @IsObject({ each: true })
39 | readonly contents: TermPolicyContentRequestDto[];
40 | }
41 |
--------------------------------------------------------------------------------
/src/modules/country/repositories/country.repository.ts:
--------------------------------------------------------------------------------
1 | import { DatabaseService } from '@common/database/services/database.service';
2 | import { IPaginationQueryOffsetParams } from '@common/pagination/interfaces/pagination.interface';
3 | import { PaginationService } from '@common/pagination/services/pagination.service';
4 | import { IResponsePagingReturn } from '@common/response/interfaces/response.interface';
5 | import { Injectable } from '@nestjs/common';
6 | import { Country } from '@prisma/client';
7 |
8 | @Injectable()
9 | export class CountryRepository {
10 | constructor(
11 | private readonly databaseService: DatabaseService,
12 | private readonly paginationService: PaginationService
13 | ) {}
14 |
15 | async findWithPagination(
16 | pagination: IPaginationQueryOffsetParams
17 | ): Promise> {
18 | return this.paginationService.offset(
19 | this.databaseService.country,
20 | pagination
21 | );
22 | }
23 |
24 | async existById(id: string): Promise<{ id: string } | null> {
25 | return this.databaseService.country.findUnique({
26 | where: { id },
27 | select: { id: true },
28 | });
29 | }
30 |
31 | async findOneById(id: string): Promise {
32 | return this.databaseService.country.findUnique({
33 | where: { id },
34 | });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.swcrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://swc.rs/schema.json",
3 | "sourceMaps": true,
4 | "module": {
5 | "type": "commonjs"
6 | },
7 | "exclude": [
8 | "node_modules",
9 | "dist",
10 | "coverage",
11 | "logs",
12 | "keys",
13 | ".warmup",
14 | ".nyc_output",
15 | "src/metadata.ts",
16 | "swagger.json",
17 | "generated"
18 | ],
19 | "jsc": {
20 | "baseUrl": "./",
21 | "paths": {
22 | "@app/*": ["src/app/*"],
23 | "@common/*": ["src/common/*"],
24 | "@configs/*": ["src/configs/*"],
25 | "@config": ["src/configs/index.ts"],
26 | "@modules/*": ["src/modules/*"],
27 | "@routes/*": ["src/router/routes/*"],
28 | "@router": ["src/router/router.module.ts"],
29 | "@workers/*": ["src/worker/*"],
30 | "@migration/*": ["src/migration/*"],
31 | "@test/*": ["test/*"],
32 | "@generated/*": ["generated/*"],
33 | "@prisma/client": ["generated/prisma-client"]
34 | },
35 | "target": "esnext",
36 | "parser": {
37 | "syntax": "typescript",
38 | "decorators": true,
39 | "dynamicImport": true
40 | },
41 | "transform": {
42 | "legacyDecorator": true,
43 | "decoratorMetadata": true
44 | }
45 | },
46 | "minify": false
47 | }
48 |
--------------------------------------------------------------------------------
/src/configs/index.ts:
--------------------------------------------------------------------------------
1 | import AppConfig from '@configs/app.config';
2 | import AuthConfig from '@configs/auth.config';
3 | import DatabaseConfig from '@configs/database.config';
4 | import AwsConfig from '@configs/aws.config';
5 | import UserConfig from '@configs/user.config';
6 | import RequestConfig from '@configs/request.config';
7 | import DocConfig from '@configs/doc.config';
8 | import MessageConfig from '@configs/message.config';
9 | import EmailConfig from '@configs/email.config';
10 | import RedisConfig from '@configs/redis.config';
11 | import ForgotPasswordConfig from '@configs/forgot-password.config';
12 | import VerificationConfig from '@configs/verification.config';
13 | import HomeConfig from '@configs/home.config';
14 | import LoggerConfig from '@configs/logger.config';
15 | import SessionConfig from '@configs/session.config';
16 | import TermPolicyConfig from '@configs/term-policy.config';
17 | import FeatureFlagConfig from '@configs/feature-flag.config';
18 | import ResponseConfig from '@configs/response.config';
19 |
20 | export default [
21 | AppConfig,
22 | AuthConfig,
23 | DatabaseConfig,
24 | AwsConfig,
25 | UserConfig,
26 | RequestConfig,
27 | DocConfig,
28 | MessageConfig,
29 | EmailConfig,
30 | RedisConfig,
31 | LoggerConfig,
32 | ForgotPasswordConfig,
33 | VerificationConfig,
34 | HomeConfig,
35 | SessionConfig,
36 | TermPolicyConfig,
37 | FeatureFlagConfig,
38 | ResponseConfig,
39 | ];
40 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.change-password.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsPassword } from '@common/request/validations/request.is-password.validation';
2 | import { faker } from '@faker-js/faker';
3 | import { UserLoginVerifyTwoFactorRequestDto } from '@modules/user/dtos/request/user.login-verify-two-factor.request.dto';
4 | import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger';
5 | import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
6 |
7 | export class UserChangePasswordRequestDto extends PartialType(
8 | OmitType(UserLoginVerifyTwoFactorRequestDto, ['challengeToken'])
9 | ) {
10 | @ApiProperty({
11 | description:
12 | "new string password, newPassword can't same with oldPassword",
13 | example: `${faker.string.alphanumeric(5).toLowerCase()}${faker.string
14 | .alphanumeric(5)
15 | .toUpperCase()}@@!123`,
16 | required: true,
17 | minLength: 8,
18 | maxLength: 50,
19 | })
20 | @IsNotEmpty()
21 | @IsString()
22 | @IsPassword()
23 | @MinLength(8)
24 | @MaxLength(50)
25 | newPassword: string;
26 |
27 | @ApiProperty({
28 | description: 'old string password',
29 | example: `${faker.string.alphanumeric(5).toLowerCase()}${faker.string
30 | .alphanumeric(5)
31 | .toUpperCase()}@@!123`,
32 | required: true,
33 | })
34 | @IsString()
35 | @IsNotEmpty()
36 | oldPassword: string;
37 | }
38 |
--------------------------------------------------------------------------------
/src/queues/bases/queue.processor.base.ts:
--------------------------------------------------------------------------------
1 | import { OnWorkerEvent, WorkerHost } from '@nestjs/bullmq';
2 | import * as Sentry from '@sentry/nestjs';
3 | import { Job } from 'bullmq';
4 | import { QueueException } from 'src/queues/exceptions/queue.exception';
5 |
6 | /**
7 | * Base class for queue processors that extends WorkerHost functionality.
8 | * Provides common error handling and Sentry integration for failed jobs.
9 | */
10 | export abstract class QueueProcessorBase extends WorkerHost {
11 | /**
12 | * Handles failed job events and reports fatal errors to Sentry.
13 | * Only reports to Sentry on the last attempt for fatal errors.
14 | *
15 | * @param {Job | undefined} job - The failed job instance
16 | * @param {Error} error - The error that caused the job to fail
17 | * @returns {void}
18 | */
19 | @OnWorkerEvent('failed')
20 | onFailed(job: Job | undefined, error: Error): void {
21 | const maxAttempts = job.opts.attempts ?? 1;
22 | const isLastAttempt = job.attemptsMade >= maxAttempts - 1;
23 |
24 | if (isLastAttempt) {
25 | let isFatal = true;
26 |
27 | if (error instanceof QueueException) {
28 | isFatal = !!error.isFatal;
29 | }
30 |
31 | if (isFatal) {
32 | try {
33 | Sentry.captureException(error);
34 | } catch (_) {}
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Build dependencies (keep node_modules for package.json but exclude if exists)
2 | node_modules/
3 | coverage/
4 | dist/
5 | .husky/
6 | .github/
7 | assets/
8 | generated/
9 | swagger.json
10 | src/metadata.ts
11 | keys/*
12 | .warmup/
13 | /.nyc_output
14 |
15 | # Logs
16 | logs/
17 | *.log
18 | npm-debug.log*
19 | pnpm-debug.log*
20 | lerna-debug.log*
21 |
22 | # Environment (contains sensitive data - but keep .env.example)
23 | .env*
24 | !.env.example
25 | config.yaml
26 | config.yml
27 |
28 | # OS files
29 | .DS_Store
30 |
31 | # Versioning and metadata
32 | .git
33 | .gitignore
34 | .dockerignore
35 |
36 | # IDE and editors
37 | /.idea
38 | .project
39 | .classpath
40 | .c9/
41 | *.launch
42 | .settings/
43 | *.sublime-workspace
44 | .vscode/*
45 | !.vscode/settings.json
46 | !.vscode/tasks.json
47 | !.vscode/launch.json
48 | !.vscode/extensions.json
49 |
50 | # pnpm
51 | .pnpm-store/
52 |
53 | # Files not required for Docker build
54 | .editorconfig
55 | cspell.json
56 | README.md
57 | LICENSE.md
58 | nodemon.json
59 |
60 | # CI configuration (except ci/dockerfile which is used for production build)
61 | ci/jwks-server/
62 |
63 | # Keep these files as they are needed for production build:
64 | # - ci/dockerfile (used by GitHub Actions)
65 | # - eslint.config.mjs (might be needed for linting)
66 | # - nest-cli.json (needed for NestJS CLI commands)
67 | # - tsconfig.json (needed for TypeScript compilation)
68 | # - test/ (keep for testing capabilities)
69 | # - scripts/ (might contain build scripts)
--------------------------------------------------------------------------------
/src/modules/feature-flag/interfaces/feature-flag.service.interface.ts:
--------------------------------------------------------------------------------
1 | import { IPaginationQueryOffsetParams } from '@common/pagination/interfaces/pagination.interface';
2 | import { IRequestApp } from '@common/request/interfaces/request.interface';
3 | import {
4 | IResponsePagingReturn,
5 | IResponseReturn,
6 | } from '@common/response/interfaces/response.interface';
7 | import { FeatureFlagUpdateMetadataRequestDto } from '@modules/feature-flag/dtos/request/feature-flag.update-metadata.request';
8 | import { FeatureFlagUpdateStatusRequestDto } from '@modules/feature-flag/dtos/request/feature-flag.update-status.request';
9 | import { FeatureFlagResponseDto } from '@modules/feature-flag/dtos/response/feature-flag.response';
10 | import { FeatureFlag } from '@prisma/client';
11 |
12 | export interface IFeatureFlagService {
13 | validateFeatureFlagGuard(request: IRequestApp, key: string): Promise;
14 | findOneByKeyAndCache(key: string): Promise;
15 | findOneMetadataByKeyAndCache(key: string): Promise;
16 | getList(
17 | pagination: IPaginationQueryOffsetParams
18 | ): Promise>;
19 | updateStatus(
20 | id: string,
21 | data: FeatureFlagUpdateStatusRequestDto
22 | ): Promise>;
23 | updateMetadata(
24 | id: string,
25 | data: FeatureFlagUpdateMetadataRequestDto
26 | ): Promise>;
27 | }
28 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/request/user.create.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiProperty } from '@nestjs/swagger';
3 | import {
4 | IsMongoId,
5 | IsNotEmpty,
6 | IsOptional,
7 | IsString,
8 | MaxLength,
9 | MinLength,
10 | } from 'class-validator';
11 | import { IsCustomEmail } from '@common/request/validations/request.custom-email.validation';
12 | import { Transform } from 'class-transformer';
13 |
14 | export class UserCreateRequestDto {
15 | @ApiProperty({
16 | example: faker.internet.email(),
17 | required: true,
18 | maxLength: 100,
19 | })
20 | @IsCustomEmail()
21 | @IsNotEmpty()
22 | @MaxLength(100)
23 | @Transform(({ value }) => value.toLowerCase().trim())
24 | email: Lowercase;
25 |
26 | @ApiProperty({
27 | example: faker.database.mongodbObjectId(),
28 | required: true,
29 | })
30 | @IsString()
31 | @IsNotEmpty()
32 | @IsMongoId()
33 | roleId: string;
34 |
35 | @ApiProperty({
36 | example: faker.person.fullName(),
37 | required: false,
38 | maxLength: 100,
39 | minLength: 1,
40 | })
41 | @IsString()
42 | @IsOptional()
43 | @MinLength(1)
44 | @MaxLength(100)
45 | name?: string;
46 |
47 | @ApiProperty({
48 | example: faker.database.mongodbObjectId(),
49 | required: true,
50 | })
51 | @IsString()
52 | @IsNotEmpty()
53 | @IsMongoId()
54 | countryId: string;
55 | }
56 |
--------------------------------------------------------------------------------
/src/modules/user/constants/user.doc.constant.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiParamOptions, ApiQueryOptions } from '@nestjs/swagger';
3 | import { EnumUserStatus } from '@prisma/client';
4 |
5 | export const UserDocParamsId: ApiParamOptions[] = [
6 | {
7 | name: 'userId',
8 | allowEmptyValue: false,
9 | required: true,
10 | type: 'string',
11 | example: faker.database.mongodbObjectId(),
12 | },
13 | ];
14 |
15 | export const UserDocParamsMobileNumberId: ApiParamOptions[] = [
16 | {
17 | name: 'mobileNumberId',
18 | allowEmptyValue: false,
19 | required: true,
20 | type: 'string',
21 | example: faker.database.mongodbObjectId(),
22 | },
23 | ];
24 |
25 | export const UserDocQueryList: ApiQueryOptions[] = [
26 | {
27 | name: 'roleId',
28 | allowEmptyValue: true,
29 | required: false,
30 | type: 'string',
31 | example: faker.database.mongodbObjectId(),
32 | description: 'Filter by roleId',
33 | },
34 | {
35 | name: 'countryId',
36 | allowEmptyValue: true,
37 | required: false,
38 | type: 'string',
39 | example: faker.database.mongodbObjectId(),
40 | },
41 | {
42 | name: 'status',
43 | allowEmptyValue: true,
44 | required: false,
45 | type: 'string',
46 | example: Object.values(EnumUserStatus).join(','),
47 | description: "value with ',' delimiter",
48 | },
49 | ];
50 |
--------------------------------------------------------------------------------
/src/modules/session/docs/session.shared.doc.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import {
3 | Doc,
4 | DocAuth,
5 | DocGuard,
6 | DocRequest,
7 | DocResponse,
8 | DocResponsePaging,
9 | } from '@common/doc/decorators/doc.decorator';
10 | import { SessionResponseDto } from '@modules/session/dtos/response/session.response.dto';
11 | import { SessionDocParamsId } from '@modules/session/constants/session.doc.constant';
12 | import { EnumPaginationType } from '@common/pagination/enums/pagination.enum';
13 |
14 | export function SessionSharedListDoc(): MethodDecorator {
15 | return applyDecorators(
16 | Doc({
17 | summary: 'get all user Sessions',
18 | }),
19 | DocAuth({
20 | xApiKey: true,
21 | jwtAccessToken: true,
22 | }),
23 | DocGuard({ termPolicy: true }),
24 | DocResponsePaging('session.list', {
25 | dto: SessionResponseDto,
26 | type: EnumPaginationType.cursor,
27 | })
28 | );
29 | }
30 |
31 | export function SessionSharedRevokeDoc(): MethodDecorator {
32 | return applyDecorators(
33 | Doc({
34 | summary: 'revoke user Session',
35 | }),
36 | DocRequest({
37 | params: SessionDocParamsId,
38 | }),
39 | DocAuth({
40 | xApiKey: true,
41 | jwtAccessToken: true,
42 | }),
43 | DocGuard({ termPolicy: true }),
44 | DocResponse('session.revoke')
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/languages/en/termPolicy.json:
--------------------------------------------------------------------------------
1 | {
2 | "list": "Terms and policies retrieved successfully.",
3 | "listAccepted": "Accepted terms and policies retrieved successfully.",
4 | "get": "Term policy details fetched successfully.",
5 | "getContent": "Term policy content retrieved successfully.",
6 | "create": "New term policy created successfully.",
7 | "update": "Term policy updated successfully.",
8 | "updateContent": "Term policy content updated successfully.",
9 | "addContent": "Content added to term policy successfully.",
10 | "removeContent": "Content removed from term policy successfully.",
11 | "delete": "Term policy deleted successfully.",
12 | "publish": "Term policy published successfully.",
13 | "accept": "Term policy accepted successfully.",
14 | "generateContentPresign": "Presigned URL for content upload generated successfully.",
15 | "error": {
16 | "notFound": "Term policy not found.",
17 | "exist": "A term policy with this name already exists.",
18 | "alreadyAccepted": "You have already accepted this term policy.",
19 | "contentNotFound": "Term policy content not found.",
20 | "contentExist": "This content already exists in the term policy.",
21 | "contentEmpty": "Term policy content cannot be empty.",
22 | "contentsLanguageMustBeUnique": "Each language can only be used once in term policy contents.",
23 | "requiredInvalid": "Required field value is invalid.",
24 | "statusInvalid": "Term policy status is invalid."
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/modules/role/dtos/request/role.update.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty, getSchemaPath } from '@nestjs/swagger';
2 | import { faker } from '@faker-js/faker';
3 | import {
4 | IsArray,
5 | IsEnum,
6 | IsNotEmpty,
7 | IsOptional,
8 | IsString,
9 | MaxLength,
10 | ValidateNested,
11 | } from 'class-validator';
12 | import { Type } from 'class-transformer';
13 | import { RoleAbilityRequestDto } from '@modules/role/dtos/request/role.ability.request.dto';
14 | import { EnumRoleType } from '@prisma/client';
15 |
16 | export class RoleUpdateRequestDto {
17 | @ApiProperty({
18 | description: 'Description of role',
19 | example: faker.lorem.sentence(),
20 | required: false,
21 | maxLength: 500,
22 | })
23 | @IsString()
24 | @IsOptional()
25 | @MaxLength(500)
26 | description?: string;
27 |
28 | @ApiProperty({
29 | description: 'Representative for role type',
30 | example: EnumRoleType.admin,
31 | required: true,
32 | enum: EnumRoleType,
33 | })
34 | @IsEnum(EnumRoleType)
35 | @IsNotEmpty()
36 | type: EnumRoleType;
37 |
38 | @ApiProperty({
39 | required: true,
40 | description: 'Ability list of role',
41 | isArray: true,
42 | type: RoleAbilityRequestDto,
43 | oneOf: [{ $ref: getSchemaPath(RoleAbilityRequestDto) }],
44 | })
45 | @Type(() => RoleAbilityRequestDto)
46 | @IsNotEmpty()
47 | @ValidateNested()
48 | @IsArray()
49 | abilities: RoleAbilityRequestDto[];
50 | }
51 |
--------------------------------------------------------------------------------
/src/modules/role/interfaces/role.service.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IPaginationIn,
3 | IPaginationQueryOffsetParams,
4 | } from '@common/pagination/interfaces/pagination.interface';
5 | import { IRequestApp } from '@common/request/interfaces/request.interface';
6 | import {
7 | IResponsePagingReturn,
8 | IResponseReturn,
9 | } from '@common/response/interfaces/response.interface';
10 | import { RoleCreateRequestDto } from '@modules/role/dtos/request/role.create.request.dto';
11 | import { RoleUpdateRequestDto } from '@modules/role/dtos/request/role.update.request.dto';
12 | import { RoleListResponseDto } from '@modules/role/dtos/response/role.list.response.dto';
13 | import { RoleAbilityDto } from '@modules/role/dtos/role.ability.dto';
14 | import { RoleDto } from '@modules/role/dtos/role.dto';
15 | import { EnumRoleType } from '@prisma/client';
16 |
17 | export interface IRoleService {
18 | getList(
19 | { where, ...params }: IPaginationQueryOffsetParams,
20 | type?: Record
21 | ): Promise>;
22 | getOne(id: string): Promise>;
23 | create(data: RoleCreateRequestDto): Promise>;
24 | update(
25 | id: string,
26 | data: RoleUpdateRequestDto
27 | ): Promise>;
28 | delete(id: string): Promise>;
29 | validateRoleGuard(
30 | request: IRequestApp,
31 | requiredRoles: EnumRoleType[]
32 | ): Promise;
33 | }
34 |
--------------------------------------------------------------------------------
/src/modules/session/dtos/response/session.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { DatabaseDto } from '@common/database/dtos/database.dto';
2 | import { RequestUserAgentDto } from '@common/request/dtos/request.user-agent.dto';
3 | import { faker } from '@faker-js/faker';
4 | import { UserListResponseDto } from '@modules/user/dtos/response/user.list.response.dto';
5 | import { ApiProperty } from '@nestjs/swagger';
6 | import { Type } from 'class-transformer';
7 |
8 | export class SessionResponseDto extends DatabaseDto {
9 | @ApiProperty({
10 | required: true,
11 | example: faker.database.mongodbObjectId(),
12 | })
13 | userId: string;
14 |
15 | @ApiProperty({
16 | required: true,
17 | type: UserListResponseDto,
18 | })
19 | @Type(() => UserListResponseDto)
20 | user: UserListResponseDto;
21 |
22 | @ApiProperty({
23 | required: true,
24 | example: faker.internet.ipv4(),
25 | })
26 | ipAddress: string;
27 |
28 | @ApiProperty({
29 | required: true,
30 | type: RequestUserAgentDto,
31 | })
32 | @Type(() => RequestUserAgentDto)
33 | userAgent: RequestUserAgentDto;
34 |
35 | @ApiProperty({
36 | required: true,
37 | example: faker.date.future(),
38 | })
39 | expiredAt: Date;
40 |
41 | @ApiProperty({
42 | required: false,
43 | example: faker.date.future(),
44 | })
45 | revokedAt?: Date;
46 |
47 | @ApiProperty({
48 | required: true,
49 | example: false,
50 | })
51 | isRevoked: boolean;
52 | }
53 |
--------------------------------------------------------------------------------
/src/modules/api-key/dtos/api-key.dto.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
3 | import { Exclude } from 'class-transformer';
4 | import { DatabaseDto } from '@common/database/dtos/database.dto';
5 | import { EnumApiKeyType } from '@prisma/client';
6 |
7 | export class ApiKeyDto extends DatabaseDto {
8 | @ApiHideProperty()
9 | @Exclude()
10 | hash: string;
11 |
12 | @ApiProperty({
13 | description: 'Active flag of api key',
14 | example: true,
15 | required: true,
16 | })
17 | isActive: boolean;
18 |
19 | @ApiProperty({
20 | description: 'Api Key start date',
21 | example: faker.date.past(),
22 | required: false,
23 | })
24 | startDate?: Date;
25 |
26 | @ApiProperty({
27 | description: 'Api Key end date',
28 | example: faker.date.future(),
29 | required: false,
30 | })
31 | endDate?: Date;
32 |
33 | @ApiProperty({
34 | description: 'Name of api key',
35 | example: faker.string.alpha(10),
36 | required: false,
37 | })
38 | name?: string;
39 |
40 | @ApiProperty({
41 | description: 'Type of api key',
42 | example: EnumApiKeyType.default,
43 | enum: EnumApiKeyType,
44 | required: true,
45 | })
46 | type: EnumApiKeyType;
47 |
48 | @ApiProperty({
49 | description: 'Unique key of api key',
50 | example: faker.string.alpha(15),
51 | required: true,
52 | })
53 | key: string;
54 | }
55 |
--------------------------------------------------------------------------------
/src/configs/app.config.ts:
--------------------------------------------------------------------------------
1 | import { EnumAppEnvironment } from '@app/enums/app.enum';
2 | import { registerAs } from '@nestjs/config';
3 | import { author, repository, version } from 'package.json';
4 |
5 | export interface IConfigApp {
6 | name: string;
7 | env: EnumAppEnvironment;
8 | timezone: string;
9 | version: string;
10 | author: {
11 | name: string;
12 | email: string;
13 | };
14 | url: string;
15 | globalPrefix: string;
16 | http: {
17 | host: string;
18 | port: number;
19 | };
20 | urlVersion: {
21 | enable: boolean;
22 | prefix: string;
23 | version: string;
24 | };
25 | }
26 |
27 | export default registerAs(
28 | 'app',
29 | (): IConfigApp => ({
30 | name: process.env.APP_NAME ?? 'ACKNestJs',
31 | env:
32 | EnumAppEnvironment[process.env.APP_ENV] ?? EnumAppEnvironment.local,
33 | timezone: process.env.APP_TIMEZONE ?? 'Asia/Jakarta',
34 | version,
35 | author: author as {
36 | name: string;
37 | email: string;
38 | },
39 | url: repository.url,
40 | globalPrefix: '/api',
41 |
42 | http: {
43 | host: process.env.HTTP_HOST ?? 'localhost',
44 | port: process.env.HTTP_PORT ? +process.env.HTTP_PORT : 3000,
45 | },
46 | urlVersion: {
47 | enable: process.env.URL_VERSIONING_ENABLE === 'true',
48 | prefix: 'v',
49 | version: process.env.URL_VERSION ?? '1',
50 | },
51 | })
52 | );
53 |
--------------------------------------------------------------------------------
/src/modules/user/dtos/response/user.list.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiHideProperty } from '@nestjs/swagger';
2 | import { Exclude } from 'class-transformer';
3 | import { UserDto } from '@modules/user/dtos/user.dto';
4 | import {
5 | EnumUserGender,
6 | EnumUserLoginFrom,
7 | EnumUserLoginWith,
8 | EnumUserSignUpFrom,
9 | EnumUserSignUpWith,
10 | } from '@prisma/client';
11 | import { UserTwoFactorDto } from '@modules/user/dtos/user.two-factor.dto';
12 |
13 | export class UserListResponseDto extends UserDto {
14 | @ApiHideProperty()
15 | @Exclude()
16 | passwordExpired?: Date;
17 |
18 | @ApiHideProperty()
19 | @Exclude()
20 | passwordCreated?: Date;
21 |
22 | @ApiHideProperty()
23 | @Exclude()
24 | passwordAttempt?: number;
25 |
26 | @ApiHideProperty()
27 | @Exclude()
28 | signUpDate: Date;
29 |
30 | @ApiHideProperty()
31 | @Exclude()
32 | signUpFrom: EnumUserSignUpFrom;
33 |
34 | @ApiHideProperty()
35 | @Exclude()
36 | signUpWith: EnumUserSignUpWith;
37 |
38 | @ApiHideProperty()
39 | @Exclude()
40 | gender?: EnumUserGender;
41 |
42 | @ApiHideProperty()
43 | @Exclude()
44 | lastLoginAt?: Date;
45 |
46 | @ApiHideProperty()
47 | @Exclude()
48 | lastIPAddress?: string;
49 |
50 | @ApiHideProperty()
51 | @Exclude()
52 | lastLoginFrom?: EnumUserLoginFrom;
53 |
54 | @ApiHideProperty()
55 | @Exclude()
56 | lastLoginWith?: EnumUserLoginWith;
57 |
58 | @ApiHideProperty()
59 | @Exclude()
60 | twoFactor: UserTwoFactorDto;
61 | }
62 |
--------------------------------------------------------------------------------