create-zhx-monorepo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +34 -0
  2. package/bin/index.js +65 -0
  3. package/package.json +18 -0
  4. package/templates/monorepo-starter/.vscode/settings.json +3 -0
  5. package/templates/monorepo-starter/README.md +42 -0
  6. package/templates/monorepo-starter/apps/web/components.json +20 -0
  7. package/templates/monorepo-starter/apps/web/eslint.config.mjs +4 -0
  8. package/templates/monorepo-starter/apps/web/next-env.d.ts +6 -0
  9. package/templates/monorepo-starter/apps/web/next.config.mjs +6 -0
  10. package/templates/monorepo-starter/apps/web/package.json +31 -0
  11. package/templates/monorepo-starter/apps/web/postcss.config.mjs +1 -0
  12. package/templates/monorepo-starter/apps/web/public/.gitkeep +0 -0
  13. package/templates/monorepo-starter/apps/web/sitemap.config.cjs +6 -0
  14. package/templates/monorepo-starter/apps/web/src/app/(auth)/layout.tsx +7 -0
  15. package/templates/monorepo-starter/apps/web/src/app/(root)/layout.tsx +15 -0
  16. package/templates/monorepo-starter/apps/web/src/app/(root)/page.tsx +14 -0
  17. package/templates/monorepo-starter/apps/web/src/app/globals.css +1 -0
  18. package/templates/monorepo-starter/apps/web/src/app/layout.tsx +24 -0
  19. package/templates/monorepo-starter/apps/web/src/components/Footer.tsx +9 -0
  20. package/templates/monorepo-starter/apps/web/src/components/Header.tsx +11 -0
  21. package/templates/monorepo-starter/apps/web/src/components/ThemeSwitch.tsx +34 -0
  22. package/templates/monorepo-starter/apps/web/src/hooks/.gitkeep +0 -0
  23. package/templates/monorepo-starter/apps/web/src/lib/.gitkeep +0 -0
  24. package/templates/monorepo-starter/apps/web/src/providers/index.tsx +10 -0
  25. package/templates/monorepo-starter/apps/web/src/providers/theme.tsx +19 -0
  26. package/templates/monorepo-starter/apps/web/src/types/index.d.ts +16 -0
  27. package/templates/monorepo-starter/apps/web/tsconfig.json +20 -0
  28. package/templates/monorepo-starter/eslint.config.mjs +12 -0
  29. package/templates/monorepo-starter/package.json +23 -0
  30. package/templates/monorepo-starter/packages/config/eslint/base.js +31 -0
  31. package/templates/monorepo-starter/packages/config/eslint/nest.js +26 -0
  32. package/templates/monorepo-starter/packages/config/eslint/next.js +39 -0
  33. package/templates/monorepo-starter/packages/config/eslint/react.js +30 -0
  34. package/templates/monorepo-starter/packages/config/package.json +32 -0
  35. package/templates/monorepo-starter/packages/config/typescript/base.json +31 -0
  36. package/templates/monorepo-starter/packages/config/typescript/nest.json +14 -0
  37. package/templates/monorepo-starter/packages/config/typescript/next.json +10 -0
  38. package/templates/monorepo-starter/packages/config/typescript/react.json +9 -0
  39. package/templates/monorepo-starter/packages/ui/components.json +20 -0
  40. package/templates/monorepo-starter/packages/ui/eslint.config.mjs +4 -0
  41. package/templates/monorepo-starter/packages/ui/package.json +38 -0
  42. package/templates/monorepo-starter/packages/ui/postcss.config.mjs +6 -0
  43. package/templates/monorepo-starter/packages/ui/src/components/button.tsx +71 -0
  44. package/templates/monorepo-starter/packages/ui/src/hooks/.gitkeep +0 -0
  45. package/templates/monorepo-starter/packages/ui/src/lib/utils.ts +6 -0
  46. package/templates/monorepo-starter/packages/ui/src/styles/globals.css +182 -0
  47. package/templates/monorepo-starter/packages/ui/tsconfig.json +13 -0
  48. package/templates/monorepo-starter/packages/ui/tsconfig.lint.json +8 -0
  49. package/templates/monorepo-starter/pnpm-lock.yaml +12441 -0
  50. package/templates/monorepo-starter/pnpm-workspace.yaml +17 -0
  51. package/templates/monorepo-starter/server/.env.example +64 -0
  52. package/templates/monorepo-starter/server/README.md +63 -0
  53. package/templates/monorepo-starter/server/eslint.config.mjs +4 -0
  54. package/templates/monorepo-starter/server/nest-cli.json +12 -0
  55. package/templates/monorepo-starter/server/package.json +97 -0
  56. package/templates/monorepo-starter/server/prisma/generated/browser.ts +54 -0
  57. package/templates/monorepo-starter/server/prisma/generated/client.ts +76 -0
  58. package/templates/monorepo-starter/server/prisma/generated/commonInputTypes.ts +577 -0
  59. package/templates/monorepo-starter/server/prisma/generated/enums.ts +68 -0
  60. package/templates/monorepo-starter/server/prisma/generated/internal/class.ts +250 -0
  61. package/templates/monorepo-starter/server/prisma/generated/internal/prismaNamespace.ts +1436 -0
  62. package/templates/monorepo-starter/server/prisma/generated/internal/prismaNamespaceBrowser.ts +227 -0
  63. package/templates/monorepo-starter/server/prisma/generated/models/BackupCode.ts +1375 -0
  64. package/templates/monorepo-starter/server/prisma/generated/models/Notification.ts +1587 -0
  65. package/templates/monorepo-starter/server/prisma/generated/models/Otp.ts +1488 -0
  66. package/templates/monorepo-starter/server/prisma/generated/models/RefreshToken.ts +1515 -0
  67. package/templates/monorepo-starter/server/prisma/generated/models/RoleAssignment.ts +1385 -0
  68. package/templates/monorepo-starter/server/prisma/generated/models/SecuritySetting.ts +1422 -0
  69. package/templates/monorepo-starter/server/prisma/generated/models/User.ts +2498 -0
  70. package/templates/monorepo-starter/server/prisma/generated/models.ts +18 -0
  71. package/templates/monorepo-starter/server/prisma/migrations/20251218164821_init/migration.sql +210 -0
  72. package/templates/monorepo-starter/server/prisma/migrations/migration_lock.toml +3 -0
  73. package/templates/monorepo-starter/server/prisma/schema.prisma +193 -0
  74. package/templates/monorepo-starter/server/prisma.config.ts +13 -0
  75. package/templates/monorepo-starter/server/scripts/generate.sh +14 -0
  76. package/templates/monorepo-starter/server/src/app.module.ts +49 -0
  77. package/templates/monorepo-starter/server/src/lib/decorators/logger.decorator.ts +20 -0
  78. package/templates/monorepo-starter/server/src/lib/decorators/public.decorator.ts +4 -0
  79. package/templates/monorepo-starter/server/src/lib/decorators/roles.decorator.ts +5 -0
  80. package/templates/monorepo-starter/server/src/lib/dto/auth.dto.ts +64 -0
  81. package/templates/monorepo-starter/server/src/lib/dto/security-setting.dto.ts +21 -0
  82. package/templates/monorepo-starter/server/src/lib/filters/exceptions.filter.ts +62 -0
  83. package/templates/monorepo-starter/server/src/lib/guards/auth.guard.ts +104 -0
  84. package/templates/monorepo-starter/server/src/lib/interceptors/response.interceptor.ts +33 -0
  85. package/templates/monorepo-starter/server/src/lib/pipes/validation.pipe.ts +7 -0
  86. package/templates/monorepo-starter/server/src/lib/schemas/env.schema.ts +99 -0
  87. package/templates/monorepo-starter/server/src/lib/utils/cookie.util.ts +23 -0
  88. package/templates/monorepo-starter/server/src/lib/utils/general.util.ts +24 -0
  89. package/templates/monorepo-starter/server/src/main.ts +41 -0
  90. package/templates/monorepo-starter/server/src/modules/auth/auth.controller.ts +74 -0
  91. package/templates/monorepo-starter/server/src/modules/auth/auth.module.ts +16 -0
  92. package/templates/monorepo-starter/server/src/modules/auth/auth.service.ts +525 -0
  93. package/templates/monorepo-starter/server/src/modules/auth/oauth.controller.ts +58 -0
  94. package/templates/monorepo-starter/server/src/modules/auth/oauth.service.ts +165 -0
  95. package/templates/monorepo-starter/server/src/modules/auth/otp.service.ts +133 -0
  96. package/templates/monorepo-starter/server/src/modules/auth/role.service.ts +99 -0
  97. package/templates/monorepo-starter/server/src/modules/auth/security-setting.service.ts +102 -0
  98. package/templates/monorepo-starter/server/src/modules/env/env.module.ts +9 -0
  99. package/templates/monorepo-starter/server/src/modules/env/env.service.ts +22 -0
  100. package/templates/monorepo-starter/server/src/modules/logger/logger.module.ts +12 -0
  101. package/templates/monorepo-starter/server/src/modules/logger/logger.service.ts +37 -0
  102. package/templates/monorepo-starter/server/src/modules/logger/winston.config.ts +32 -0
  103. package/templates/monorepo-starter/server/src/modules/notification/notification.module.ts +11 -0
  104. package/templates/monorepo-starter/server/src/modules/notification/notification.service.ts +118 -0
  105. package/templates/monorepo-starter/server/src/modules/prisma/prisma.extension.ts +72 -0
  106. package/templates/monorepo-starter/server/src/modules/prisma/prisma.module.ts +9 -0
  107. package/templates/monorepo-starter/server/src/modules/prisma/prisma.service.ts +49 -0
  108. package/templates/monorepo-starter/server/src/modules/public/public.controller.ts +21 -0
  109. package/templates/monorepo-starter/server/src/modules/public/public.module.ts +9 -0
  110. package/templates/monorepo-starter/server/src/modules/public/public.service.ts +30 -0
  111. package/templates/monorepo-starter/server/src/modules/scheduler/cleanup.service.ts +33 -0
  112. package/templates/monorepo-starter/server/src/modules/scheduler/scheduler.module.ts +7 -0
  113. package/templates/monorepo-starter/server/src/modules/template/template.module.ts +8 -0
  114. package/templates/monorepo-starter/server/src/modules/template/template.service.ts +33 -0
  115. package/templates/monorepo-starter/server/src/modules/token/token.module.ts +11 -0
  116. package/templates/monorepo-starter/server/src/modules/token/token.service.ts +131 -0
  117. package/templates/monorepo-starter/server/src/types/express.d.ts +10 -0
  118. package/templates/monorepo-starter/server/src/types/index.d.ts +13 -0
  119. package/templates/monorepo-starter/server/templates/notification.templates.ts +243 -0
  120. package/templates/monorepo-starter/server/test/app.e2e-spec.ts +25 -0
  121. package/templates/monorepo-starter/server/test/jest-e2e.json +9 -0
  122. package/templates/monorepo-starter/server/tsconfig.json +23 -0
  123. package/templates/monorepo-starter/server/tsup.config.ts +14 -0
  124. package/templates/monorepo-starter/tsconfig.json +3 -0
  125. package/templates/monorepo-starter/turbo.json +21 -0
@@ -0,0 +1,118 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import { Resend } from "resend";
3
+ import { EnvService } from "@modules/env/env.service";
4
+ import { NotificationStatus, NotificationType, Prisma } from "@generated/prisma";
5
+ import { PrismaService } from "@modules/prisma/prisma.service";
6
+ import { TemplateService } from "@modules/template/template.service";
7
+ import { LoggerService } from "@modules/logger/logger.service";
8
+ import { InjectLogger } from "@decorators/logger.decorator";
9
+
10
+ interface SendNotificationProps {
11
+ userId: string;
12
+ purpose: NotificationPurpose;
13
+ to: string;
14
+ metadata: Record<string, any>;
15
+ }
16
+
17
+ @Injectable()
18
+ export class NotificationService {
19
+ @InjectLogger()
20
+ private readonly logger!: LoggerService;
21
+ private readonly resend: Resend;
22
+
23
+ constructor(
24
+ private readonly env: EnvService,
25
+ private readonly prisma: PrismaService,
26
+ private readonly templateService: TemplateService
27
+ ) {
28
+ this.resend = new Resend(this.env.get("RESEND_API_KEY"));
29
+ }
30
+
31
+ async sendNotification({
32
+ userId,
33
+ purpose,
34
+ to,
35
+ metadata,
36
+ }: SendNotificationProps) {
37
+ const { html, subject, text } = this.templateService.resolveTemplate(
38
+ purpose,
39
+ metadata
40
+ );
41
+ const type: Exclude<NotificationType, "inApp"> = to.includes("@")
42
+ ? "email"
43
+ : "sms";
44
+
45
+ try {
46
+ const notification = await this.createNotificationRecord({
47
+ userId,
48
+ type,
49
+ title: subject,
50
+ message: text,
51
+ purpose,
52
+ metadata,
53
+ });
54
+
55
+ this.logger.debug(`Notification record created`, {
56
+ notification: notification.id,
57
+ });
58
+
59
+ if (type === "email") {
60
+ const from = "Your App <onboarding@resend.dev>";
61
+ await this.resend.emails.send({ from, to, subject, html });
62
+ } else if (type === "sms") {
63
+ // TODO integrate Twilio/Nexmo here
64
+ console.log(`Sending SMS to ${to}: ${purpose} with data`, metadata);
65
+ }
66
+
67
+ await this.updateNotificationStatus(notification.id, "sent");
68
+ } catch (error) {
69
+ this.logger.error(`❌ Notification send Failed`, {
70
+ type,
71
+ to,
72
+ error,
73
+ });
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ async notifyInApp(
79
+ userId: string,
80
+ purpose: NotificationPurpose,
81
+ title: string,
82
+ message: string,
83
+ metadata?: Record<string, any>
84
+ ) {
85
+ return this.createNotificationRecord({
86
+ userId,
87
+ type: "inApp",
88
+ title,
89
+ message,
90
+ purpose,
91
+ metadata,
92
+ status: "sent",
93
+ });
94
+ }
95
+
96
+ private async createNotificationRecord(
97
+ data: Prisma.NotificationUncheckedCreateInput
98
+ ) {
99
+ return this.prisma.notification.create({ data });
100
+ }
101
+
102
+ private async updateNotificationStatus(
103
+ id: string,
104
+ status: NotificationStatus
105
+ ) {
106
+ return this.prisma.notification.update({
107
+ where: { id },
108
+ data: { status },
109
+ });
110
+ }
111
+
112
+ async markAsRead(id: string) {
113
+ return this.prisma.notification.update({
114
+ where: { id },
115
+ data: { status: "read", readAt: new Date() },
116
+ });
117
+ }
118
+ }
@@ -0,0 +1,72 @@
1
+ import { Prisma } from "@generated/prisma";
2
+
3
+ const hasDeletedAt = (model: string) => {
4
+ const dmmf = Prisma.dmmf.datamodel.models;
5
+ const m = dmmf.find((m) => m.name === model);
6
+ return !!m?.fields.find((f) => f.name === "deletedAt");
7
+ };
8
+
9
+ export const softDeleteExtension = Prisma.defineExtension({
10
+ name: "softDelete",
11
+
12
+ query: {
13
+ $allModels: {
14
+ async delete({ model, args, query }) {
15
+ const hasDel = hasDeletedAt(model);
16
+ const force = (args as any).force;
17
+
18
+ if (hasDel && !force) {
19
+ const prisma = Prisma.getExtensionContext(this);
20
+ console.log("prisma", prisma);
21
+ return (prisma as any)[model].update({
22
+ where: args.where,
23
+ data: { deletedAt: new Date() },
24
+ });
25
+ }
26
+
27
+ return query(args);
28
+ },
29
+
30
+ async deleteMany({ model, args, query }) {
31
+ const hasDel = hasDeletedAt(model);
32
+ const force = (args as any).force;
33
+
34
+ if (hasDel && !force) {
35
+ const prisma = Prisma.getExtensionContext(this);
36
+ console.log("prisma", prisma);
37
+ return (prisma as any)[model].updateMany({
38
+ where: args.where,
39
+ data: { deletedAt: new Date() },
40
+ });
41
+ }
42
+
43
+ return query(args);
44
+ },
45
+
46
+ async $allOperations({ model, operation, args, query }) {
47
+ const hasDel = hasDeletedAt(model);
48
+ const _args = args as Record<string, any>;
49
+ const targetOps = [
50
+ "findUnique",
51
+ "findFirst",
52
+ "findMany",
53
+ "count",
54
+ "aggregate",
55
+ ];
56
+
57
+ if (!targetOps.includes(operation) || !hasDel) return query(args);
58
+ if (_args.includeDeleted) {
59
+ delete _args.includeDeleted;
60
+ return query(_args);
61
+ }
62
+
63
+ _args.where = _args.where ?? {};
64
+ if (!("deletedAt" in _args.where)) {
65
+ _args.where = { ..._args.where, deletedAt: null };
66
+ }
67
+
68
+ return query(_args);
69
+ },
70
+ },
71
+ },
72
+ });
@@ -0,0 +1,9 @@
1
+ import { Global, Module } from "@nestjs/common";
2
+ import { PrismaService } from "./prisma.service";
3
+
4
+ @Global()
5
+ @Module({
6
+ providers: [PrismaService],
7
+ exports: [PrismaService],
8
+ })
9
+ export class PrismaModule {}
@@ -0,0 +1,49 @@
1
+ import type { OnModuleDestroy, OnModuleInit } from "@nestjs/common";
2
+ import { Injectable } from "@nestjs/common";
3
+ import { PrismaClient } from "@generated/prisma";
4
+ import { PrismaPg } from "@prisma/adapter-pg";
5
+
6
+ import { LoggerService } from "@modules/logger/logger.service";
7
+ import { InjectLogger } from "@decorators/logger.decorator";
8
+ import { softDeleteExtension } from "./prisma.extension";
9
+
10
+ @Injectable()
11
+ export class PrismaService
12
+ extends PrismaClient
13
+ implements OnModuleInit, OnModuleDestroy
14
+ {
15
+ @InjectLogger()
16
+ private readonly logger!: LoggerService;
17
+
18
+ constructor() {
19
+ const adapter = new PrismaPg({
20
+ connectionString: process.env.DB_URI!,
21
+ });
22
+
23
+ super({ adapter });
24
+
25
+ Object.assign(this, this.$extends(softDeleteExtension));
26
+ }
27
+
28
+ async onModuleInit() {
29
+ this.logger.log("Connecting to the database...");
30
+ try {
31
+ await this.$connect();
32
+ this.logger.log("✅ Database connection established.");
33
+ } catch (error) {
34
+ this.logger.error("❌ Database connection failed", { error });
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ async onModuleDestroy() {
40
+ this.logger.log("Disconnecting from the database...");
41
+ try {
42
+ await this.$disconnect();
43
+ this.logger.log("Database connection closed.");
44
+ } catch (error) {
45
+ this.logger.error("❌ Error disconnecting database", { error });
46
+ throw error;
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ import { Controller, Get, Req } from "@nestjs/common";
2
+ import { PublicService } from "./public.service";
3
+ import type { Request } from "express";
4
+ import { Public } from "@decorators/public.decorator";
5
+
6
+ @Controller()
7
+ export class PublicController {
8
+ constructor(private readonly publicService: PublicService) {}
9
+
10
+ @Public()
11
+ @Get("/")
12
+ welcome(@Req() req: Request) {
13
+ return this.publicService.welcome(req);
14
+ }
15
+
16
+ @Public()
17
+ @Get("/health")
18
+ healthCheck() {
19
+ return this.publicService.getHealth();
20
+ }
21
+ }
@@ -0,0 +1,9 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { PublicService } from './public.service';
3
+ import { PublicController } from './public.controller';
4
+
5
+ @Module({
6
+ providers: [PublicService],
7
+ controllers: [PublicController]
8
+ })
9
+ export class PublicModule {}
@@ -0,0 +1,30 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import { LoggerService } from "@modules/logger/logger.service";
3
+ import type { Request } from "express";
4
+ import { InjectLogger } from "@decorators/logger.decorator";
5
+
6
+ @Injectable()
7
+ export class PublicService {
8
+ @InjectLogger()
9
+ private readonly logger!: LoggerService;
10
+
11
+ welcome(req: Request) {
12
+ this.logger.log(
13
+ `Welcome endpoint hit from IP: ${req.ip}, path: ${req.url}`
14
+ );
15
+ return { message: "Server is running 🚀" };
16
+ }
17
+
18
+ getHealth() {
19
+ const uptime = process.uptime();
20
+ const timestamp = new Date().toISOString();
21
+
22
+ this.logger.log(`Health check requested. Uptime: ${uptime}s`);
23
+ return {
24
+ message: "Server is healthy",
25
+ status: "ok",
26
+ uptime,
27
+ timestamp,
28
+ };
29
+ }
30
+ }
@@ -0,0 +1,33 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import { Cron, CronExpression } from "@nestjs/schedule";
3
+ import { PrismaService } from "@modules/prisma/prisma.service";
4
+ import { LoggerService } from "@modules/logger/logger.service";
5
+ import { InjectLogger } from "@decorators/logger.decorator";
6
+
7
+ @Injectable()
8
+ export class CleanupService {
9
+ @InjectLogger()
10
+ private readonly logger!: LoggerService;
11
+
12
+ constructor(private readonly prisma: PrismaService) {}
13
+
14
+ @Cron(CronExpression.EVERY_HOUR)
15
+ async handleOtpCleanup() {
16
+ this.logger.log("🧹 Running OTP cleanup job...");
17
+ const result = await this.prisma.otp.deleteMany({
18
+ where: { expiresAt: { lt: new Date() } },
19
+ });
20
+ this.logger.log(`✅ Deleted ${result.count} expired OTPs`);
21
+ }
22
+
23
+ @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
24
+ async handleRefreshTokenCleanup() {
25
+ this.logger.log("🧹 Running Refresh Token cleanup job...");
26
+ const result = await this.prisma.refreshToken.deleteMany({
27
+ where: {
28
+ expiresAt: { lt: new Date() },
29
+ },
30
+ });
31
+ this.logger.log(`✅ Deleted ${result.count} expired refresh tokens`);
32
+ }
33
+ }
@@ -0,0 +1,7 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { CleanupService } from "./cleanup.service";
3
+
4
+ @Module({
5
+ providers: [CleanupService],
6
+ })
7
+ export class SchedulerModule {}
@@ -0,0 +1,8 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { TemplateService } from "./template.service";
3
+
4
+ @Module({
5
+ providers: [TemplateService],
6
+ exports: [TemplateService],
7
+ })
8
+ export class TemplateModule {}
@@ -0,0 +1,33 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import { EnvService } from "@modules//env/env.service";
3
+
4
+ import * as templates from "@templates/notification.templates";
5
+
6
+ @Injectable()
7
+ export class TemplateService {
8
+ constructor(private readonly env: EnvService) {}
9
+
10
+ private readonly templateFactory: Record<
11
+ NotificationPurpose,
12
+ (data: any, env: EnvService) => TemplateReturn
13
+ > = {
14
+ signin: templates.signinTemplate,
15
+ signup: templates.signupTemplate,
16
+ verifyIdentifier: templates.verifyIdentifierTemplate,
17
+ setPassword: templates.setPasswordTemplate,
18
+ resetPassword: templates.resetPasswordTemplate,
19
+ changeIdentifier: templates.changeIdentifierTemplate,
20
+ verifyMfa: templates.verifyMfaTemplate,
21
+ enableMfa: templates.enableMfaTemplate,
22
+ disableMfa: templates.disableMfaTemplate,
23
+ };
24
+
25
+ resolveTemplate(purpose: NotificationPurpose, metadata: any): TemplateReturn {
26
+ const templateFn = this.templateFactory[purpose];
27
+ if (!templateFn) {
28
+ throw new Error(`Undefined template purpose: ${purpose}`);
29
+ }
30
+
31
+ return templateFn(metadata, this.env);
32
+ }
33
+ }
@@ -0,0 +1,11 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { TokenService } from "./token.service";
3
+ import { JwtModule } from "@nestjs/jwt";
4
+ import { CookieService } from "@utils/cookie.util";
5
+
6
+ @Module({
7
+ imports: [JwtModule.register({})],
8
+ providers: [TokenService, CookieService],
9
+ exports: [TokenService],
10
+ })
11
+ export class TokenModule {}
@@ -0,0 +1,131 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import { JwtService } from "@nestjs/jwt";
3
+ import { expiryDate } from "@utils/general.util";
4
+ import { PrismaService } from "@modules/prisma/prisma.service";
5
+ import { UserRole } from "@generated/prisma";
6
+ import { EnvService } from "@modules/env/env.service";
7
+ import type { Request, Response } from "express";
8
+ import { CookieService } from "@utils/cookie.util";
9
+
10
+ export interface TokenPayload {
11
+ sub: string;
12
+ roles: UserRole[];
13
+ }
14
+
15
+ interface TokensType {
16
+ accessToken: string;
17
+ refreshToken: string;
18
+ tokenId: string;
19
+ }
20
+
21
+ @Injectable()
22
+ export class TokenService {
23
+ constructor(
24
+ private readonly jwtService: JwtService,
25
+ private readonly env: EnvService,
26
+ private readonly prisma: PrismaService,
27
+ private readonly cookieService: CookieService
28
+ ) {}
29
+
30
+ async generateTokens(req: Request, payload: TokenPayload) {
31
+ const [accessToken, refreshToken] = await Promise.all([
32
+ this.jwtService.signAsync(payload, {
33
+ secret: this.env.get("JWT_ACCESS_SECRET"),
34
+ expiresIn: this.env.get("ACCESS_TOKEN_EXP"),
35
+ }),
36
+ this.jwtService.signAsync(payload, {
37
+ secret: this.env.get("JWT_REFRESH_SECRET"),
38
+ expiresIn: this.env.get("REFRESH_TOKEN_EXP"),
39
+ }),
40
+ ]);
41
+
42
+ const tokenId = req.cookies["tokenId"] || "undefined";
43
+ const refreshExp = expiryDate(this.env.get("REFRESH_TOKEN_EXP"), true);
44
+
45
+ const tokenData = {
46
+ token: refreshToken,
47
+ userId: payload.sub,
48
+ ip: req.ip || "Unknown IP",
49
+ userAgent: req.headers["user-agent"] || "Unknown User Agent",
50
+ lastUsed: new Date(),
51
+ blacklisted: false,
52
+ isActive: true,
53
+ expiresAt: refreshExp,
54
+ };
55
+
56
+ const newToken = await this.prisma.refreshToken.upsert({
57
+ where: { id: tokenId },
58
+ update: tokenData,
59
+ create: tokenData,
60
+ });
61
+
62
+ return { accessToken, refreshToken, tokenId: newToken.id };
63
+ }
64
+
65
+ async verifyToken(
66
+ token: string,
67
+ type: "access" | "refresh"
68
+ ): Promise<TokenPayload | null> {
69
+ const secret =
70
+ type === "access"
71
+ ? this.env.get("JWT_ACCESS_SECRET")
72
+ : this.env.get("JWT_REFRESH_SECRET");
73
+ try {
74
+ return await this.jwtService.verifyAsync<TokenPayload>(token, { secret });
75
+ } catch (error) {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ async refreshTokens(req: Request, res: Response, payload: TokenPayload) {
81
+ const refreshToken = req.cookies["refreshToken"];
82
+
83
+ const tokenRecord = await this.prisma.refreshToken.findUnique({
84
+ where: { token: refreshToken, blacklisted: false },
85
+ });
86
+
87
+ if (!tokenRecord) {
88
+ throw new Error("Invalid or expired refresh token.");
89
+ }
90
+
91
+ await this.createAuthSession(req, res, {
92
+ id: payload.sub,
93
+ roles: payload.roles,
94
+ });
95
+ }
96
+
97
+ async createAuthSession(req: Request, res: Response, user: Express.User) {
98
+ const tokens = await this.generateTokens(req, {
99
+ sub: user.id,
100
+ roles: user.roles,
101
+ });
102
+ this.setAuthCookies(res, tokens);
103
+ return tokens;
104
+ }
105
+
106
+ attachDecodedUser = (req: Request, decoded: TokenPayload) => {
107
+ if (decoded) req["user"] = { id: decoded.sub, roles: decoded.roles };
108
+ };
109
+
110
+ setAuthCookies(res: Response, tokens: TokensType): void {
111
+ const { accessToken, refreshToken, tokenId } = tokens;
112
+ const accessExp = expiryDate(this.env.get("ACCESS_TOKEN_EXP"), true);
113
+ const refreshExp = expiryDate(this.env.get("REFRESH_TOKEN_EXP"), true);
114
+
115
+ this.cookieService.setCookie(res, "accessToken", accessToken, {
116
+ expires: accessExp,
117
+ });
118
+ this.cookieService.setCookie(res, "refreshToken", refreshToken, {
119
+ expires: refreshExp,
120
+ });
121
+ this.cookieService.setCookie(res, "tokenId", tokenId, {
122
+ expires: refreshExp,
123
+ });
124
+ }
125
+
126
+ clearAuthCookies(res: Response): void {
127
+ res.clearCookie("accessToken");
128
+ res.clearCookie("refreshToken");
129
+ res.clearCookie("tokenId");
130
+ }
131
+ }
@@ -0,0 +1,10 @@
1
+ import { UserRole } from "@generated/prisma";
2
+
3
+ declare global {
4
+ namespace Express {
5
+ interface User {
6
+ id: string;
7
+ roles: UserRole[];
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,13 @@
1
+ import { OrderStatus, OtpPurpose } from "@generated/prisma";
2
+
3
+ declare global {
4
+ type IdentifierKey = "email" | "phone";
5
+
6
+ type NotificationPurpose = "signin" | "signup" | OtpPurpose;
7
+
8
+ type TemplateReturn = {
9
+ subject: string;
10
+ html: string;
11
+ text: string;
12
+ };
13
+ }