├── .npmrc ├── keys └── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── src ├── languages │ └── en │ │ ├── hello.json │ │ ├── activityLog.json │ │ ├── passwordHistory.json │ │ ├── app.json │ │ ├── country.json │ │ ├── policy.json │ │ ├── file.json │ │ ├── session.json │ │ ├── health.json │ │ ├── role.json │ │ ├── featureFlag.json │ │ ├── apiKey.json │ │ └── termPolicy.json ├── common │ ├── cache │ │ └── constants │ │ │ └── cache.constant.ts │ ├── message │ │ ├── enums │ │ │ └── message.enum.ts │ │ └── interfaces │ │ │ ├── message.interface.ts │ │ │ └── message.service.interface.ts │ ├── redis │ │ └── constants │ │ │ └── redis.constant.ts │ ├── request │ │ ├── enums │ │ │ ├── request.enum.ts │ │ │ └── request.status-code.enum.ts │ │ ├── constants │ │ │ └── request.constant.ts │ │ ├── exceptions │ │ │ ├── request.validation.exception.ts │ │ │ └── request.too-many.exception.ts │ │ ├── middlewares │ │ │ ├── request.helmet.middleware.ts │ │ │ ├── request.response-time.middleware.ts │ │ │ └── request.compression.middleware.ts │ │ ├── interfaces │ │ │ └── request.interface.ts │ │ └── pipes │ │ │ └── request.required.pipe.ts │ ├── helper │ │ ├── enums │ │ │ └── helper.enum.ts │ │ ├── interfaces │ │ │ └── helper.interface.ts │ │ └── helper.module.ts │ ├── response │ │ ├── constants │ │ │ └── response.constant.ts │ │ └── dtos │ │ │ └── response.error.dto.ts │ ├── aws │ │ ├── enums │ │ │ └── aws.enum.ts │ │ ├── constants │ │ │ └── aws.constant.ts │ │ └── aws.module.ts │ ├── pagination │ │ ├── enums │ │ │ ├── pagination.status-code.enum.ts │ │ │ └── pagination.enum.ts │ │ ├── constants │ │ │ └── pagination.constant.ts │ │ ├── pagination.module.ts │ │ └── interfaces │ │ │ └── pagination.service.interface.ts │ ├── file │ │ ├── enums │ │ │ ├── file.status-code.enum.ts │ │ │ └── file.enum.ts │ │ ├── constants │ │ │ └── file.constant.ts │ │ ├── file.module.ts │ │ ├── dtos │ │ │ ├── file.single.dto.ts │ │ │ └── file.multiple.dto.ts │ │ ├── interfaces │ │ │ ├── file.service.interface.ts │ │ │ └── file.interface.ts │ │ └── exceptions │ │ │ └── file.import.exception.ts │ ├── logger │ │ ├── interfaces │ │ │ └── logger.interface.ts │ │ ├── enums │ │ │ └── logger.enum.ts │ │ └── logger.module.ts │ ├── doc │ │ └── enums │ │ │ └── doc.enum.ts │ └── database │ │ ├── interfaces │ │ └── database.service.interface.ts │ │ ├── dtos │ │ ├── database.id.dto.ts │ │ └── database.dto.ts │ │ └── database.module.ts ├── app │ ├── enums │ │ ├── app.status-code.enum.ts │ │ └── app.enum.ts │ └── interfaces │ │ └── app.interface.ts ├── modules │ ├── role │ │ ├── constants │ │ │ ├── role.constant.ts │ │ │ ├── role.list.constant.ts │ │ │ └── role.doc.constant.ts │ │ ├── enums │ │ │ └── role.status-code.enum.ts │ │ ├── role.module.ts │ │ ├── dtos │ │ │ ├── response │ │ │ │ └── role.list.response.dto.ts │ │ │ ├── role.ability.dto.ts │ │ │ ├── request │ │ │ │ ├── role.create.request.dto.ts │ │ │ │ ├── role.ability.request.dto.ts │ │ │ │ └── role.update.request.dto.ts │ │ │ └── role.dto.ts │ │ ├── decorators │ │ │ └── role.decorator.ts │ │ ├── utils │ │ │ └── role.util.ts │ │ ├── docs │ │ │ └── role.public.doc.ts │ │ └── interfaces │ │ │ └── role.service.interface.ts │ ├── api-key │ │ ├── constants │ │ │ ├── api-key.constant.ts │ │ │ ├── api-key.list.constant.ts │ │ │ └── api-key.doc.constant.ts │ │ ├── interfaces │ │ │ └── api-key.interface.ts │ │ ├── enums │ │ │ └── api-key.status-code.enum.ts │ │ ├── dtos │ │ │ ├── request │ │ │ │ ├── api-key.update-status.request.dto.ts │ │ │ │ ├── api-key.update.request.dto.ts │ │ │ │ └── api-key.update-date.request.dto.ts │ │ │ ├── response │ │ │ │ └── api-key.create.response.dto.ts │ │ │ └── api-key.dto.ts │ │ ├── api-key.module.ts │ │ └── guards │ │ │ └── x-api-key │ │ │ └── api-key.x-api-key.guard.ts │ ├── session │ │ ├── constants │ │ │ ├── session.constant.ts │ │ │ ├── session.list.constant.ts │ │ │ └── session.doc.constant.ts │ │ ├── enums │ │ │ └── session.status-code.enum.ts │ │ ├── interfaces │ │ │ ├── session.interface.ts │ │ │ └── session.service.interface.ts │ │ ├── docs │ │ │ └── session.shared.doc.ts │ │ └── dtos │ │ │ └── response │ │ │ └── session.response.dto.ts │ ├── term-policy │ │ ├── templates │ │ │ ├── term-policy.cookies.en.hbs │ │ │ ├── term-policy.marketing.en.hbs │ │ │ ├── term-policy.privacy.en.hbs │ │ │ └── term-policy.term.en.hbs │ │ ├── constants │ │ │ ├── term-policy.constant.ts │ │ │ └── term-policy.list.constant.ts │ │ ├── enums │ │ │ └── term-policy.status-code.enum.ts │ │ ├── interfaces │ │ │ ├── term-policy.interface.ts │ │ │ └── term-policy.template-service.interface.ts │ │ ├── dtos │ │ │ ├── request │ │ │ │ ├── term-policy.remove-content.request.dto.ts │ │ │ │ ├── term-policy.accept.request.dto.ts │ │ │ │ ├── term-policy.create.request.dto.ts │ │ │ │ ├── term-policy.content-presign.request.dto.ts │ │ │ │ └── term-policy.content.request.dto.ts │ │ │ └── term-policy.content.dto.ts │ │ ├── term-policy.module.ts │ │ ├── decorators │ │ │ └── term-policy.decorator.ts │ │ └── docs │ │ │ └── term-policy.public.doc.ts │ ├── user │ │ ├── constants │ │ │ ├── user.constant.ts │ │ │ ├── user.list.constant.ts │ │ │ └── user.doc.constant.ts │ │ ├── dtos │ │ │ ├── request │ │ │ │ ├── user.generate-import.request.dto.ts │ │ │ │ ├── user.forgot-password.request.dto.ts │ │ │ │ ├── user.send-email-verification.request.dto.ts │ │ │ │ ├── user.two-factor-disable.request.dto.ts │ │ │ │ ├── user.verify-email.request.dto.ts │ │ │ │ ├── user.check.request.dto.ts │ │ │ │ ├── user.create-social.request.dto.ts │ │ │ │ ├── user.update-status.request.dto.ts │ │ │ │ ├── user.login-enable-two-factor.request.dto.ts │ │ │ │ ├── user.two-factor-enable.request.dto.ts │ │ │ │ ├── user.import.request.dto.ts │ │ │ │ ├── user.claim-username.request.dto.ts │ │ │ │ ├── user.generate-photo-profile.request.dto.ts │ │ │ │ ├── user.forgot-password-reset.request.dto.ts │ │ │ │ ├── user.profile.request.dto.ts │ │ │ │ ├── user.mobile-number.request.dto.ts │ │ │ │ ├── user.login.request.dto.ts │ │ │ │ ├── user.change-password.request.dto.ts │ │ │ │ └── user.create.request.dto.ts │ │ │ ├── response │ │ │ │ ├── user.two-factor-enable.response.dto.ts │ │ │ │ ├── user.check.response.dto.ts │ │ │ │ ├── user.two-factor-setup.response.dto.ts │ │ │ │ ├── user.login.response.dto.ts │ │ │ │ ├── user.two-factor.response.dto.ts │ │ │ │ ├── user.profile.response.dto.ts │ │ │ │ ├── user.two-factor-status.response.dto.ts │ │ │ │ └── user.list.response.dto.ts │ │ │ ├── user.term-policy.dto.ts │ │ │ ├── user.mobile-number.dto.ts │ │ │ └── user.two-factor.dto.ts │ │ ├── docs │ │ │ └── user.user.doc.ts │ │ ├── enums │ │ │ └── user.status-code.enum.ts │ │ ├── user.module.ts │ │ └── interfaces │ │ │ └── user.interface.ts │ ├── feature-flag │ │ ├── constants │ │ │ ├── feature-flag.list.constant.ts │ │ │ ├── feature-flag.constant.ts │ │ │ └── feature-flag.doc.ts │ │ ├── interfaces │ │ │ ├── feature-flag.interface.ts │ │ │ └── feature-flag.service.interface.ts │ │ ├── enums │ │ │ └── feature-flag.status-code.enum.ts │ │ ├── feature-flag.module.ts │ │ ├── dtos │ │ │ ├── response │ │ │ │ └── feature-flag.response.ts │ │ │ └── request │ │ │ │ ├── feature-flag.update-status.request.ts │ │ │ │ └── feature-flag.update-metadata.request.ts │ │ └── decorators │ │ │ └── feature-flag.decorator.ts │ ├── policy │ │ ├── constants │ │ │ └── policy.constant.ts │ │ ├── enums │ │ │ ├── policy.status-code.enum.ts │ │ │ └── policy.enum.ts │ │ ├── interfaces │ │ │ ├── policy.interface.ts │ │ │ └── policy.service.interface.ts │ │ ├── policy.module.ts │ │ └── decorators │ │ │ └── policy.decorator.ts │ ├── auth │ │ ├── enums │ │ │ ├── auth.enum.ts │ │ │ └── auth.status-code.enum.ts │ │ ├── constants │ │ │ └── auth.constant.ts │ │ ├── dtos │ │ │ └── response │ │ │ │ └── auth.token.response.dto.ts │ │ ├── decorators │ │ │ └── auth.social.decorator.ts │ │ └── guards │ │ │ └── social │ │ │ ├── auth.social.google.guard.ts │ │ │ └── auth.social.apple.guard.ts │ ├── country │ │ ├── constants │ │ │ └── country.list.constant.ts │ │ ├── enums │ │ │ └── country.status-code.enum.ts │ │ ├── interfaces │ │ │ └── country.service.interface.ts │ │ ├── country.module.ts │ │ ├── utils │ │ │ └── country.util.ts │ │ ├── docs │ │ │ └── country.public.doc.ts │ │ ├── services │ │ │ └── country.service.ts │ │ └── repositories │ │ │ └── country.repository.ts │ ├── activity-log │ │ ├── constants │ │ │ └── activity-log.constant.ts │ │ ├── interfaces │ │ │ ├── activity-log.interface.ts │ │ │ └── activity-log.service.interface.ts │ │ ├── utils │ │ │ └── activity-log.util.ts │ │ ├── activity-log.module.ts │ │ ├── docs │ │ │ ├── activity-log.shared.doc.ts │ │ │ └── activity-log.admin.doc.ts │ │ └── decorators │ │ │ └── activity-log.decorator.ts │ ├── email │ │ ├── dtos │ │ │ ├── email.create-by-admin.dto.ts │ │ │ ├── email.forgot-password.dto.ts │ │ │ ├── email.verified.dto.ts │ │ │ ├── email.mobile-number-verified.dto.ts │ │ │ ├── email.temp-password.dto.ts │ │ │ ├── email.send.dto.ts │ │ │ ├── email.verification.dto.ts │ │ │ └── email.worker.dto.ts │ │ ├── templates │ │ │ ├── email.change-password.template.hbs │ │ │ ├── email.reset-two-factor-by-admin.template.hbs │ │ │ ├── email.welcome.template.hbs │ │ │ ├── email.verified.template.hbs │ │ │ ├── email.mobile-number-verified.template.hbs │ │ │ ├── email.forgot-password.template.hbs │ │ │ ├── email.temp-password.template.hbs │ │ │ ├── email.verification.template.hbs │ │ │ └── email.create-by-admin.template.hbs │ │ ├── enums │ │ │ └── email.enum.ts │ │ └── email.module.ts │ ├── password-history │ │ ├── interfaces │ │ │ ├── password-history.interface.ts │ │ │ └── password-history.service.interface.ts │ │ ├── utils │ │ │ └── password-history.util.ts │ │ ├── password-history.module.ts │ │ ├── docs │ │ │ ├── password-history.shared.doc.ts │ │ │ └── password-history.admin.doc.ts │ │ └── dtos │ │ │ └── response │ │ │ └── password-history.response.dto.ts │ ├── hello │ │ ├── hello.module.ts │ │ ├── interfaces │ │ │ └── hello.service.interface.ts │ │ ├── docs │ │ │ └── hello.public.doc.ts │ │ └── controllers │ │ │ └── hello.public.controller.ts │ └── health │ │ ├── dtos │ │ └── response │ │ │ ├── health.sentry.response.dto.ts │ │ │ └── health.database.response.dto.ts │ │ ├── indicators │ │ ├── health.redis.indicator.ts │ │ ├── health.database.indicator.ts │ │ ├── health.aws-s3.indicator.ts │ │ ├── health.aws-ses.indicator.ts │ │ └── health.sentry.indicator.ts │ │ └── health.module.ts ├── migration │ ├── enums │ │ └── migration.enum.ts │ ├── interfaces │ │ ├── migration.seed.interface.ts │ │ └── migration.interface.ts │ └── data │ │ ├── migration.country.data.ts │ │ ├── migration.api-key.data.ts │ │ ├── migration.user.data.ts │ │ └── migration.role.data.ts ├── queues │ ├── constants │ │ └── queue.constant.ts │ ├── enums │ │ └── queue.enum.ts │ ├── exceptions │ │ └── queue.exception.ts │ ├── queue.module.ts │ ├── decorators │ │ └── queue.decorator.ts │ └── bases │ │ └── queue.processor.base.ts ├── configs │ ├── response.config.ts │ ├── session.config.ts │ ├── feature-flag.config.ts │ ├── home.config.ts │ ├── email.config.ts │ ├── term-policy.config.ts │ ├── user.config.ts │ ├── database.config.ts │ ├── message.config.ts │ ├── doc.config.ts │ ├── forgot-password.config.ts │ ├── verification.config.ts │ ├── redis.config.ts │ ├── logger.config.ts │ ├── index.ts │ └── app.config.ts ├── migration.ts └── router │ └── routes │ ├── routes.user.module.ts │ ├── routes.system.module.ts │ └── routes.public.module.ts ├── .prettierrc ├── pnpm-workspace.yaml ├── dockerfile ├── .vscode ├── launch.json ├── extensions.json └── settings.json ├── .github ├── dependabot.yml └── workflows │ ├── linter.yml │ ├── test.yml │ └── release-version.yml ├── .commitlintrc ├── .gitignore ├── LICENSE.md ├── nest-cli.json ├── ci └── dockerfile ├── .swcrc └── .dockerignore /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict = true -------------------------------------------------------------------------------- /keys/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !.gitignore -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit "$1" -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/**/templates/* 2 | generated/ 3 | swagger.json 4 | src/metadata.ts -------------------------------------------------------------------------------- /src/languages/en/hello.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "Hello! Welcome to our service." 3 | } 4 | -------------------------------------------------------------------------------- /src/common/cache/constants/cache.constant.ts: -------------------------------------------------------------------------------- 1 | export const CacheMainProvider = 'CacheMainProvider'; 2 | -------------------------------------------------------------------------------- /src/common/message/enums/message.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumMessageLanguage { 2 | en = 'en', 3 | } 4 | -------------------------------------------------------------------------------- /src/languages/en/activityLog.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": "Welcome! Here's your activity history." 3 | } 4 | -------------------------------------------------------------------------------- /src/app/enums/app.status-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumAppStatusCodeError { 2 | unknown = 5000, 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/role/constants/role.constant.ts: -------------------------------------------------------------------------------- 1 | export const RoleRequiredMetaKey = 'RoleRequiredMetaKey'; 2 | -------------------------------------------------------------------------------- /src/languages/en/passwordHistory.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": "Password histories retrieved successfully." 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/api-key/constants/api-key.constant.ts: -------------------------------------------------------------------------------- 1 | export const ApiKeyXTypeMetaKey = 'ApiKeyXTypeMetaKey'; 2 | -------------------------------------------------------------------------------- /src/modules/session/constants/session.constant.ts: -------------------------------------------------------------------------------- 1 | export const SessionCacheProvider = 'SessionCacheProvider'; 2 | -------------------------------------------------------------------------------- /src/modules/term-policy/templates/term-policy.cookies.en.hbs: -------------------------------------------------------------------------------- 1 |

Cookies version 1

-------------------------------------------------------------------------------- /src/common/redis/constants/redis.constant.ts: -------------------------------------------------------------------------------- 1 | export const RedisClientCachedProvider = 'RedisClientCachedProvider'; 2 | -------------------------------------------------------------------------------- /src/modules/term-policy/templates/term-policy.marketing.en.hbs: -------------------------------------------------------------------------------- 1 |

Marketing version 1

-------------------------------------------------------------------------------- /src/modules/term-policy/templates/term-policy.privacy.en.hbs: -------------------------------------------------------------------------------- 1 |

Privacy Policy version 1

-------------------------------------------------------------------------------- /src/modules/term-policy/templates/term-policy.term.en.hbs: -------------------------------------------------------------------------------- 1 |

Terms of Policy version 1

-------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm db:generate 2 | pnpm lint:staged 3 | pnpm deadcode 4 | pnpm spell 5 | NODE_ENV=test pnpm test 6 | -------------------------------------------------------------------------------- /src/common/request/enums/request.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumRequestTimezone { 2 | asiaJakarta = 'Asia/Jakarta', 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/user/constants/user.constant.ts: -------------------------------------------------------------------------------- 1 | export const UserGuardIsVerifiedMetaKey = 'UserGuardIsVerifiedMetaKey'; 2 | -------------------------------------------------------------------------------- /src/common/helper/enums/helper.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumHelperDateDayOf { 2 | start = 'start', 3 | end = 'end', 4 | } 5 | -------------------------------------------------------------------------------- /src/common/response/constants/response.constant.ts: -------------------------------------------------------------------------------- 1 | export const ResponseMessagePathMetaKey = 'ResponseMessagePathMetaKey'; 2 | -------------------------------------------------------------------------------- /src/modules/feature-flag/constants/feature-flag.list.constant.ts: -------------------------------------------------------------------------------- 1 | export const FeatureFlagDefaultAvailableSearch = ['key']; 2 | -------------------------------------------------------------------------------- /src/modules/policy/constants/policy.constant.ts: -------------------------------------------------------------------------------- 1 | export const PolicyRequiredAbilityMetaKey = 'PolicyRequiredAbilityMetaKey'; 2 | -------------------------------------------------------------------------------- /src/migration/enums/migration.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumMigrationType { 2 | seed = 'seed', 3 | remove = 'remove', 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/feature-flag/constants/feature-flag.constant.ts: -------------------------------------------------------------------------------- 1 | export const FeatureFlagKeyPathMetaKey = 'FeatureFlagKeyPathMetaKey'; 2 | -------------------------------------------------------------------------------- /src/modules/session/constants/session.list.constant.ts: -------------------------------------------------------------------------------- 1 | export const SessionDefaultAvailableOrderBy = ['createdAt', 'updatedAt']; 2 | -------------------------------------------------------------------------------- /src/common/aws/enums/aws.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumAwsS3Accessibility { 2 | public = 'public', 3 | private = 'private', 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/term-policy/constants/term-policy.constant.ts: -------------------------------------------------------------------------------- 1 | export const TermPolicyRequiredGuardMetaKey = 'TermPolicyRequiredMetaKey'; 2 | -------------------------------------------------------------------------------- /src/languages/en/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "swagger": "APIs Documentation for {appName} application." 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/auth/enums/auth.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumAuthTwoFactorMethod { 2 | code = 'code', 3 | backupCodes = 'backupCodes', 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/feature-flag/interfaces/feature-flag.interface.ts: -------------------------------------------------------------------------------- 1 | export type IFeatureFlagMetadata = Record; 2 | -------------------------------------------------------------------------------- /src/modules/auth/constants/auth.constant.ts: -------------------------------------------------------------------------------- 1 | export const AuthJwtAccessGuardKey = 'JwtAccess'; 2 | export const AuthJwtRefreshGuardKey = 'JwtRefresh'; 3 | -------------------------------------------------------------------------------- /src/modules/session/enums/session.status-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumSessionStatusCodeError { 2 | notFound = 5040, 3 | forbidden = 5041, 4 | } 5 | -------------------------------------------------------------------------------- /src/migration/interfaces/migration.seed.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IMigrationSeed { 2 | seed(): Promise; 3 | remove(): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/policy/enums/policy.status-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumPolicyStatusCodeError { 2 | forbidden = 5180, 3 | predefinedNotFound = 5182, 4 | } 5 | -------------------------------------------------------------------------------- /src/queues/constants/queue.constant.ts: -------------------------------------------------------------------------------- 1 | export const QueueConfigKey = 'QueueConfigKey'; 2 | export const QueueProcessorConfigKey = 'QueueProcessorConfigKey'; 3 | -------------------------------------------------------------------------------- /src/languages/en/country.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": "Countries retrieved successfully.", 3 | "error": { 4 | "notFound": "Country not found." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/api-key/interfaces/api-key.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IApiKeyGenerateCredential { 2 | key: string; 3 | secret: string; 4 | hash: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/aws/constants/aws.constant.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Maximum number of parts allowed for AWS S3 multipart upload operations. 3 | */ 4 | export const AwsS3MaxPartNumber = 10000; 5 | -------------------------------------------------------------------------------- /src/common/pagination/enums/pagination.status-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumPaginationStatusCodeError { 2 | orderByNotAllowed = 5020, 3 | filterInvalidValue = 5021, 4 | } 5 | -------------------------------------------------------------------------------- /src/common/file/enums/file.status-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumFileStatusCodeError { 2 | required = 5010, 3 | extensionInvalid = 5011, 4 | requiredExtractFirst = 5012, 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/country/constants/country.list.constant.ts: -------------------------------------------------------------------------------- 1 | export const CountryDefaultAvailableSearch = [ 2 | 'name', 3 | 'alpha2Code', 4 | 'alpha3Code', 5 | 'continent', 6 | ]; 7 | -------------------------------------------------------------------------------- /src/app/enums/app.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumAppEnvironment { 2 | production = 'production', 3 | staging = 'staging', 4 | development = 'development', 5 | local = 'local', 6 | } 7 | -------------------------------------------------------------------------------- /src/common/logger/interfaces/logger.interface.ts: -------------------------------------------------------------------------------- 1 | export interface LoggerDebugInfo { 2 | memory: { 3 | rss: number; 4 | heapUsed: number; 5 | }; 6 | uptime: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/country/enums/country.status-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumCountryStatusCodeError { 2 | notFound = 5140, 3 | isActive = 5141, 4 | inactive = 5142, 5 | exist = 5143, 6 | } 7 | -------------------------------------------------------------------------------- /src/queues/enums/queue.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumQueue { 2 | EMAIL = 'email', 3 | } 4 | 5 | export enum EnumQueuePriority { 6 | HIGH = 1, 7 | MEDIUM = 5, 8 | LOW = 10, 9 | } 10 | -------------------------------------------------------------------------------- /src/migration/interfaces/migration.interface.ts: -------------------------------------------------------------------------------- 1 | import { EnumMigrationType } from '@migration/enums/migration.enum'; 2 | 3 | export interface IMigrationOptions { 4 | type: EnumMigrationType; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/activity-log/constants/activity-log.constant.ts: -------------------------------------------------------------------------------- 1 | export const ActivityLogActionMetaKey = 'ActivityLogActionMetaKey'; 2 | export const ActivityLogMetadataMetaKey = 'ActivityLogMetadataMetaKey'; 3 | -------------------------------------------------------------------------------- /src/common/request/enums/request.status-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumRequestStatusCodeError { 2 | validation = 5030, 3 | timeout = 5031, 4 | paramRequired = 5032, 5 | envForbidden = 5034, 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/email/dtos/email.create-by-admin.dto.ts: -------------------------------------------------------------------------------- 1 | import { EmailTempPasswordDto } from '@modules/email/dtos/email.temp-password.dto'; 2 | 3 | export class EmailCreateByAdminDto extends EmailTempPasswordDto {} 4 | -------------------------------------------------------------------------------- /src/modules/email/dtos/email.forgot-password.dto.ts: -------------------------------------------------------------------------------- 1 | import { EmailVerificationDto } from '@modules/email/dtos/email.verification.dto'; 2 | 3 | export class EmailForgotPasswordDto extends EmailVerificationDto {} 4 | -------------------------------------------------------------------------------- /src/common/doc/enums/doc.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumDocRequestBodyType { 2 | json = 'json', 3 | formData = 'formData', 4 | formUrlencoded = 'formUrlencoded', 5 | text = 'text', 6 | none = 'none', 7 | } 8 | -------------------------------------------------------------------------------- /src/common/database/interfaces/database.service.interface.ts: -------------------------------------------------------------------------------- 1 | import { HealthIndicatorResult } from '@nestjs/terminus'; 2 | 3 | export interface IDatabaseService { 4 | isHealthy(): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/logger/enums/logger.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumLoggerLevel { 2 | error = 'error', 3 | warn = 'warn', 4 | info = 'info', 5 | verbose = 'verbose', 6 | debug = 'debug', 7 | silly = 'silly', 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/password-history/interfaces/password-history.interface.ts: -------------------------------------------------------------------------------- 1 | import { PasswordHistory, User } from '@prisma/client'; 2 | 3 | export interface IPasswordHistory extends PasswordHistory { 4 | user: User; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/role/constants/role.list.constant.ts: -------------------------------------------------------------------------------- 1 | import { EnumRoleType } from '@prisma/client'; 2 | 3 | export const RoleDefaultAvailableSearch = ['name']; 4 | export const RoleDefaultType = Object.values(EnumRoleType); 5 | -------------------------------------------------------------------------------- /src/modules/role/enums/role.status-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EnumRoleStatusCodeError { 2 | notFound = 5060, 3 | exist = 5061, 4 | predefinedNotFound = 5062, 5 | forbidden = 5063, 6 | used = 5064, 7 | } 8 | -------------------------------------------------------------------------------- /src/languages/en/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "forbidden": "Sorry, you don't have the necessary permissions to perform this action.", 4 | "predefinedNotFound": "Predefined abilities not setted." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/api-key/constants/api-key.list.constant.ts: -------------------------------------------------------------------------------- 1 | import { EnumApiKeyType } from '@prisma/client'; 2 | 3 | export const ApiKeyDefaultAvailableSearch = ['name']; 4 | export const ApiKeyDefaultType = Object.values(EnumApiKeyType); 5 | -------------------------------------------------------------------------------- /src/modules/email/templates/email.change-password.template.hbs: -------------------------------------------------------------------------------- 1 |

Hi{username},


Change password successfully.

Support Email:{supportEmail}.

Visit us:{homeUrl}.


By:{homeName}.

-------------------------------------------------------------------------------- /src/modules/email/templates/email.reset-two-factor-by-admin.template.hbs: -------------------------------------------------------------------------------- 1 |

Hi{username},


Reset Two Factor By Admin

Support Email:{supportEmail}.

Visit us:{homeUrl}.


By:{homeName}.

-------------------------------------------------------------------------------- /src/common/database/dtos/database.id.dto.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseDto } from '@common/database/dtos/database.dto'; 2 | import { PickType } from '@nestjs/swagger'; 3 | 4 | export class DatabaseIdDto extends PickType(DatabaseDto, ['id'] as const) {} 5 | -------------------------------------------------------------------------------- /src/modules/email/templates/email.welcome.template.hbs: -------------------------------------------------------------------------------- 1 |

Welcome {username},


Sign up success with email {email}.

Support Email: {supportEmail}.

Visit us: {homeUrl}.


By: {homeName}.

-------------------------------------------------------------------------------- /src/modules/user/constants/user.list.constant.ts: -------------------------------------------------------------------------------- 1 | import { EnumUserStatus } from '@prisma/client'; 2 | 3 | export const UserDefaultAvailableSearch = ['name', 'username', 'email']; 4 | export const UserDefaultStatus = Object.values(EnumUserStatus); 5 | -------------------------------------------------------------------------------- /src/modules/email/templates/email.verified.template.hbs: -------------------------------------------------------------------------------- 1 |

Hi{username},


Email is verified successfully

Reference:{reference}.

Support Email:{supportEmail}.

Visit us:{homeUrl}.


By:{homeName}.

-------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 4, 7 | "useTabs": false, 8 | "endOfLine": "lf", 9 | "bracketSpacing": true, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/en/file.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "required": "This field is required and cannot be left blank.", 4 | "requiredParseFirst": "Please parse the data before proceeding.", 5 | "extensionInvalid": "The file extension is invalid." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/activity-log/interfaces/activity-log.interface.ts: -------------------------------------------------------------------------------- 1 | import { ActivityLog, User } from '@prisma/client'; 2 | 3 | export interface IActivityLog extends ActivityLog { 4 | user: User; 5 | } 6 | 7 | export type IActivityLogMetadata = Record; 8 | -------------------------------------------------------------------------------- /src/modules/email/dtos/email.verified.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from '@nestjs/swagger'; 2 | import { EmailVerificationDto } from '@modules/email/dtos/email.verification.dto'; 3 | 4 | export class EmailVerifiedDto extends PickType(EmailVerificationDto, [ 5 | 'reference', 6 | ] as const) {} 7 | -------------------------------------------------------------------------------- /src/languages/en/session.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": "Sessions retrieved successfully.", 3 | "revoke": "Session revoked successfully.", 4 | "error": { 5 | "notFound": "Sorry, we couldn't find the session.", 6 | "forbidden": "You cannot revoke your current session." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/queues/exceptions/queue.exception.ts: -------------------------------------------------------------------------------- 1 | export class QueueException extends Error { 2 | readonly isFatal: boolean = false; 3 | 4 | constructor(message: string, isFatal?: boolean) { 5 | super(message); 6 | 7 | this.isFatal = isFatal ?? this.isFatal; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/configs/response.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export interface IConfigRequest { 4 | cachePrefix: string; 5 | } 6 | 7 | export default registerAs( 8 | 'response', 9 | (): IConfigRequest => ({ 10 | cachePrefix: 'Apis', 11 | }) 12 | ); 13 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | ignoredBuiltDependencies: 2 | - '@scarf/scarf' 3 | - '@sentry-internal/node-cpu-profiler' 4 | - msgpackr-extract 5 | - unrs-resolver 6 | 7 | onlyBuiltDependencies: 8 | - '@nestjs/core' 9 | - '@prisma/client' 10 | - '@prisma/engines' 11 | - '@swc/core' 12 | - prisma 13 | -------------------------------------------------------------------------------- /src/modules/email/templates/email.mobile-number-verified.template.hbs: -------------------------------------------------------------------------------- 1 |

Hi{username},


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 | --------------------------------------------------------------------------------