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,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Injectable,
|
|
4
|
+
type OnModuleInit,
|
|
5
|
+
} from "@nestjs/common";
|
|
6
|
+
import passport from "passport";
|
|
7
|
+
import {
|
|
8
|
+
Strategy as GoogleStrategy,
|
|
9
|
+
type Profile as GoogleProfile,
|
|
10
|
+
} from "passport-google-oauth20";
|
|
11
|
+
import {
|
|
12
|
+
Strategy as FacebookStrategy,
|
|
13
|
+
type Profile as FacebookProfile,
|
|
14
|
+
} from "passport-facebook";
|
|
15
|
+
import { EnvService } from "@modules/env/env.service";
|
|
16
|
+
import { PrismaService } from "@modules/prisma/prisma.service";
|
|
17
|
+
import { OtpService } from "./otp.service";
|
|
18
|
+
import { NotificationService } from "@modules/notification/notification.service";
|
|
19
|
+
|
|
20
|
+
interface NormalizedProfile {
|
|
21
|
+
provider: "google" | "facebook";
|
|
22
|
+
id: string;
|
|
23
|
+
email: string | null;
|
|
24
|
+
displayName: string;
|
|
25
|
+
firstName: string;
|
|
26
|
+
lastName: string;
|
|
27
|
+
photo?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Injectable()
|
|
31
|
+
export class OAuthService implements OnModuleInit {
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly prisma: PrismaService,
|
|
34
|
+
private readonly otpService: OtpService,
|
|
35
|
+
private readonly env: EnvService,
|
|
36
|
+
private readonly notifyService: NotificationService
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
onModuleInit() {
|
|
40
|
+
this.initGoogleStrategy();
|
|
41
|
+
this.initFacebookStrategy();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private initGoogleStrategy() {
|
|
45
|
+
passport.use(
|
|
46
|
+
"google",
|
|
47
|
+
new GoogleStrategy(
|
|
48
|
+
{
|
|
49
|
+
clientID: this.env.get("GOOGLE_CLIENT_ID"),
|
|
50
|
+
clientSecret: this.env.get("GOOGLE_CLIENT_SECRET"),
|
|
51
|
+
callbackURL: this.env.get("GOOGLE_CALLBACK_URL"),
|
|
52
|
+
scope: ["profile", "email"],
|
|
53
|
+
},
|
|
54
|
+
async (_, __, profile: GoogleProfile, done) => {
|
|
55
|
+
try {
|
|
56
|
+
const user = await this.validateOAuthLogin(profile);
|
|
57
|
+
done(null, user);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
done(err, false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private initFacebookStrategy() {
|
|
67
|
+
passport.use(
|
|
68
|
+
"facebook",
|
|
69
|
+
new FacebookStrategy(
|
|
70
|
+
{
|
|
71
|
+
clientID: this.env.get("FACEBOOK_CLIENT_ID"),
|
|
72
|
+
clientSecret: this.env.get("FACEBOOK_CLIENT_SECRET"),
|
|
73
|
+
callbackURL: this.env.get("FACEBOOK_CALLBACK_URL"),
|
|
74
|
+
scope: "email",
|
|
75
|
+
profileFields: ["id", "emails", "name", "displayName"],
|
|
76
|
+
},
|
|
77
|
+
async (_, __, profile: FacebookProfile, done) => {
|
|
78
|
+
try {
|
|
79
|
+
const user = await this.validateOAuthLogin(profile);
|
|
80
|
+
done(null, user);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
done(err, null);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async validateOAuthLogin(profile: GoogleProfile | FacebookProfile) {
|
|
90
|
+
const normalized = this.normalizeProfile(profile);
|
|
91
|
+
|
|
92
|
+
if (!normalized.email) {
|
|
93
|
+
throw new BadRequestException("No email found");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let user = await this.prisma.user.findUnique({
|
|
97
|
+
where: { email: normalized.email },
|
|
98
|
+
include: { roles: true },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!user) {
|
|
102
|
+
user = await this.prisma.user.create({
|
|
103
|
+
data: {
|
|
104
|
+
email: normalized.email,
|
|
105
|
+
firstName: normalized.firstName,
|
|
106
|
+
lastName: normalized.lastName,
|
|
107
|
+
displayName: normalized.displayName,
|
|
108
|
+
imageUrl: normalized.photo,
|
|
109
|
+
isEmailVerified: true,
|
|
110
|
+
password: null,
|
|
111
|
+
roles: { create: [{ role: "customer" }] },
|
|
112
|
+
},
|
|
113
|
+
include: { roles: true },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await this.notifyService.sendNotification({
|
|
117
|
+
userId: user.id,
|
|
118
|
+
purpose: "signup",
|
|
119
|
+
to: user.email!,
|
|
120
|
+
metadata: { user },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!user.password) {
|
|
125
|
+
await this.otpService.sendOtp({
|
|
126
|
+
userId: user.id,
|
|
127
|
+
purpose: "setPassword",
|
|
128
|
+
identifier: normalized.email,
|
|
129
|
+
type: "token",
|
|
130
|
+
metadata: { user },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
id: user.id,
|
|
136
|
+
roles: user.roles.map((r) => r.role),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private normalizeProfile(
|
|
141
|
+
profile: GoogleProfile | FacebookProfile
|
|
142
|
+
): NormalizedProfile {
|
|
143
|
+
const provider = profile.provider as "google" | "facebook";
|
|
144
|
+
|
|
145
|
+
const email = profile.emails?.[0]?.value || null;
|
|
146
|
+
const displayName =
|
|
147
|
+
profile.displayName ||
|
|
148
|
+
`${profile.name?.givenName || ""} ${profile.name?.familyName || ""}`.trim();
|
|
149
|
+
|
|
150
|
+
const firstName = profile.name?.givenName || "";
|
|
151
|
+
const lastName = profile.name?.familyName || "";
|
|
152
|
+
|
|
153
|
+
const photo = profile.photos?.[0]?.value;
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
provider,
|
|
157
|
+
id: profile.id,
|
|
158
|
+
email,
|
|
159
|
+
displayName,
|
|
160
|
+
firstName,
|
|
161
|
+
lastName,
|
|
162
|
+
photo,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
|
2
|
+
import { OtpPurpose, OtpType } from "@prisma/client";
|
|
3
|
+
import { PrismaService } from "@modules/prisma/prisma.service";
|
|
4
|
+
import { NotificationService } from "@modules/notification/notification.service";
|
|
5
|
+
import { expiryDate } from "@utils/general.util";
|
|
6
|
+
import { EnvService } from "@modules/env/env.service";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import { LoggerService } from "@modules/logger/logger.service";
|
|
9
|
+
import { InjectLogger } from "@decorators/logger.decorator";
|
|
10
|
+
|
|
11
|
+
interface SendOtpPayload {
|
|
12
|
+
userId: string;
|
|
13
|
+
identifier: string;
|
|
14
|
+
purpose: OtpPurpose;
|
|
15
|
+
type?: OtpType;
|
|
16
|
+
notify?: boolean;
|
|
17
|
+
metadata?: Record<string, any>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface verifyOtpPayload {
|
|
21
|
+
userId: string;
|
|
22
|
+
secret: string;
|
|
23
|
+
purpose: OtpPurpose;
|
|
24
|
+
type?: OtpType;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Injectable()
|
|
28
|
+
export class OtpService {
|
|
29
|
+
@InjectLogger()
|
|
30
|
+
private readonly logger!: LoggerService;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly prisma: PrismaService,
|
|
34
|
+
private readonly notifyService: NotificationService,
|
|
35
|
+
private readonly env: EnvService
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
async sendOtp({
|
|
39
|
+
userId,
|
|
40
|
+
identifier,
|
|
41
|
+
purpose,
|
|
42
|
+
type = "otp",
|
|
43
|
+
notify = true,
|
|
44
|
+
metadata,
|
|
45
|
+
}: SendOtpPayload) {
|
|
46
|
+
let otp = await this.prisma.otp.findFirst({
|
|
47
|
+
where: {
|
|
48
|
+
userId,
|
|
49
|
+
purpose,
|
|
50
|
+
type,
|
|
51
|
+
usedAt: null,
|
|
52
|
+
expiresAt: { gt: new Date() },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!otp) {
|
|
57
|
+
otp = await this.prisma.otp.create({
|
|
58
|
+
data: {
|
|
59
|
+
userId,
|
|
60
|
+
purpose,
|
|
61
|
+
type,
|
|
62
|
+
secret: this.generateSecret(type),
|
|
63
|
+
expiresAt: expiryDate(this.env.get("OTP_EXP"), true),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.logger.log(`🔢 OTP generated`, {
|
|
69
|
+
userId,
|
|
70
|
+
purpose,
|
|
71
|
+
type,
|
|
72
|
+
expiresAt: otp.expiresAt,
|
|
73
|
+
context: OtpService.name,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!notify) return otp;
|
|
77
|
+
|
|
78
|
+
await this.notifyService.sendNotification({
|
|
79
|
+
userId: userId,
|
|
80
|
+
purpose,
|
|
81
|
+
to: identifier,
|
|
82
|
+
metadata: { otp, identifier, ...metadata },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return otp;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async verifyOtp({ userId, secret, purpose, type = "otp" }: verifyOtpPayload) {
|
|
89
|
+
const otp = await this.prisma.otp.findFirst({
|
|
90
|
+
where: {
|
|
91
|
+
userId,
|
|
92
|
+
secret,
|
|
93
|
+
purpose,
|
|
94
|
+
type,
|
|
95
|
+
usedAt: null,
|
|
96
|
+
expiresAt: { gt: new Date() },
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!otp) {
|
|
101
|
+
this.logger.warn(`Invalid OTP`, {
|
|
102
|
+
userId,
|
|
103
|
+
purpose,
|
|
104
|
+
context: OtpService.name,
|
|
105
|
+
});
|
|
106
|
+
throw new UnauthorizedException("Invalid OTP");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await this.prisma.otp.update({
|
|
110
|
+
where: { id: otp.id },
|
|
111
|
+
data: { usedAt: new Date() },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
this.logger.log(`✅ OTP verified`, {
|
|
115
|
+
userId,
|
|
116
|
+
purpose,
|
|
117
|
+
context: OtpService.name,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return otp;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private generateSecret(type: OtpType, prefix = "") {
|
|
124
|
+
switch (type) {
|
|
125
|
+
case "token":
|
|
126
|
+
return `${prefix}${crypto.randomBytes(32).toString("hex")}`;
|
|
127
|
+
case "otp":
|
|
128
|
+
return crypto.randomInt(100000, 999999).toString();
|
|
129
|
+
default:
|
|
130
|
+
throw new Error(`Unsupported type: ${type}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { BadRequestException, Injectable } from "@nestjs/common";
|
|
2
|
+
import { PrismaService } from "@modules/prisma/prisma.service";
|
|
3
|
+
import { LoggerService } from "@modules/logger/logger.service";
|
|
4
|
+
import { InjectLogger } from "@decorators/logger.decorator";
|
|
5
|
+
import { UserRole } from "@generated/prisma";
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class RoleService {
|
|
9
|
+
@InjectLogger()
|
|
10
|
+
private readonly logger!: LoggerService;
|
|
11
|
+
|
|
12
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
13
|
+
|
|
14
|
+
/** Assign a role to a user */
|
|
15
|
+
async assignRole(userId: string, role: UserRole) {
|
|
16
|
+
const existing = await this.prisma.roleAssignment.findFirst({
|
|
17
|
+
where: { userId, role },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (existing) {
|
|
21
|
+
throw new BadRequestException(`User already has role '${role}'.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const newRole = await this.prisma.roleAssignment.create({
|
|
25
|
+
data: { userId, role },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.logger.log(`✅ Role '${role}' assigned to user ${userId}`, {
|
|
29
|
+
userId,
|
|
30
|
+
role,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
message: `Role '${role}' assigned successfully.`,
|
|
35
|
+
data: { newRole },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Revoke a role from a user */
|
|
40
|
+
async revokeRole(userId: string, role: UserRole) {
|
|
41
|
+
const roleRecord = await this.prisma.roleAssignment.findFirst({
|
|
42
|
+
where: { userId, role },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!roleRecord) {
|
|
46
|
+
throw new BadRequestException(`User does not have role '${role}'.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await this.prisma.roleAssignment.delete({
|
|
50
|
+
where: { id: roleRecord.id },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.logger.log(`🚫 Role '${role}' revoked from user ${userId}`, {
|
|
54
|
+
userId,
|
|
55
|
+
role,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return { message: `Role '${role}' revoked successfully.` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Get all roles for a user */
|
|
62
|
+
async getUserRoles(userId: string) {
|
|
63
|
+
const roles = await this.prisma.roleAssignment.findMany({
|
|
64
|
+
where: { userId },
|
|
65
|
+
select: { role: true },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const list = roles.map((r) => r.role);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
message: `Roles fetched successfully.`,
|
|
72
|
+
data: { userId, roles: list },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Check if user has specific role */
|
|
77
|
+
async hasRole(userId: string, role: UserRole) {
|
|
78
|
+
const existing = await this.prisma.roleAssignment.findFirst({
|
|
79
|
+
where: { userId, role },
|
|
80
|
+
});
|
|
81
|
+
return !!existing;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Update user's roles in bulk (sync) */
|
|
85
|
+
async syncRoles(userId: string, roles: UserRole[]) {
|
|
86
|
+
await this.prisma.roleAssignment.deleteMany({ where: { userId } });
|
|
87
|
+
|
|
88
|
+
await this.prisma.roleAssignment.createMany({
|
|
89
|
+
data: roles.map((r) => ({ userId, role: r })),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.logger.log(`🔄 Roles synced for user ${userId}`, {
|
|
93
|
+
userId,
|
|
94
|
+
roles,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { message: "User roles synced successfully.", data: { roles } };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Injectable,
|
|
4
|
+
NotFoundException,
|
|
5
|
+
} from "@nestjs/common";
|
|
6
|
+
import { PrismaService } from "@modules/prisma/prisma.service";
|
|
7
|
+
import { LoggerService } from "@modules/logger/logger.service";
|
|
8
|
+
import { InjectLogger } from "@decorators/logger.decorator";
|
|
9
|
+
import crypto from "crypto";
|
|
10
|
+
import { expiryDate } from "@utils/general.util";
|
|
11
|
+
import type { Request } from "express";
|
|
12
|
+
import type {
|
|
13
|
+
UpdateSecuritySettingDto,
|
|
14
|
+
VerifyBackupCodeDto,
|
|
15
|
+
} from "@dto/security-setting.dto";
|
|
16
|
+
|
|
17
|
+
@Injectable()
|
|
18
|
+
export class SecuritySettingService {
|
|
19
|
+
@InjectLogger()
|
|
20
|
+
private readonly logger!: LoggerService;
|
|
21
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
22
|
+
|
|
23
|
+
async generateBackupCodes(req: Request) {
|
|
24
|
+
const userId = req.user!.id;
|
|
25
|
+
const count = 8;
|
|
26
|
+
|
|
27
|
+
const codes = Array.from({ length: count }).map(() =>
|
|
28
|
+
crypto.randomBytes(6).toString("hex").toUpperCase()
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
await this.prisma.backupCode.deleteMany({ where: { userId } });
|
|
32
|
+
|
|
33
|
+
await this.prisma.backupCode.createMany({
|
|
34
|
+
data: codes.map((code) => ({
|
|
35
|
+
userId,
|
|
36
|
+
code,
|
|
37
|
+
isUsed: false,
|
|
38
|
+
expiresAt: expiryDate("7d", true),
|
|
39
|
+
})),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.logger.log("🔢 Backup codes generated", {
|
|
43
|
+
userId,
|
|
44
|
+
count,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { message: "Backup Codes Generated Successfully", data: { codes } };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async verifyBackupCode(dto: VerifyBackupCodeDto, req: Request) {
|
|
51
|
+
const userId = req.user!.id;
|
|
52
|
+
|
|
53
|
+
const backup = await this.prisma.backupCode.findFirst({
|
|
54
|
+
where: { userId, code: dto.code, usedAt: null },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!backup) {
|
|
58
|
+
throw new BadRequestException("Invalid or used backup code.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await this.prisma.backupCode.update({
|
|
62
|
+
where: { id: backup.id },
|
|
63
|
+
data: { usedAt: new Date() },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.logger.log("✅ Backup code used successfully", {
|
|
67
|
+
userId,
|
|
68
|
+
code: dto.code,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return { message: "Backup code verified successfully." };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getBackupCodes(req: Request) {
|
|
75
|
+
const userId = req.user!.id;
|
|
76
|
+
|
|
77
|
+
const codes = await this.prisma.backupCode.findMany({
|
|
78
|
+
where: { userId, usedAt: null },
|
|
79
|
+
select: { code: true },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!codes.length) {
|
|
83
|
+
throw new NotFoundException("No active backup codes found.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
message: "Backup codes fetched successfully.",
|
|
88
|
+
data: { codes: codes.map((c) => c.code) },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async updateSecuritySetting(dto: UpdateSecuritySettingDto, req: Request) {
|
|
93
|
+
const userId = req.user!.id;
|
|
94
|
+
|
|
95
|
+
await this.prisma.securitySetting.update({
|
|
96
|
+
where: { userId },
|
|
97
|
+
data: dto,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return { message: "Recovery information updated successfully." };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common";
|
|
2
|
+
import { ConfigService } from "@nestjs/config";
|
|
3
|
+
import type { EnvSchema } from "@schemas/env.schema";
|
|
4
|
+
import { LoggerService } from "@modules/logger/logger.service";
|
|
5
|
+
import { InjectLogger } from "@decorators/logger.decorator";
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class EnvService {
|
|
9
|
+
@InjectLogger()
|
|
10
|
+
private readonly logger!: LoggerService;
|
|
11
|
+
|
|
12
|
+
constructor(private readonly config: ConfigService<EnvSchema>) {}
|
|
13
|
+
|
|
14
|
+
get<K extends keyof EnvSchema>(key: K): EnvSchema[K] {
|
|
15
|
+
const value = this.config.get(key);
|
|
16
|
+
if (value === undefined) {
|
|
17
|
+
this.logger.warn(`Missing env key: ${String(key)}`);
|
|
18
|
+
throw new Error(`Missing env key: ${String(key)}`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Global, Module } from "@nestjs/common";
|
|
2
|
+
import { WinstonModule } from "nest-winston";
|
|
3
|
+
import { winstonConfig } from "./winston.config";
|
|
4
|
+
import { LoggerService } from "./logger.service";
|
|
5
|
+
|
|
6
|
+
@Global()
|
|
7
|
+
@Module({
|
|
8
|
+
imports: [WinstonModule.forRoot(winstonConfig)],
|
|
9
|
+
providers: [LoggerService],
|
|
10
|
+
exports: [LoggerService],
|
|
11
|
+
})
|
|
12
|
+
export class LoggerModule {}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Inject, Injectable, Scope } from "@nestjs/common";
|
|
2
|
+
import { WINSTON_MODULE_PROVIDER } from "nest-winston";
|
|
3
|
+
import type { Logger } from "winston";
|
|
4
|
+
|
|
5
|
+
@Injectable({ scope: Scope.TRANSIENT })
|
|
6
|
+
export class LoggerService {
|
|
7
|
+
private context?: string;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
@Inject(WINSTON_MODULE_PROVIDER)
|
|
11
|
+
private readonly winston: Logger
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
setContext(context: string) {
|
|
15
|
+
this.context = context;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private withContext(meta: Record<string, any> = {}) {
|
|
19
|
+
return this.context ? { context: this.context, ...meta } : meta;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
log(message: string, meta: Record<string, any> = {}) {
|
|
23
|
+
this.winston.info(message, this.withContext(meta));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
error(message: string, meta: Record<string, any> = {}) {
|
|
27
|
+
this.winston.error(message, this.withContext(meta));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
warn(message: string, meta: Record<string, any> = {}) {
|
|
31
|
+
this.winston.warn(message, this.withContext(meta));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
debug(message: string, meta: Record<string, any> = {}) {
|
|
35
|
+
this.winston.debug(message, this.withContext(meta));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as winston from "winston";
|
|
2
|
+
import "winston-daily-rotate-file";
|
|
3
|
+
import { utilities } from "nest-winston";
|
|
4
|
+
|
|
5
|
+
export const winstonConfig = {
|
|
6
|
+
transports: [
|
|
7
|
+
new winston.transports.Console({
|
|
8
|
+
format: winston.format.combine(
|
|
9
|
+
winston.format.timestamp(),
|
|
10
|
+
utilities.format.nestLike("EcomApp", { prettyPrint: true })
|
|
11
|
+
),
|
|
12
|
+
}),
|
|
13
|
+
new winston.transports.DailyRotateFile({
|
|
14
|
+
dirname: "logs",
|
|
15
|
+
filename: "app-%DATE%.log",
|
|
16
|
+
datePattern: "DD-MM-YYYY",
|
|
17
|
+
zippedArchive: true,
|
|
18
|
+
maxSize: "20m",
|
|
19
|
+
maxFiles: "14d",
|
|
20
|
+
level: "info",
|
|
21
|
+
}),
|
|
22
|
+
new winston.transports.DailyRotateFile({
|
|
23
|
+
dirname: "logs",
|
|
24
|
+
filename: "error-%DATE%.log",
|
|
25
|
+
datePattern: "DD-MM-YYYY",
|
|
26
|
+
zippedArchive: true,
|
|
27
|
+
maxSize: "20m",
|
|
28
|
+
maxFiles: "30d",
|
|
29
|
+
level: "error",
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Global, Module } from "@nestjs/common";
|
|
2
|
+
import { NotificationService } from "./notification.service";
|
|
3
|
+
import { TemplateModule } from "@modules/template/template.module";
|
|
4
|
+
|
|
5
|
+
@Global()
|
|
6
|
+
@Module({
|
|
7
|
+
imports: [TemplateModule],
|
|
8
|
+
providers: [NotificationService],
|
|
9
|
+
exports: [NotificationService],
|
|
10
|
+
})
|
|
11
|
+
export class NotificationModule {}
|