create-arktos 1.0.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/.env.example +69 -0
- package/LICENSE +21 -0
- package/README.md +533 -0
- package/bin/cli.js +318 -0
- package/eslint.config.js +79 -0
- package/package.json +108 -0
- package/prisma/schema.prisma +172 -0
- package/src/app.ts +101 -0
- package/src/config/env.validation.ts +15 -0
- package/src/config/logger.ts +76 -0
- package/src/constants/errorCodes.ts +59 -0
- package/src/constants/messages.ts +190 -0
- package/src/controllers/auth.controller.ts +546 -0
- package/src/middleware/index.ts +638 -0
- package/src/routes/auth.routes.ts +29 -0
- package/src/routes/index.ts +63 -0
- package/src/schemas/index.ts +139 -0
- package/src/services/database.service.ts +120 -0
- package/src/services/email.service.ts +244 -0
- package/src/services/jwt.service.ts +227 -0
- package/src/types/express.d.ts +19 -0
- package/src/types/index.ts +156 -0
- package/src/utils/response.ts +55 -0
- package/src/views/emails/notification.html +87 -0
- package/src/views/emails/resetPassword.html +118 -0
- package/src/views/emails/verification.html +107 -0
- package/src/views/emails/welcome.html +113 -0
- package/template.package.json +82 -0
- package/tsconfig.json +35 -0
- package/vercel.json +34 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { AuthenticatedRequest } from '../types';
|
|
5
|
+
import { createSuccessResponse, createErrorResponse } from '../utils/response';
|
|
6
|
+
import { validation } from '../middleware';
|
|
7
|
+
import {
|
|
8
|
+
registerSchema,
|
|
9
|
+
loginSchema,
|
|
10
|
+
refreshTokenSchema,
|
|
11
|
+
resendVerificationSchema,
|
|
12
|
+
forgotPasswordSchema,
|
|
13
|
+
resetPasswordSchema,
|
|
14
|
+
updateProfileSchema,
|
|
15
|
+
changePasswordSchema
|
|
16
|
+
} from '../schemas';
|
|
17
|
+
import DatabaseService from '../services/database.service';
|
|
18
|
+
import logger from '../config/logger';
|
|
19
|
+
import { ERROR_CODES } from '../constants/errorCodes';
|
|
20
|
+
|
|
21
|
+
const dbService = DatabaseService.getInstance();
|
|
22
|
+
const prisma = dbService.getClient();
|
|
23
|
+
|
|
24
|
+
interface TokenPair {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
refreshToken: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const generateTokens = (userId: string): TokenPair => {
|
|
30
|
+
const accessToken = jwt.sign(
|
|
31
|
+
{ userId },
|
|
32
|
+
process.env.JWT_SECRET as string
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const refreshToken = jwt.sign(
|
|
36
|
+
{ userId },
|
|
37
|
+
process.env.JWT_REFRESH_SECRET as string
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return { accessToken, refreshToken };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const logLoginAttempt = async (
|
|
44
|
+
userId: string | 'unknown',
|
|
45
|
+
req: Request,
|
|
46
|
+
isSuccess: boolean,
|
|
47
|
+
failReason?: string
|
|
48
|
+
): Promise<void> => {
|
|
49
|
+
try {
|
|
50
|
+
if (userId === 'unknown') return;
|
|
51
|
+
|
|
52
|
+
const clientIP = req.ip || req.socket.remoteAddress || 'unknown';
|
|
53
|
+
const userAgent = req.headers['user-agent'] || 'unknown';
|
|
54
|
+
|
|
55
|
+
await prisma.loginLog.create({
|
|
56
|
+
data: {
|
|
57
|
+
userId,
|
|
58
|
+
loginType: 'EMAIL',
|
|
59
|
+
ipAddress: clientIP,
|
|
60
|
+
userAgent,
|
|
61
|
+
isSuccess,
|
|
62
|
+
failReason,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logger.error('Failed to log login attempt:', error);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const register = async (req: Request, res: Response): Promise<void> => {
|
|
71
|
+
try {
|
|
72
|
+
const validatedData = validation.request(registerSchema, req.body);
|
|
73
|
+
const { email, password, firstName, lastName, username } = validatedData;
|
|
74
|
+
|
|
75
|
+
// Check if user exists
|
|
76
|
+
const existingUser = await prisma.user.findUnique({
|
|
77
|
+
where: { email: email.toLowerCase() }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (existingUser) {
|
|
81
|
+
res.status(409).json(createErrorResponse(
|
|
82
|
+
'User already exists with this email',
|
|
83
|
+
ERROR_CODES.AUTH_USER_EXISTS
|
|
84
|
+
));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check username if provided
|
|
89
|
+
if (username) {
|
|
90
|
+
const existingUsername = await prisma.user.findUnique({
|
|
91
|
+
where: { username }
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (existingUsername) {
|
|
95
|
+
res.status(409).json(createErrorResponse(
|
|
96
|
+
'Username already taken',
|
|
97
|
+
ERROR_CODES.AUTH_USERNAME_TAKEN
|
|
98
|
+
));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Hash password
|
|
104
|
+
const bcrypt = require('bcryptjs');
|
|
105
|
+
const saltRounds = parseInt(process.env.BCRYPT_SALT_ROUNDS || '12');
|
|
106
|
+
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
|
107
|
+
|
|
108
|
+
// Create user
|
|
109
|
+
const user = await prisma.user.create({
|
|
110
|
+
data: {
|
|
111
|
+
email: email.toLowerCase(),
|
|
112
|
+
password: hashedPassword,
|
|
113
|
+
firstName,
|
|
114
|
+
lastName,
|
|
115
|
+
username,
|
|
116
|
+
},
|
|
117
|
+
select: {
|
|
118
|
+
id: true,
|
|
119
|
+
email: true,
|
|
120
|
+
firstName: true,
|
|
121
|
+
lastName: true,
|
|
122
|
+
username: true,
|
|
123
|
+
isEmailVerified: true,
|
|
124
|
+
createdAt: true,
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Create email verification
|
|
129
|
+
const verificationToken = crypto.randomBytes(32).toString('hex');
|
|
130
|
+
|
|
131
|
+
await prisma.emailVerification.create({
|
|
132
|
+
data: {
|
|
133
|
+
userId: user.id,
|
|
134
|
+
token: verificationToken,
|
|
135
|
+
email: user.email,
|
|
136
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Send verification email (implement email service)
|
|
141
|
+
// await emailService.sendVerificationEmail(user.email, verificationToken, user.firstName);
|
|
142
|
+
|
|
143
|
+
await logLoginAttempt(user.id, req, false, 'REGISTRATION_PENDING_VERIFICATION');
|
|
144
|
+
|
|
145
|
+
logger.info(`New user registered: ${user.email}`);
|
|
146
|
+
|
|
147
|
+
res.status(201).json(createSuccessResponse(
|
|
148
|
+
{ user },
|
|
149
|
+
'Registration successful. Please check your email to verify your account.',
|
|
150
|
+
'REGISTRATION_SUCCESS'
|
|
151
|
+
));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.error('Registration error:', error);
|
|
154
|
+
res.status(500).json(createErrorResponse(
|
|
155
|
+
'Internal server error',
|
|
156
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
157
|
+
));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const login = async (req: Request, res: Response): Promise<void> => {
|
|
162
|
+
try {
|
|
163
|
+
const validatedData = validation.request(loginSchema, req.body);
|
|
164
|
+
const { email, password } = validatedData;
|
|
165
|
+
|
|
166
|
+
// Find user
|
|
167
|
+
const user = await prisma.user.findUnique({
|
|
168
|
+
where: { email: email.toLowerCase() }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!user) {
|
|
172
|
+
await logLoginAttempt('unknown', req, false, 'USER_NOT_FOUND');
|
|
173
|
+
res.status(401).json(createErrorResponse(
|
|
174
|
+
'Invalid credentials',
|
|
175
|
+
ERROR_CODES.AUTH_INVALID_CREDENTIALS
|
|
176
|
+
));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!user.isActive) {
|
|
181
|
+
await logLoginAttempt(user.id, req, false, 'ACCOUNT_DEACTIVATED');
|
|
182
|
+
res.status(403).json(createErrorResponse(
|
|
183
|
+
'Account is deactivated',
|
|
184
|
+
ERROR_CODES.AUTH_ACCOUNT_DEACTIVATED
|
|
185
|
+
));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Verify password
|
|
190
|
+
const bcrypt = require('bcryptjs');
|
|
191
|
+
const isPasswordValid = await bcrypt.compare(password, user.password);
|
|
192
|
+
|
|
193
|
+
if (!isPasswordValid) {
|
|
194
|
+
await logLoginAttempt(user.id, req, false, 'INVALID_PASSWORD');
|
|
195
|
+
res.status(401).json(createErrorResponse(
|
|
196
|
+
'Invalid credentials',
|
|
197
|
+
ERROR_CODES.AUTH_INVALID_CREDENTIALS
|
|
198
|
+
));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Update last login
|
|
203
|
+
await prisma.user.update({
|
|
204
|
+
where: { id: user.id },
|
|
205
|
+
data: { lastLoginAt: new Date() }
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Generate tokens
|
|
209
|
+
const { accessToken, refreshToken } = generateTokens(user.id);
|
|
210
|
+
|
|
211
|
+
// Store refresh token
|
|
212
|
+
const expiresAt = new Date();
|
|
213
|
+
expiresAt.setDate(expiresAt.getDate() + 7); // 7 days
|
|
214
|
+
|
|
215
|
+
await prisma.refreshToken.create({
|
|
216
|
+
data: {
|
|
217
|
+
userId: user.id,
|
|
218
|
+
token: refreshToken,
|
|
219
|
+
expiresAt,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await logLoginAttempt(user.id, req, true);
|
|
224
|
+
|
|
225
|
+
logger.info(`User logged in: ${user.email}`);
|
|
226
|
+
|
|
227
|
+
const userResponse = {
|
|
228
|
+
id: user.id,
|
|
229
|
+
email: user.email,
|
|
230
|
+
firstName: user.firstName,
|
|
231
|
+
lastName: user.lastName,
|
|
232
|
+
username: user.username,
|
|
233
|
+
avatar: user.avatar,
|
|
234
|
+
role: user.role,
|
|
235
|
+
isEmailVerified: user.isEmailVerified,
|
|
236
|
+
lastLoginAt: user.lastLoginAt,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
res.json(createSuccessResponse(
|
|
240
|
+
{
|
|
241
|
+
user: userResponse,
|
|
242
|
+
tokens: { accessToken, refreshToken }
|
|
243
|
+
},
|
|
244
|
+
'Login successful',
|
|
245
|
+
'LOGIN_SUCCESS'
|
|
246
|
+
));
|
|
247
|
+
} catch (error) {
|
|
248
|
+
logger.error('Login error:', error);
|
|
249
|
+
res.status(500).json(createErrorResponse(
|
|
250
|
+
'Internal server error',
|
|
251
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
252
|
+
));
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const refreshToken = async (req: Request, res: Response): Promise<void> => {
|
|
257
|
+
try {
|
|
258
|
+
const validatedData = validation.request(refreshTokenSchema, req.body);
|
|
259
|
+
const { refreshToken: token } = validatedData;
|
|
260
|
+
|
|
261
|
+
// Verify refresh token
|
|
262
|
+
let decoded: any;
|
|
263
|
+
try {
|
|
264
|
+
decoded = jwt.verify(token, process.env.JWT_REFRESH_SECRET!);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
res.status(401).json(createErrorResponse(
|
|
267
|
+
'Invalid refresh token',
|
|
268
|
+
ERROR_CODES.AUTH_TOKEN_EXPIRED
|
|
269
|
+
));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check if token exists and is not revoked
|
|
274
|
+
const tokenRecord = await prisma.refreshToken.findUnique({
|
|
275
|
+
where: { token },
|
|
276
|
+
include: { user: true },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (!tokenRecord || tokenRecord.isRevoked || tokenRecord.expiresAt < new Date()) {
|
|
280
|
+
res.status(401).json(createErrorResponse(
|
|
281
|
+
'Invalid or expired refresh token',
|
|
282
|
+
ERROR_CODES.AUTH_TOKEN_EXPIRED
|
|
283
|
+
));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Generate new tokens
|
|
288
|
+
const { accessToken, refreshToken: newRefreshToken } = generateTokens(decoded.userId);
|
|
289
|
+
|
|
290
|
+
// Revoke old token
|
|
291
|
+
await prisma.refreshToken.update({
|
|
292
|
+
where: { id: tokenRecord.id },
|
|
293
|
+
data: { isRevoked: true },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Create new refresh token
|
|
297
|
+
const expiresAt = new Date();
|
|
298
|
+
expiresAt.setDate(expiresAt.getDate() + 7); // 7 days
|
|
299
|
+
|
|
300
|
+
await prisma.refreshToken.create({
|
|
301
|
+
data: {
|
|
302
|
+
userId: decoded.userId,
|
|
303
|
+
token: newRefreshToken,
|
|
304
|
+
expiresAt,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
res.json(createSuccessResponse(
|
|
309
|
+
{
|
|
310
|
+
tokens: {
|
|
311
|
+
accessToken,
|
|
312
|
+
refreshToken: newRefreshToken,
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
'Tokens refreshed successfully',
|
|
316
|
+
'TOKEN_REFRESH_SUCCESS'
|
|
317
|
+
));
|
|
318
|
+
} catch (error) {
|
|
319
|
+
logger.error('Token refresh error:', error);
|
|
320
|
+
res.status(500).json(createErrorResponse(
|
|
321
|
+
'Internal server error',
|
|
322
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
323
|
+
));
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
export const logout = async (req: Request, res: Response): Promise<void> => {
|
|
328
|
+
try {
|
|
329
|
+
const { refreshToken } = req.body;
|
|
330
|
+
|
|
331
|
+
if (refreshToken) {
|
|
332
|
+
await prisma.refreshToken.updateMany({
|
|
333
|
+
where: { token: refreshToken },
|
|
334
|
+
data: { isRevoked: true },
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
logger.info(`User logged out: ${(req as AuthenticatedRequest).user?.email || 'Unknown'}`);
|
|
339
|
+
|
|
340
|
+
res.json(createSuccessResponse(
|
|
341
|
+
null,
|
|
342
|
+
'Logged out successfully',
|
|
343
|
+
'LOGOUT_SUCCESS'
|
|
344
|
+
));
|
|
345
|
+
} catch (error) {
|
|
346
|
+
logger.error('Logout error:', error);
|
|
347
|
+
res.status(500).json(createErrorResponse(
|
|
348
|
+
'Internal server error',
|
|
349
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
350
|
+
));
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export const verifyEmail = async (req: Request, res: Response): Promise<void> => {
|
|
355
|
+
try {
|
|
356
|
+
const { token } = req.params;
|
|
357
|
+
|
|
358
|
+
const verification = await prisma.emailVerification.findUnique({
|
|
359
|
+
where: { token },
|
|
360
|
+
include: { user: true },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
if (!verification) {
|
|
364
|
+
res.status(400).json(createErrorResponse(
|
|
365
|
+
'Invalid verification token',
|
|
366
|
+
ERROR_CODES.AUTH_INVALID_TOKEN
|
|
367
|
+
));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (verification.expiresAt < new Date()) {
|
|
372
|
+
res.status(400).json(createErrorResponse(
|
|
373
|
+
'Verification token has expired',
|
|
374
|
+
ERROR_CODES.AUTH_TOKEN_EXPIRED
|
|
375
|
+
));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (verification.status === 'VERIFIED') {
|
|
380
|
+
res.json(createSuccessResponse(
|
|
381
|
+
null,
|
|
382
|
+
'Email already verified',
|
|
383
|
+
'EMAIL_ALREADY_VERIFIED'
|
|
384
|
+
));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Update user and verification status
|
|
389
|
+
await prisma.user.update({
|
|
390
|
+
where: { id: verification.userId },
|
|
391
|
+
data: { isEmailVerified: true }
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await prisma.emailVerification.update({
|
|
395
|
+
where: { id: verification.id },
|
|
396
|
+
data: { status: 'VERIFIED' },
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
logger.info(`Email verified for user: ${verification.user.email}`);
|
|
400
|
+
|
|
401
|
+
res.json(createSuccessResponse(
|
|
402
|
+
null,
|
|
403
|
+
'Email verified successfully',
|
|
404
|
+
'EMAIL_VERIFICATION_SUCCESS'
|
|
405
|
+
));
|
|
406
|
+
} catch (error) {
|
|
407
|
+
logger.error('Email verification error:', error);
|
|
408
|
+
res.status(500).json(createErrorResponse(
|
|
409
|
+
'Internal server error',
|
|
410
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
411
|
+
));
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
export const getProfile = async (req: AuthenticatedRequest, res: Response): Promise<void> => {
|
|
416
|
+
try {
|
|
417
|
+
res.json(createSuccessResponse(
|
|
418
|
+
{ user: req.user },
|
|
419
|
+
'Profile retrieved successfully',
|
|
420
|
+
'PROFILE_SUCCESS'
|
|
421
|
+
));
|
|
422
|
+
} catch (error) {
|
|
423
|
+
logger.error('Get profile error:', error);
|
|
424
|
+
res.status(500).json(createErrorResponse(
|
|
425
|
+
'Internal server error',
|
|
426
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
427
|
+
));
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
export const updateProfile = async (req: AuthenticatedRequest, res: Response): Promise<void> => {
|
|
432
|
+
try {
|
|
433
|
+
const validatedData = validation.request(updateProfileSchema, req.body);
|
|
434
|
+
const { firstName, lastName, username, avatar } = validatedData;
|
|
435
|
+
|
|
436
|
+
const updateData: any = {};
|
|
437
|
+
if (firstName !== undefined) updateData.firstName = firstName;
|
|
438
|
+
if (lastName !== undefined) updateData.lastName = lastName;
|
|
439
|
+
if (avatar !== undefined) updateData.avatar = avatar;
|
|
440
|
+
|
|
441
|
+
if (username !== undefined && username !== req.user!.username) {
|
|
442
|
+
const existingUsername = await prisma.user.findUnique({
|
|
443
|
+
where: { username }
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (existingUsername && existingUsername.id !== req.user!.id) {
|
|
447
|
+
res.status(409).json(createErrorResponse(
|
|
448
|
+
'Username already taken',
|
|
449
|
+
ERROR_CODES.AUTH_USERNAME_TAKEN
|
|
450
|
+
));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
updateData.username = username;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const updatedUser = await prisma.user.update({
|
|
457
|
+
where: { id: req.user!.id },
|
|
458
|
+
data: updateData,
|
|
459
|
+
select: {
|
|
460
|
+
id: true,
|
|
461
|
+
email: true,
|
|
462
|
+
firstName: true,
|
|
463
|
+
lastName: true,
|
|
464
|
+
username: true,
|
|
465
|
+
avatar: true,
|
|
466
|
+
role: true,
|
|
467
|
+
isEmailVerified: true,
|
|
468
|
+
lastLoginAt: true,
|
|
469
|
+
updatedAt: true,
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
logger.info(`Profile updated for user: ${req.user!.email}`);
|
|
474
|
+
|
|
475
|
+
res.json(createSuccessResponse(
|
|
476
|
+
{ user: updatedUser },
|
|
477
|
+
'Profile updated successfully',
|
|
478
|
+
'PROFILE_UPDATE_SUCCESS'
|
|
479
|
+
));
|
|
480
|
+
} catch (error) {
|
|
481
|
+
logger.error('Update profile error:', error);
|
|
482
|
+
res.status(500).json(createErrorResponse(
|
|
483
|
+
'Internal server error',
|
|
484
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
485
|
+
));
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
export const changePassword = async (req: AuthenticatedRequest, res: Response): Promise<void> => {
|
|
490
|
+
try {
|
|
491
|
+
const validatedData = validation.request(changePasswordSchema, req.body);
|
|
492
|
+
const { currentPassword, newPassword } = validatedData;
|
|
493
|
+
|
|
494
|
+
const user = await prisma.user.findUnique({
|
|
495
|
+
where: { id: req.user!.id }
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (!user) {
|
|
499
|
+
res.status(404).json(createErrorResponse(
|
|
500
|
+
'User not found',
|
|
501
|
+
ERROR_CODES.AUTH_USER_NOT_FOUND
|
|
502
|
+
));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const bcrypt = require('bcryptjs');
|
|
507
|
+
const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.password);
|
|
508
|
+
|
|
509
|
+
if (!isCurrentPasswordValid) {
|
|
510
|
+
res.status(400).json(createErrorResponse(
|
|
511
|
+
'Current password is incorrect',
|
|
512
|
+
ERROR_CODES.AUTH_INVALID_PASSWORD
|
|
513
|
+
));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Hash new password
|
|
518
|
+
const saltRounds = parseInt(process.env.BCRYPT_SALT_ROUNDS || '12');
|
|
519
|
+
const hashedNewPassword = await bcrypt.hash(newPassword, saltRounds);
|
|
520
|
+
|
|
521
|
+
await prisma.user.update({
|
|
522
|
+
where: { id: req.user!.id },
|
|
523
|
+
data: { password: hashedNewPassword }
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Revoke all refresh tokens
|
|
527
|
+
await prisma.refreshToken.updateMany({
|
|
528
|
+
where: { userId: req.user!.id },
|
|
529
|
+
data: { isRevoked: true },
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
logger.info(`Password changed for user: ${req.user!.email}`);
|
|
533
|
+
|
|
534
|
+
res.json(createSuccessResponse(
|
|
535
|
+
null,
|
|
536
|
+
'Password changed successfully. Please log in again.',
|
|
537
|
+
'PASSWORD_CHANGE_SUCCESS'
|
|
538
|
+
));
|
|
539
|
+
} catch (error) {
|
|
540
|
+
logger.error('Change password error:', error);
|
|
541
|
+
res.status(500).json(createErrorResponse(
|
|
542
|
+
'Internal server error',
|
|
543
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
544
|
+
));
|
|
545
|
+
}
|
|
546
|
+
};
|