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,525 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Injectable,
|
|
4
|
+
UnauthorizedException,
|
|
5
|
+
} from "@nestjs/common";
|
|
6
|
+
import type { Request, Response } from "express";
|
|
7
|
+
import argon2 from "argon2";
|
|
8
|
+
import { PrismaService } from "@modules/prisma/prisma.service";
|
|
9
|
+
import {
|
|
10
|
+
ChangeIdentifierDto,
|
|
11
|
+
RequestOtpDto,
|
|
12
|
+
ResetPasswordDto,
|
|
13
|
+
SignInDto,
|
|
14
|
+
SignUpDto,
|
|
15
|
+
ValidateOtpDto,
|
|
16
|
+
} from "@dto/auth.dto";
|
|
17
|
+
import { TokenService } from "@modules/token/token.service";
|
|
18
|
+
import { Prisma, type User } from "@generated/prisma";
|
|
19
|
+
import { OtpService } from "./otp.service";
|
|
20
|
+
import { NotificationService } from "@modules/notification/notification.service";
|
|
21
|
+
import { LoggerService } from "@modules/logger/logger.service";
|
|
22
|
+
import { InjectLogger } from "@decorators/logger.decorator";
|
|
23
|
+
|
|
24
|
+
@Injectable()
|
|
25
|
+
export class AuthService {
|
|
26
|
+
@InjectLogger()
|
|
27
|
+
private readonly logger!: LoggerService;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly prisma: PrismaService,
|
|
31
|
+
private readonly tokenService: TokenService,
|
|
32
|
+
private readonly otpService: OtpService,
|
|
33
|
+
private readonly notifyService: NotificationService
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
async signUp(dto: SignUpDto) {
|
|
37
|
+
const { key, value, query } = this.parseIdentifier(dto.identifier);
|
|
38
|
+
this.logger.log(`🔐 Sign-up attempt`, {
|
|
39
|
+
identifier: dto.identifier,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const existingUser = await this.prisma.user.findUnique({
|
|
43
|
+
where: query,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (existingUser) {
|
|
47
|
+
throw new BadRequestException(`${key} already in use.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const hashedPassword = await this.hashPassword(dto.password);
|
|
51
|
+
|
|
52
|
+
const newUser = await this.prisma.user.create({
|
|
53
|
+
data: {
|
|
54
|
+
[key]: value,
|
|
55
|
+
password: hashedPassword,
|
|
56
|
+
firstName: dto.firstName,
|
|
57
|
+
lastName: dto.lastName,
|
|
58
|
+
displayName: `${dto.firstName} ${dto.lastName}`.trim(),
|
|
59
|
+
username: dto.username,
|
|
60
|
+
roles: { create: [{ role: "customer" }] },
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await this.otpService.sendOtp({
|
|
65
|
+
userId: newUser.id,
|
|
66
|
+
identifier: value,
|
|
67
|
+
purpose: "verifyIdentifier",
|
|
68
|
+
metadata: { user: newUser },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await this.notifyService.sendNotification({
|
|
72
|
+
userId: newUser.id,
|
|
73
|
+
purpose: "signup",
|
|
74
|
+
to: value,
|
|
75
|
+
metadata: { user: newUser },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.logger.log(`✅ Sign-up success`, {
|
|
79
|
+
userId: newUser.id,
|
|
80
|
+
identifier: dto.identifier,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
message: `User created successfully. Please verify your ${key}.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async signIn(dto: SignInDto, req: Request, res: Response) {
|
|
89
|
+
const { user, key, value } = await this.findUserByIdentifier(
|
|
90
|
+
dto.identifier,
|
|
91
|
+
{
|
|
92
|
+
roles: true,
|
|
93
|
+
securitySetting: true,
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
this.logger.log(`🔐 Sign-in attempt`, {
|
|
98
|
+
identifier: dto.identifier,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!user.password) {
|
|
102
|
+
await this.otpService.sendOtp({
|
|
103
|
+
userId: user.id,
|
|
104
|
+
purpose: "setPassword",
|
|
105
|
+
identifier: value,
|
|
106
|
+
type: "token",
|
|
107
|
+
metadata: { user },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
throw new UnauthorizedException(
|
|
111
|
+
"Password not set. Please set your password to continue."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isPasswordValid = await this.verifyPassword(
|
|
116
|
+
dto.password,
|
|
117
|
+
user.password
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (!isPasswordValid) {
|
|
121
|
+
throw new UnauthorizedException("Invalid credentials");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await this.checkVerificationStatus(user, key, value, "unverified");
|
|
125
|
+
|
|
126
|
+
if (user.securitySetting?.isMfaEnabled) {
|
|
127
|
+
const otp = await this.otpService.sendOtp({
|
|
128
|
+
userId: user.id,
|
|
129
|
+
identifier: value,
|
|
130
|
+
purpose: "verifyMfa",
|
|
131
|
+
metadata: { user },
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
message: "MFA code sent. Please verify to complete login.",
|
|
135
|
+
data: { secret: otp.secret },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const roles = user.roles.map((r) => r.role);
|
|
140
|
+
|
|
141
|
+
await this.tokenService.createAuthSession(req, res, {
|
|
142
|
+
id: user.id,
|
|
143
|
+
roles: roles,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await this.prisma.user.update({
|
|
147
|
+
where: { id: user.id },
|
|
148
|
+
data: { lastLoginAt: new Date() },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await this.notifyService.sendNotification({
|
|
152
|
+
userId: user.id,
|
|
153
|
+
purpose: "signin",
|
|
154
|
+
to: value,
|
|
155
|
+
metadata: { user },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.logger.log(`✅ Sign-in success`, {
|
|
159
|
+
userId: user.id,
|
|
160
|
+
identifier: dto.identifier,
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
message: "Signed in successfully",
|
|
164
|
+
data: { id: user.id, roles: roles },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async signOut(req: Request, res: Response) {
|
|
169
|
+
const refreshToken = req.cookies.refreshToken;
|
|
170
|
+
const tokenId = req.cookies.tokenId;
|
|
171
|
+
|
|
172
|
+
if (refreshToken && tokenId) {
|
|
173
|
+
await this.prisma.refreshToken.update({
|
|
174
|
+
where: { token: refreshToken, id: tokenId },
|
|
175
|
+
data: { blacklisted: true },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.tokenService.clearAuthCookies(res);
|
|
180
|
+
this.logger.log("🚪 User signed out", { tokenId });
|
|
181
|
+
|
|
182
|
+
return { message: "Signed out successfully" };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async requestOtp(dto: RequestOtpDto) {
|
|
186
|
+
const { user, key, value } = await this.findUserByIdentifier(
|
|
187
|
+
dto.identifier,
|
|
188
|
+
{ securitySetting: true }
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (dto.purpose === "verifyIdentifier") {
|
|
192
|
+
await this.checkVerificationStatus(user, key, value, "verified");
|
|
193
|
+
await this.otpService.sendOtp({
|
|
194
|
+
userId: user.id,
|
|
195
|
+
identifier: value,
|
|
196
|
+
purpose: dto.purpose,
|
|
197
|
+
metadata: { user },
|
|
198
|
+
});
|
|
199
|
+
return { message: `Verification OTP sent.` };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (dto.purpose.includes("Password")) {
|
|
203
|
+
if (dto.purpose === "setPassword" && user.password) {
|
|
204
|
+
throw new BadRequestException(
|
|
205
|
+
"Password already set. Use resetPassword."
|
|
206
|
+
);
|
|
207
|
+
} else if (dto.purpose === "resetPassword" && !user.password) {
|
|
208
|
+
throw new BadRequestException("No password set. Use setPassword.");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await this.otpService.sendOtp({
|
|
212
|
+
userId: user.id,
|
|
213
|
+
identifier: value,
|
|
214
|
+
purpose: dto.purpose,
|
|
215
|
+
metadata: { user },
|
|
216
|
+
});
|
|
217
|
+
return { message: `${dto.purpose} OTP sent.` };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (dto.purpose === "changeIdentifier") {
|
|
221
|
+
await this.otpService.sendOtp({
|
|
222
|
+
userId: user.id,
|
|
223
|
+
identifier: value,
|
|
224
|
+
purpose: dto.purpose,
|
|
225
|
+
metadata: { user },
|
|
226
|
+
});
|
|
227
|
+
return { message: `Change ${key} Otp Send` };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (dto.purpose.includes("Mfa")) {
|
|
231
|
+
if (dto.purpose === "enableMfa") {
|
|
232
|
+
if (user.securitySetting?.isMfaEnabled) {
|
|
233
|
+
throw new BadRequestException("MFA is already enabled.");
|
|
234
|
+
}
|
|
235
|
+
} else if (dto.purpose === "disableMfa") {
|
|
236
|
+
if (!user.securitySetting?.isMfaEnabled) {
|
|
237
|
+
throw new BadRequestException("MFA is already disabled.");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
await this.otpService.sendOtp({
|
|
241
|
+
userId: user.id,
|
|
242
|
+
identifier: value,
|
|
243
|
+
purpose: dto.purpose,
|
|
244
|
+
metadata: { user },
|
|
245
|
+
});
|
|
246
|
+
return { message: `${dto.purpose} OTP sent.` };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
throw new BadRequestException(`Invalid purpose: ${dto.purpose}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async validateOtp(dto: ValidateOtpDto, req: Request, res: Response) {
|
|
253
|
+
const { key, value, user } = await this.findUserByIdentifier(
|
|
254
|
+
dto.identifier
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await this.otpService.verifyOtp({
|
|
258
|
+
userId: user.id,
|
|
259
|
+
purpose: dto.purpose,
|
|
260
|
+
secret: dto.secret,
|
|
261
|
+
type: dto.type,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (dto.purpose === "verifyIdentifier") {
|
|
265
|
+
await this.prisma.user.update({
|
|
266
|
+
where: { id: user.id },
|
|
267
|
+
data:
|
|
268
|
+
key === "email"
|
|
269
|
+
? { isEmailVerified: true }
|
|
270
|
+
: { isPhoneVerified: true },
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return { message: `${key} verified successfully.` };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (dto.purpose.includes("Password")) {
|
|
277
|
+
const otp = await this.otpService.sendOtp({
|
|
278
|
+
userId: user.id,
|
|
279
|
+
identifier: value,
|
|
280
|
+
purpose: dto.purpose,
|
|
281
|
+
type: "token",
|
|
282
|
+
notify: false,
|
|
283
|
+
metadata: { user },
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
message: "OTP validated successfully.",
|
|
287
|
+
data: { secret: otp.secret },
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (dto.purpose === "changeIdentifier") {
|
|
292
|
+
const otp = await this.otpService.sendOtp({
|
|
293
|
+
userId: user.id,
|
|
294
|
+
identifier: value,
|
|
295
|
+
purpose: dto.purpose,
|
|
296
|
+
type: "token",
|
|
297
|
+
notify: false,
|
|
298
|
+
metadata: { user },
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
message: "OTP validated successfully.",
|
|
303
|
+
data: { secret: otp.secret },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (dto.purpose.includes("Mfa")) {
|
|
308
|
+
//TODO add logic enable mfa
|
|
309
|
+
if (dto.purpose === "enableMfa") {
|
|
310
|
+
await this.prisma.securitySetting.update({
|
|
311
|
+
where: { userId: user.id },
|
|
312
|
+
data: { isMfaEnabled: true }, // TODO add mfa method details
|
|
313
|
+
});
|
|
314
|
+
} else if (dto.purpose === "disableMfa") {
|
|
315
|
+
await this.prisma.securitySetting.update({
|
|
316
|
+
where: { userId: user.id },
|
|
317
|
+
data: { isMfaEnabled: false },
|
|
318
|
+
});
|
|
319
|
+
} else if (dto.purpose === "verifyMfa") {
|
|
320
|
+
const roles = user.roles.map((r) => r.role);
|
|
321
|
+
|
|
322
|
+
await this.tokenService.createAuthSession(req, res, {
|
|
323
|
+
id: user.id,
|
|
324
|
+
roles: roles,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
message: "MFA verified. Signed in successfully.",
|
|
329
|
+
data: { id: user.id, roles: roles },
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
await this.notifyService.sendNotification({
|
|
334
|
+
userId: user.id,
|
|
335
|
+
to: dto.identifier,
|
|
336
|
+
purpose: dto.purpose,
|
|
337
|
+
metadata: { user },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return { message: `${dto.purpose} Successfully` };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
throw new BadRequestException(`Invalid purpose: ${dto.purpose}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async resetPassword(dto: ResetPasswordDto) {
|
|
347
|
+
const { user } = await this.findUserByIdentifier(dto.identifier);
|
|
348
|
+
|
|
349
|
+
const isTokenValid = await this.otpService.verifyOtp({
|
|
350
|
+
userId: user.id,
|
|
351
|
+
purpose: dto.purpose,
|
|
352
|
+
secret: dto.secret,
|
|
353
|
+
type: "token",
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
if (!isTokenValid) {
|
|
357
|
+
throw new BadRequestException("Invalid Token");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const hashedPassword = await this.hashPassword(dto.newPassword);
|
|
361
|
+
|
|
362
|
+
await this.prisma.user.update({
|
|
363
|
+
where: { id: user.id },
|
|
364
|
+
data: { password: hashedPassword },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await this.notifyService.sendNotification({
|
|
368
|
+
userId: user.id,
|
|
369
|
+
to: dto.identifier,
|
|
370
|
+
purpose: dto.purpose,
|
|
371
|
+
metadata: { user },
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
this.logger.log(`🔑 Password reset successful`, { userId: user.id });
|
|
375
|
+
|
|
376
|
+
return { message: "Password reset successfully" };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async changeIdentifierReq(dto: ChangeIdentifierDto) {
|
|
380
|
+
const { user } = await this.findUserByIdentifier(dto.identifier);
|
|
381
|
+
|
|
382
|
+
const {
|
|
383
|
+
key: newKey,
|
|
384
|
+
value: newValue,
|
|
385
|
+
query: newQuery,
|
|
386
|
+
} = this.parseIdentifier(dto.newIdentifier);
|
|
387
|
+
|
|
388
|
+
const existingUser = await this.prisma.user.findFirst({
|
|
389
|
+
where: newQuery,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
if (existingUser) {
|
|
393
|
+
throw new BadRequestException(`${newKey} already in use.`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const isTokenValid = await this.otpService.verifyOtp({
|
|
397
|
+
userId: user.id,
|
|
398
|
+
purpose: dto.purpose,
|
|
399
|
+
secret: dto.secret,
|
|
400
|
+
type: "token",
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (!isTokenValid) {
|
|
404
|
+
throw new BadRequestException("Invalid Token");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await this.otpService.sendOtp({
|
|
408
|
+
userId: user.id,
|
|
409
|
+
identifier: newValue,
|
|
410
|
+
purpose: dto.purpose,
|
|
411
|
+
metadata: { user },
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
this.logger.log("🔄 Identifier change requested", {
|
|
415
|
+
userId: user.id,
|
|
416
|
+
newIdentifier: dto.newIdentifier,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
message: `Link sent to new ${newKey}. Please verify to complete the change.`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async changeIdentifier(dto: ChangeIdentifierDto) {
|
|
425
|
+
const { user, key } = await this.findUserByIdentifier(dto.identifier);
|
|
426
|
+
|
|
427
|
+
await this.otpService.verifyOtp({
|
|
428
|
+
userId: user.id,
|
|
429
|
+
purpose: dto.purpose,
|
|
430
|
+
secret: dto.secret,
|
|
431
|
+
type: "token",
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
await this.prisma.user.update({
|
|
435
|
+
where: { id: user.id },
|
|
436
|
+
data: { [key]: dto.newIdentifier },
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
await this.prisma.refreshToken.updateMany({
|
|
440
|
+
where: { userId: user.id },
|
|
441
|
+
data: { blacklisted: true },
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
await this.notifyService.sendNotification({
|
|
445
|
+
userId: user.id,
|
|
446
|
+
to: dto.newIdentifier,
|
|
447
|
+
purpose: dto.purpose,
|
|
448
|
+
metadata: {
|
|
449
|
+
user,
|
|
450
|
+
identifier: dto.identifier,
|
|
451
|
+
newIdentifier: dto.newIdentifier,
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
this.logger.log("✅ Identifier changed successfully", { userId: user.id });
|
|
456
|
+
|
|
457
|
+
return { message: `${key} changed successfully.` };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private async hashPassword(password: string): Promise<string> {
|
|
461
|
+
return argon2.hash(password);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private async verifyPassword(
|
|
465
|
+
password: string,
|
|
466
|
+
hash: string
|
|
467
|
+
): Promise<boolean> {
|
|
468
|
+
return argon2.verify(hash, password);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private async findUserByIdentifier(
|
|
472
|
+
identifier: string,
|
|
473
|
+
include: Prisma.UserInclude = {}
|
|
474
|
+
) {
|
|
475
|
+
const { key, value, query } = this.parseIdentifier(identifier);
|
|
476
|
+
const user = await this.prisma.user.findUnique({
|
|
477
|
+
where: query,
|
|
478
|
+
include,
|
|
479
|
+
});
|
|
480
|
+
if (!user) throw new BadRequestException("User not found");
|
|
481
|
+
return { user, key, value };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private async checkVerificationStatus(
|
|
485
|
+
user: User,
|
|
486
|
+
key: IdentifierKey,
|
|
487
|
+
value: string,
|
|
488
|
+
check: "verified" | "unverified"
|
|
489
|
+
) {
|
|
490
|
+
const isVerified =
|
|
491
|
+
key === "email" ? user.isEmailVerified : user.isPhoneVerified;
|
|
492
|
+
|
|
493
|
+
if (check === "verified" && isVerified) {
|
|
494
|
+
throw new BadRequestException(`${key} is already verified.`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (check === "unverified" && !isVerified) {
|
|
498
|
+
await this.otpService.sendOtp({
|
|
499
|
+
userId: user.id,
|
|
500
|
+
identifier: value,
|
|
501
|
+
purpose: "verifyIdentifier",
|
|
502
|
+
metadata: { user },
|
|
503
|
+
});
|
|
504
|
+
this.logger.log(
|
|
505
|
+
"📨 Auto-sent verification OTP due to unverified identifier",
|
|
506
|
+
{ userId: user.id, key, value }
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
throw new UnauthorizedException(`${key} not verified`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private parseIdentifier(identifier: string): {
|
|
514
|
+
key: IdentifierKey;
|
|
515
|
+
value: string;
|
|
516
|
+
query: Prisma.UserWhereUniqueInput;
|
|
517
|
+
} {
|
|
518
|
+
const isEmail = identifier.includes("@");
|
|
519
|
+
const key = isEmail ? "email" : "phone";
|
|
520
|
+
const value = isEmail ? identifier.toLowerCase() : identifier;
|
|
521
|
+
const query = key === "email" ? { email: value } : { phone: value };
|
|
522
|
+
|
|
523
|
+
return { key, value, query };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Controller, Get, Req, Res, UseGuards } from "@nestjs/common";
|
|
2
|
+
import { AuthGuard } from "@nestjs/passport";
|
|
3
|
+
import { Public } from "@decorators/public.decorator";
|
|
4
|
+
import type { Request, Response } from "express";
|
|
5
|
+
import { TokenService } from "@modules/token/token.service";
|
|
6
|
+
import { PrismaService } from "@modules/prisma/prisma.service";
|
|
7
|
+
|
|
8
|
+
@Controller("oauth")
|
|
9
|
+
export class OAuthController {
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly tokenService: TokenService,
|
|
12
|
+
private readonly prisma: PrismaService
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
@Public()
|
|
16
|
+
@Get("google")
|
|
17
|
+
@UseGuards(AuthGuard("google"))
|
|
18
|
+
googleLogin() {}
|
|
19
|
+
|
|
20
|
+
@Public()
|
|
21
|
+
@Get("google/callback")
|
|
22
|
+
@UseGuards(AuthGuard("google"))
|
|
23
|
+
async googleCallback(
|
|
24
|
+
@Req() req: Request,
|
|
25
|
+
@Res({ passthrough: true }) res: Response
|
|
26
|
+
) {
|
|
27
|
+
const user = req.user!;
|
|
28
|
+
await this.tokenService.createAuthSession(req, res, user);
|
|
29
|
+
await this.prisma.user.update({
|
|
30
|
+
where: { id: user.id },
|
|
31
|
+
data: { lastLoginAt: new Date() },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return res.redirect("/");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Public()
|
|
38
|
+
@Get("facebook")
|
|
39
|
+
@UseGuards(AuthGuard("facebook"))
|
|
40
|
+
facebookLogin() {}
|
|
41
|
+
|
|
42
|
+
@Public()
|
|
43
|
+
@Get("facebook/callback")
|
|
44
|
+
@UseGuards(AuthGuard("facebook"))
|
|
45
|
+
async facebookCallback(
|
|
46
|
+
@Req() req: Request,
|
|
47
|
+
@Res({ passthrough: true }) res: Response
|
|
48
|
+
) {
|
|
49
|
+
const user = req.user!;
|
|
50
|
+
await this.tokenService.createAuthSession(req, res, user);
|
|
51
|
+
await this.prisma.user.update({
|
|
52
|
+
where: { id: user.id },
|
|
53
|
+
data: { lastLoginAt: new Date() },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return res.redirect("/");
|
|
57
|
+
}
|
|
58
|
+
}
|