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.
@@ -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
+ };