Skip to content

Commit dbc338b

Browse files
committed
Update: Update Backend Auth Module for Signup Flow (Model, Controller, Validator, Types)
1 parent bc93691 commit dbc338b

12 files changed

Lines changed: 159 additions & 48 deletions

File tree

LocalMind-Backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@types/express": "^5.0.3",
2828
"@types/node": "^24.7.2",
2929
"@types/nodemailer": "^7.0.3",
30+
"@types/bcrypt": "^5.0.2",
3031
"jest": "^30.2.0",
3132
"prettier": "3.6.2",
3233
"ts-jest": "^29.4.5",
@@ -46,6 +47,7 @@
4647
"@types/mongoose": "^5.11.97",
4748
"@types/morgan": "^1.9.10",
4849
"argon2": "^0.44.0",
50+
"bcrypt": "^5.1.1",
4951
"axios": "^1.12.2",
5052
"chalk": "^5.6.2",
5153
"cloudflared-tunnel": "^1.0.3",

LocalMind-Backend/src/api/v1/user/__test__/user.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ describe('User Registration Tests', () => {
3838
}
3939

4040
try {
41-
const res = await axios.post(`${env.BACKEND_URL}/user/register`, {
42-
name: 'Test User',
41+
const res = await axios.post(`${env.BACKEND_URL}/auth/signup`, {
42+
firstName: 'Test User',
43+
birthPlace: 'Test City',
44+
location: 'Test Country',
4345
email: testEmail,
4446
password: 'Test@1234',
4547
})
@@ -70,8 +72,10 @@ describe('User Registration Tests', () => {
7072
}
7173

7274
try {
73-
const res = await axios.post(`${env.BACKEND_URL}/user/register`, {
74-
name: 'Duplicate User',
75+
const res = await axios.post(`${env.BACKEND_URL}/auth/signup`, {
76+
firstName: 'Duplicate User',
77+
birthPlace: 'Duplicate City',
78+
location: 'Duplicate Country',
7579
email: testEmail,
7680
password: 'Test@1234',
7781
})
@@ -81,7 +85,7 @@ describe('User Registration Tests', () => {
8185
throw Error('Should not be able to register with existing email')
8286
} catch (error: any) {
8387
expect(error.response).toBeDefined()
84-
expect(error.response.status).toBe(404)
88+
expect(error.response.status).toBe(409)
8589
expect(error.response.data.message).toMatch(/already exists/i)
8690
}
8791
}, 10000)

LocalMind-Backend/src/api/v1/user/user.constant.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ enum UserConstant {
5858
INVALID_INPUT = 'User is not available in request',
5959
INVALID_CREDENTIALS = 'Invalid credentials',
6060
NAME_REQUIRED = 'Name is required!',
61+
FIRST_NAME_REQUIRED = 'First name is required',
62+
BIRTH_PLACE_REQUIRED = 'Birth place is required',
63+
LOCATION_REQUIRED = 'Location is required',
64+
INVALID_ROLE = 'Invalid user role',
65+
INVALID_PORTFOLIO_URL = 'Portfolio URL is invalid',
66+
BIO_TOO_LONG = 'Bio exceeds the maximum allowed length',
6167

6268
// ✅ DATABASE & SERVER ERRORS
6369

@@ -82,3 +88,15 @@ enum UserConstant {
8288
}
8389

8490
export default UserConstant
91+
92+
export const AllowedUserRoles = ['user', 'admin', 'creator'] as const
93+
94+
export const PasswordConfig = {
95+
minLength: 8,
96+
maxLength: 20,
97+
saltRounds: 10,
98+
}
99+
100+
export const BioConfig = {
101+
maxLength: 500,
102+
}

LocalMind-Backend/src/api/v1/user/user.controller.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ class UserController {
3030
try {
3131
const validatedData = await userRegisterSchema.parseAsync(req.body)
3232

33-
if (!req.body.password) {
34-
throw new Error(UserConstant.PASSWORD_REQUIRED)
33+
const existingUser = await UserUtils.findUserByEmail(validatedData.email)
34+
35+
if (existingUser) {
36+
throw new Error(UserConstant.EMAIL_ALREADY_EXISTS)
3537
}
3638

3739
const user = await userService.createUser(validatedData)
3840

39-
const { password: _password, ...userObj } = user
41+
const userObj = UserUtils.sanitizeUser(user)
4042

4143
const token = UserUtils.generateToken({
4244
userId: String(user._id),
@@ -48,6 +50,14 @@ class UserController {
4850

4951
SendResponse.success(res, UserConstant.CREATE_USER_SUCCESS, { userObj, token }, 201)
5052
} catch (err: any) {
53+
if (err.message === UserConstant.EMAIL_ALREADY_EXISTS) {
54+
SendResponse.error(res, err.message, 409)
55+
return
56+
}
57+
if (err?.name === 'ZodError') {
58+
SendResponse.error(res, err.message || UserConstant.CREATE_USER_FAILED, 400, err)
59+
return
60+
}
5161
SendResponse.error(res, err.message || UserConstant.CREATE_USER_FAILED, 500, err)
5262
}
5363
}
@@ -68,6 +78,10 @@ class UserController {
6878

6979
SendResponse.success(res, UserConstant.LOGIN_USER_SUCCESS, { user, token }, StatusConstant.OK)
7080
} catch (err: any) {
81+
if (err?.name === 'ZodError') {
82+
SendResponse.error(res, err.message || UserConstant.INVALID_CREDENTIALS, 400, err)
83+
return
84+
}
7185
SendResponse.error(res, err.message || UserConstant.INVALID_CREDENTIALS, 401, err)
7286
}
7387
}
@@ -91,7 +105,6 @@ class UserController {
91105
}
92106

93107
const userObj: Partial<IUser> = { ...user }
94-
delete userObj.password
95108

96109
SendResponse.success(res, UserConstant.USER_PROFILE_SUCCESS, userObj, 200)
97110
} catch (err: any) {

LocalMind-Backend/src/api/v1/user/user.middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class UserMiddleware {
1212
throw new Error(UserConstant.TOKEN_MISSING)
1313
}
1414

15-
const decodedData: IUser | null = UserUtils.verifyToken(token)
15+
const decodedData: Partial<IUser> | null = UserUtils.verifyToken(token)
1616

1717
if (!decodedData) {
1818
throw new Error(UserConstant.INVALID_TOKEN)

LocalMind-Backend/src/api/v1/user/user.model.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import mongoose, { Schema, Model } from 'mongoose'
22
import { IUser } from './user.type'
3+
import { AllowedUserRoles } from './user.constant'
34

45
const userSchema: Schema<IUser> = new Schema<IUser>(
56
{
6-
name: {
7+
firstName: {
78
type: String,
89
required: true,
10+
trim: true,
911
},
1012
email: {
1113
type: String,
1214
required: true,
1315
unique: true,
1416
index: true,
17+
lowercase: true,
18+
trim: true,
1519
},
1620
password: {
1721
type: String,
@@ -20,8 +24,29 @@ const userSchema: Schema<IUser> = new Schema<IUser>(
2024
},
2125
role: {
2226
type: String,
27+
enum: AllowedUserRoles,
2328
default: 'user',
2429
},
30+
birthPlace: {
31+
type: String,
32+
required: true,
33+
trim: true,
34+
},
35+
location: {
36+
type: String,
37+
required: true,
38+
trim: true,
39+
},
40+
portfolioUrl: {
41+
type: String,
42+
default: null,
43+
trim: true,
44+
},
45+
bio: {
46+
type: String,
47+
default: null,
48+
trim: true,
49+
},
2550
apikey: {
2651
type: String,
2752
default: null,

LocalMind-Backend/src/api/v1/user/user.routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import userMiddleware from './user.middleware'
66

77
router.post('/v1/user/register', userController.register)
88

9+
router.post('/v1/auth/signup', userController.register)
10+
911
router.post('/v1/user/login', userController.login)
12+
router.post('/v1/auth/login', userController.login)
1013

1114
router.get('/v1/user/apiKey/generate', userMiddleware.middleware, userController.apiEndPointCreater)
1215

LocalMind-Backend/src/api/v1/user/user.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ class userService {
2828
const user = await User.create({
2929
...data,
3030
password: hashPassword,
31+
role: data.role || 'user',
32+
portfolioUrl: data.portfolioUrl ?? null,
33+
bio: data.bio ?? null,
3134
})
3235
return user
3336
}
Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
import { AllowedUserRoles } from './user.constant'
2+
3+
export type UserRole = (typeof AllowedUserRoles)[number]
4+
15
export interface IUser {
2-
_id?: string | undefined
3-
name?: string | null
6+
_id?: string
7+
firstName: string
48
email: string
59
password?: string
6-
role?: string
10+
role?: UserRole
11+
birthPlace: string
12+
location: string
13+
portfolioUrl?: string | null
14+
bio?: string | null
715
apikey?: string | null
816
model?: string | null
917
modelApiKey?: string | null
18+
createdAt?: Date
19+
updatedAt?: Date
1020
}

LocalMind-Backend/src/api/v1/user/user.utils.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import jwt, { SignOptions } from 'jsonwebtoken'
22
import { env } from '../../../constant/env.constant'
33
import { IUser } from './user.type'
44
import User from './user.model'
5+
import bcrypt from 'bcrypt'
56
import * as argon2 from 'argon2'
6-
import UserConstant from './user.constant'
7+
import UserConstant, { PasswordConfig } from './user.constant'
78

89
export interface JwtPayload {
910
userId: string
@@ -21,7 +22,7 @@ class UserUtils {
2122
} as SignOptions)
2223
}
2324

24-
public static verifyToken(token: string): IUser | null {
25+
public static verifyToken(token: string): Partial<IUser> | null {
2526
try {
2627
const decoded = jwt.verify(token, this.JWT_SECRET)
2728

@@ -61,11 +62,11 @@ class UserUtils {
6162
if (!PassMatch) {
6263
throw new Error(UserConstant.INVALID_PASSWORD)
6364
}
64-
const userObj: IUser = user.toObject()
65-
delete (userObj as { password?: string }).password
66-
delete (userObj as { createdAt?: Date }).createdAt
67-
delete (userObj as { updatedAt?: Date }).updatedAt
68-
delete (userObj as { __v?: number }).__v
65+
const userObj = this.sanitizeUser(user)
66+
67+
if (!userObj) {
68+
throw new Error(UserConstant.USER_NOT_FOUND)
69+
}
6970

7071
const token = this.generateToken({
7172
userId: String(user._id),
@@ -84,22 +85,16 @@ class UserUtils {
8485

8586
public static async findById(userId: string): Promise<IUser | null> {
8687
const user = await User.findById(userId)
87-
return user
88+
return this.sanitizeUser(user) as IUser | null
8889
}
8990

9091
public static async passwordHash(password: string): Promise<string> {
91-
return await argon2.hash(password)
92+
return bcrypt.hash(password, PasswordConfig.saltRounds)
9293
}
9394

9495
public static async findUserByEmail(email: string): Promise<Partial<IUser> | null> {
9596
const user = await User.findOne({ email })
96-
if (!user) return null
97-
const userObj = user.toObject() as IUser
98-
delete (userObj as { password?: string }).password
99-
delete (userObj as { createdAt?: Date }).createdAt
100-
delete (userObj as { updatedAt?: Date }).updatedAt
101-
delete (userObj as { __v?: number }).__v
102-
return userObj
97+
return this.sanitizeUser(user)
10398
}
10499

105100
private static async passwordMatching({
@@ -109,7 +104,24 @@ class UserUtils {
109104
dbPass: string
110105
userPass: string
111106
}): Promise<boolean> {
112-
return await argon2.verify(dbPass, userPass)
107+
const isBcryptMatch = await bcrypt.compare(userPass, dbPass)
108+
if (isBcryptMatch) return true
109+
110+
try {
111+
return await argon2.verify(dbPass, userPass)
112+
} catch {
113+
return false
114+
}
115+
}
116+
public static sanitizeUser(user: IUser | null): Partial<IUser> | null {
117+
if (!user) return null
118+
const userObj = typeof (user as any).toObject === 'function' ? (user as any).toObject() : { ...user }
119+
120+
delete (userObj as { password?: string }).password
121+
delete (userObj as { __v?: number }).__v
122+
delete (userObj as { apikey?: string | null }).apikey
123+
delete (userObj as { modelApiKey?: string | null }).modelApiKey
124+
return userObj as Partial<IUser>
113125
}
114126
public static maskApiKey(apiKey: string): string {
115127
if (!apiKey || apiKey.length < 8) return '*'

0 commit comments

Comments
 (0)