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.
- package/README.md +34 -0
- package/bin/index.js +65 -0
- package/package.json +18 -0
- package/templates/monorepo-starter/.vscode/settings.json +3 -0
- package/templates/monorepo-starter/README.md +42 -0
- package/templates/monorepo-starter/apps/web/components.json +20 -0
- package/templates/monorepo-starter/apps/web/eslint.config.mjs +4 -0
- package/templates/monorepo-starter/apps/web/next-env.d.ts +6 -0
- package/templates/monorepo-starter/apps/web/next.config.mjs +6 -0
- package/templates/monorepo-starter/apps/web/package.json +31 -0
- package/templates/monorepo-starter/apps/web/postcss.config.mjs +1 -0
- package/templates/monorepo-starter/apps/web/public/.gitkeep +0 -0
- package/templates/monorepo-starter/apps/web/sitemap.config.cjs +6 -0
- package/templates/monorepo-starter/apps/web/src/app/(auth)/layout.tsx +7 -0
- package/templates/monorepo-starter/apps/web/src/app/(root)/layout.tsx +15 -0
- package/templates/monorepo-starter/apps/web/src/app/(root)/page.tsx +14 -0
- package/templates/monorepo-starter/apps/web/src/app/globals.css +1 -0
- package/templates/monorepo-starter/apps/web/src/app/layout.tsx +24 -0
- package/templates/monorepo-starter/apps/web/src/components/Footer.tsx +9 -0
- package/templates/monorepo-starter/apps/web/src/components/Header.tsx +11 -0
- package/templates/monorepo-starter/apps/web/src/components/ThemeSwitch.tsx +34 -0
- package/templates/monorepo-starter/apps/web/src/hooks/.gitkeep +0 -0
- package/templates/monorepo-starter/apps/web/src/lib/.gitkeep +0 -0
- package/templates/monorepo-starter/apps/web/src/providers/index.tsx +10 -0
- package/templates/monorepo-starter/apps/web/src/providers/theme.tsx +19 -0
- package/templates/monorepo-starter/apps/web/src/types/index.d.ts +16 -0
- package/templates/monorepo-starter/apps/web/tsconfig.json +20 -0
- package/templates/monorepo-starter/eslint.config.mjs +12 -0
- package/templates/monorepo-starter/package.json +23 -0
- package/templates/monorepo-starter/packages/config/eslint/base.js +31 -0
- package/templates/monorepo-starter/packages/config/eslint/nest.js +26 -0
- package/templates/monorepo-starter/packages/config/eslint/next.js +39 -0
- package/templates/monorepo-starter/packages/config/eslint/react.js +30 -0
- package/templates/monorepo-starter/packages/config/package.json +32 -0
- package/templates/monorepo-starter/packages/config/typescript/base.json +31 -0
- package/templates/monorepo-starter/packages/config/typescript/nest.json +14 -0
- package/templates/monorepo-starter/packages/config/typescript/next.json +10 -0
- package/templates/monorepo-starter/packages/config/typescript/react.json +9 -0
- package/templates/monorepo-starter/packages/ui/components.json +20 -0
- package/templates/monorepo-starter/packages/ui/eslint.config.mjs +4 -0
- package/templates/monorepo-starter/packages/ui/package.json +38 -0
- package/templates/monorepo-starter/packages/ui/postcss.config.mjs +6 -0
- package/templates/monorepo-starter/packages/ui/src/components/button.tsx +71 -0
- package/templates/monorepo-starter/packages/ui/src/hooks/.gitkeep +0 -0
- package/templates/monorepo-starter/packages/ui/src/lib/utils.ts +6 -0
- package/templates/monorepo-starter/packages/ui/src/styles/globals.css +182 -0
- package/templates/monorepo-starter/packages/ui/tsconfig.json +13 -0
- package/templates/monorepo-starter/packages/ui/tsconfig.lint.json +8 -0
- package/templates/monorepo-starter/pnpm-lock.yaml +12441 -0
- package/templates/monorepo-starter/pnpm-workspace.yaml +17 -0
- package/templates/monorepo-starter/server/.env.example +64 -0
- package/templates/monorepo-starter/server/README.md +63 -0
- package/templates/monorepo-starter/server/eslint.config.mjs +4 -0
- package/templates/monorepo-starter/server/nest-cli.json +12 -0
- package/templates/monorepo-starter/server/package.json +97 -0
- package/templates/monorepo-starter/server/prisma/generated/browser.ts +54 -0
- package/templates/monorepo-starter/server/prisma/generated/client.ts +76 -0
- package/templates/monorepo-starter/server/prisma/generated/commonInputTypes.ts +577 -0
- package/templates/monorepo-starter/server/prisma/generated/enums.ts +68 -0
- package/templates/monorepo-starter/server/prisma/generated/internal/class.ts +250 -0
- package/templates/monorepo-starter/server/prisma/generated/internal/prismaNamespace.ts +1436 -0
- package/templates/monorepo-starter/server/prisma/generated/internal/prismaNamespaceBrowser.ts +227 -0
- package/templates/monorepo-starter/server/prisma/generated/models/BackupCode.ts +1375 -0
- package/templates/monorepo-starter/server/prisma/generated/models/Notification.ts +1587 -0
- package/templates/monorepo-starter/server/prisma/generated/models/Otp.ts +1488 -0
- package/templates/monorepo-starter/server/prisma/generated/models/RefreshToken.ts +1515 -0
- package/templates/monorepo-starter/server/prisma/generated/models/RoleAssignment.ts +1385 -0
- package/templates/monorepo-starter/server/prisma/generated/models/SecuritySetting.ts +1422 -0
- package/templates/monorepo-starter/server/prisma/generated/models/User.ts +2498 -0
- package/templates/monorepo-starter/server/prisma/generated/models.ts +18 -0
- package/templates/monorepo-starter/server/prisma/migrations/20251218164821_init/migration.sql +210 -0
- package/templates/monorepo-starter/server/prisma/migrations/migration_lock.toml +3 -0
- package/templates/monorepo-starter/server/prisma/schema.prisma +193 -0
- package/templates/monorepo-starter/server/prisma.config.ts +13 -0
- package/templates/monorepo-starter/server/scripts/generate.sh +14 -0
- package/templates/monorepo-starter/server/src/app.module.ts +49 -0
- package/templates/monorepo-starter/server/src/lib/decorators/logger.decorator.ts +20 -0
- package/templates/monorepo-starter/server/src/lib/decorators/public.decorator.ts +4 -0
- package/templates/monorepo-starter/server/src/lib/decorators/roles.decorator.ts +5 -0
- package/templates/monorepo-starter/server/src/lib/dto/auth.dto.ts +64 -0
- package/templates/monorepo-starter/server/src/lib/dto/security-setting.dto.ts +21 -0
- package/templates/monorepo-starter/server/src/lib/filters/exceptions.filter.ts +62 -0
- package/templates/monorepo-starter/server/src/lib/guards/auth.guard.ts +104 -0
- package/templates/monorepo-starter/server/src/lib/interceptors/response.interceptor.ts +33 -0
- package/templates/monorepo-starter/server/src/lib/pipes/validation.pipe.ts +7 -0
- package/templates/monorepo-starter/server/src/lib/schemas/env.schema.ts +99 -0
- package/templates/monorepo-starter/server/src/lib/utils/cookie.util.ts +23 -0
- package/templates/monorepo-starter/server/src/lib/utils/general.util.ts +24 -0
- package/templates/monorepo-starter/server/src/main.ts +41 -0
- package/templates/monorepo-starter/server/src/modules/auth/auth.controller.ts +74 -0
- package/templates/monorepo-starter/server/src/modules/auth/auth.module.ts +16 -0
- package/templates/monorepo-starter/server/src/modules/auth/auth.service.ts +525 -0
- package/templates/monorepo-starter/server/src/modules/auth/oauth.controller.ts +58 -0
- package/templates/monorepo-starter/server/src/modules/auth/oauth.service.ts +165 -0
- package/templates/monorepo-starter/server/src/modules/auth/otp.service.ts +133 -0
- package/templates/monorepo-starter/server/src/modules/auth/role.service.ts +99 -0
- package/templates/monorepo-starter/server/src/modules/auth/security-setting.service.ts +102 -0
- package/templates/monorepo-starter/server/src/modules/env/env.module.ts +9 -0
- package/templates/monorepo-starter/server/src/modules/env/env.service.ts +22 -0
- package/templates/monorepo-starter/server/src/modules/logger/logger.module.ts +12 -0
- package/templates/monorepo-starter/server/src/modules/logger/logger.service.ts +37 -0
- package/templates/monorepo-starter/server/src/modules/logger/winston.config.ts +32 -0
- package/templates/monorepo-starter/server/src/modules/notification/notification.module.ts +11 -0
- package/templates/monorepo-starter/server/src/modules/notification/notification.service.ts +118 -0
- package/templates/monorepo-starter/server/src/modules/prisma/prisma.extension.ts +72 -0
- package/templates/monorepo-starter/server/src/modules/prisma/prisma.module.ts +9 -0
- package/templates/monorepo-starter/server/src/modules/prisma/prisma.service.ts +49 -0
- package/templates/monorepo-starter/server/src/modules/public/public.controller.ts +21 -0
- package/templates/monorepo-starter/server/src/modules/public/public.module.ts +9 -0
- package/templates/monorepo-starter/server/src/modules/public/public.service.ts +30 -0
- package/templates/monorepo-starter/server/src/modules/scheduler/cleanup.service.ts +33 -0
- package/templates/monorepo-starter/server/src/modules/scheduler/scheduler.module.ts +7 -0
- package/templates/monorepo-starter/server/src/modules/template/template.module.ts +8 -0
- package/templates/monorepo-starter/server/src/modules/template/template.service.ts +33 -0
- package/templates/monorepo-starter/server/src/modules/token/token.module.ts +11 -0
- package/templates/monorepo-starter/server/src/modules/token/token.service.ts +131 -0
- package/templates/monorepo-starter/server/src/types/express.d.ts +10 -0
- package/templates/monorepo-starter/server/src/types/index.d.ts +13 -0
- package/templates/monorepo-starter/server/templates/notification.templates.ts +243 -0
- package/templates/monorepo-starter/server/test/app.e2e-spec.ts +25 -0
- package/templates/monorepo-starter/server/test/jest-e2e.json +9 -0
- package/templates/monorepo-starter/server/tsconfig.json +23 -0
- package/templates/monorepo-starter/server/tsup.config.ts +14 -0
- package/templates/monorepo-starter/tsconfig.json +3 -0
- 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,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,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,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
|
+
}
|