From 457cca2171d91ef626ac42d4bc22f52afcf02924 Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 27 Jul 2026 07:33:01 +0100 Subject: [PATCH 1/7] implemneted the authenticated profile --- Backend/src/services/auth.service.ts | 3 ++- Backend/src/validators/auth.validator.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Backend/src/services/auth.service.ts b/Backend/src/services/auth.service.ts index 4744c4b..5f1a19a 100644 --- a/Backend/src/services/auth.service.ts +++ b/Backend/src/services/auth.service.ts @@ -26,6 +26,7 @@ import type { ResetPasswordInput, VerifyEmailInput, ResendVerificationEmailInput, + UpdateProfileInput, } from '../validators/auth.validator'; const EMAIL_VERIFICATION_TOKEN_EXPIRY_MINUTES = 60 * 24; @@ -394,4 +395,4 @@ export class AuthService { } } -export const authService = new AuthService(); +export const authService = new AuthService(); \ No newline at end of file diff --git a/Backend/src/validators/auth.validator.ts b/Backend/src/validators/auth.validator.ts index 1adf9ac..66bd4e2 100644 --- a/Backend/src/validators/auth.validator.ts +++ b/Backend/src/validators/auth.validator.ts @@ -78,6 +78,12 @@ export const logoutSchema = z.object({ refreshToken: z.string().min(1, 'Refresh token is required'), }); +export const updateProfileSchema = z.object({ + firstName: z.string().min(1, 'First name must be at least 1 character').optional(), + lastName: z.string().min(1, 'Last name must be at least 1 character').optional(), + phoneNumber: phoneNumberField.optional(), +}); + export type RegisterInput = z.infer; export type LoginInput = z.infer; export type RefreshTokenInput = z.infer; @@ -86,3 +92,4 @@ export type ResetPasswordInput = z.infer; export type VerifyEmailInput = z.infer; export type ResendVerificationEmailInput = z.infer; export type LogoutInput = z.infer; +export type UpdateProfileInput = z.infer; \ No newline at end of file From 046b49035c57e0d8438e2512f3e7a630dd79e0c0 Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 27 Jul 2026 07:33:12 +0100 Subject: [PATCH 2/7] implemneted the authenticated profile --- Backend/src/services/auth.service.ts | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Backend/src/services/auth.service.ts b/Backend/src/services/auth.service.ts index 5f1a19a..b55b3af 100644 --- a/Backend/src/services/auth.service.ts +++ b/Backend/src/services/auth.service.ts @@ -337,6 +337,39 @@ export class AuthService { return user; } + async updateProfile(userId: string, data: UpdateProfileInput) { + const user = await userRepository.findById(userId); + if (!user) { + throw new AppError('User not found', 404); + } + + const updateData: any = {}; + if (data.firstName !== undefined) updateData.firstName = data.firstName; + if (data.lastName !== undefined) updateData.lastName = data.lastName; + + // If phone number is being updated, check for uniqueness + if (data.phoneNumber !== undefined) { + // Normalize the phone number (it's already normalized by the validator, but just to be safe) + const normalizedPhone = data.phoneNumber; + + // Check if another user has this phone number + const existingUserWithPhone = await userRepository.findByPhoneNumber(normalizedPhone); + if (existingUserWithPhone && existingUserWithPhone.id !== userId) { + throw new AppError('User with this phone number already exists', 409); + } + + updateData.phoneNumber = normalizedPhone; + } + + // If there's nothing to update, just return the current user + if (Object.keys(updateData).length === 0) { + return user; + } + + const updatedUser = await userRepository.update(userId, updateData); + return updatedUser; + } + /** * Logs out the session identified by the presented refresh token. The token * must still be valid (present and unrevoked) so an attacker cannot use the From 8bfb390ad06f459b81355fdd5a9a664df0785eca Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 27 Jul 2026 07:33:26 +0100 Subject: [PATCH 3/7] implemneted the authenticated profile --- Backend/src/controllers/auth.controller.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Backend/src/controllers/auth.controller.ts b/Backend/src/controllers/auth.controller.ts index 9fa62db..db53d28 100644 --- a/Backend/src/controllers/auth.controller.ts +++ b/Backend/src/controllers/auth.controller.ts @@ -112,6 +112,24 @@ export class AuthController { } } + async updateProfile(req: Request, res: Response, next: NextFunction) { + try { + const userId = (req as any).user?.userId; + if (!userId) { + throw new AppError('User not authenticated', 401); + } + + const user = await authService.updateProfile(userId, req.body); + res.status(200).json({ + success: true, + message: 'Profile updated successfully', + data: { user }, + }); + } catch (error) { + next(error); + } + } + async logout(req: Request, res: Response, next: NextFunction) { try { const userId = (req as any).user?.userId; @@ -147,4 +165,4 @@ export class AuthController { } } -export const authController = new AuthController(); +export const authController = new AuthController(); \ No newline at end of file From 25a578e316d534eb2b5c13a3fe31055341171379 Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 27 Jul 2026 07:33:41 +0100 Subject: [PATCH 4/7] implemneted the authenticated profile --- Backend/src/routes/auth.routes.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Backend/src/routes/auth.routes.ts b/Backend/src/routes/auth.routes.ts index ffd4aea..411b4a3 100644 --- a/Backend/src/routes/auth.routes.ts +++ b/Backend/src/routes/auth.routes.ts @@ -12,6 +12,7 @@ import { verifyEmailSchema, resendVerificationEmailSchema, logoutSchema, + updateProfileSchema, } from '../validators/auth.validator'; const router = Router(); @@ -28,6 +29,7 @@ router.post('/logout', authenticate, sessionRateLimiter, validate(logoutSchema), // Protected routes router.get('/profile', authenticate, authController.getProfile); +router.put('/profile', authenticate, validate(updateProfileSchema), authController.updateProfile); router.post('/logout-all', authenticate, sessionRateLimiter, authController.logoutAll); -export const authRouter = router; +export const authRouter = router; \ No newline at end of file From ed5c796cf5e735a005375fef33ab22534073fd56 Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 27 Jul 2026 07:34:07 +0100 Subject: [PATCH 5/7] implemneted the authenticated profile --- .../integration/profile.integration.test.ts | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 Backend/tests/integration/profile.integration.test.ts diff --git a/Backend/tests/integration/profile.integration.test.ts b/Backend/tests/integration/profile.integration.test.ts new file mode 100644 index 0000000..ea20858 --- /dev/null +++ b/Backend/tests/integration/profile.integration.test.ts @@ -0,0 +1,160 @@ +import request from 'supertest'; +import { app } from '../../src/app'; +import { prisma } from '../../src/database'; +import { hashPassword } from '../../src/utils/crypto'; +import { normalizePhoneNumber } from '../../src/utils/identity'; + +describe('Profile Integration Tests', () => { + let authToken: string; + let userId: string; + const testEmail = 'testprofile@example.com'; + const testPassword = 'Test12345'; + + beforeEach(async () => { + // Clean up existing user + await prisma.user.deleteMany({ where: { email: testEmail } }); + + // Create test user + const passwordHash = await hashPassword(testPassword); + const user = await prisma.user.create({ + data: { + email: testEmail, + passwordHash, + firstName: 'Original', + lastName: 'Name', + phoneNumber: null, + }, + }); + userId = user.id; + + // Login to get auth token + const loginResponse = await request(app) + .post('/api/auth/login') + .send({ email: testEmail, password: testPassword }); + + authToken = loginResponse.body.data.accessToken; + }); + + afterEach(async () => { + await prisma.user.deleteMany({ where: { email: testEmail } }); + }); + + describe('GET /api/auth/profile', () => { + it('should return user profile when authenticated', async () => { + const response = await request(app) + .get('/api/auth/profile') + .set('Authorization', `Bearer ${authToken}`); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.data.user.email).toBe(testEmail); + expect(response.body.data.user.firstName).toBe('Original'); + expect(response.body.data.user.passwordHash).toBeUndefined(); + }); + + it('should reject unauthenticated requests', async () => { + const response = await request(app).get('/api/auth/profile'); + expect(response.status).toBe(401); + }); + }); + + describe('PUT /api/auth/profile', () => { + it('should update first and last name successfully', async () => { + const response = await request(app) + .put('/api/auth/profile') + .set('Authorization', `Bearer ${authToken}`) + .send({ firstName: 'Updated', lastName: 'User' }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.data.user.firstName).toBe('Updated'); + expect(response.body.data.user.lastName).toBe('User'); + }); + + it('should update phone number successfully', async () => { + const phoneNumber = '08012345678'; + const normalizedPhone = normalizePhoneNumber(phoneNumber); + + const response = await request(app) + .put('/api/auth/profile') + .set('Authorization', `Bearer ${authToken}`) + .send({ phoneNumber }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.data.user.phoneNumber).toBe(normalizedPhone); + }); + + it('should reject duplicate phone number', async () => { + // Create second user + const secondEmail = 'seconduser@example.com'; + const secondPhone = '08098765432'; + const normalizedSecondPhone = normalizePhoneNumber(secondPhone); + + await prisma.user.create({ + data: { + email: secondEmail, + passwordHash: await hashPassword('Test12345'), + phoneNumber: normalizedSecondPhone, + }, + }); + + // Try to update first user's phone to the same number + const response = await request(app) + .put('/api/auth/profile') + .set('Authorization', `Bearer ${authToken}`) + .send({ phoneNumber: secondPhone }); + + expect(response.status).toBe(409); + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('User with this phone number already exists'); + + // Clean up second user + await prisma.user.deleteMany({ where: { email: secondEmail } }); + }); + + it('should allow updating to the same phone number (no conflict)', async () => { + const phoneNumber = '08012345678'; + const normalizedPhone = normalizePhoneNumber(phoneNumber); + + // First set the phone number + await prisma.user.update({ + where: { id: userId }, + data: { phoneNumber: normalizedPhone }, + }); + + // Try to update to the same number again + const response = await request(app) + .put('/api/auth/profile') + .set('Authorization', `Bearer ${authToken}`) + .send({ phoneNumber }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + }); + + it('should reject invalid phone number', async () => { + const response = await request(app) + .put('/api/auth/profile') + .set('Authorization', `Bearer ${authToken}`) + .send({ phoneNumber: 'invalidphone' }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should return the same safe user shape as GET profile', async () => { + const response = await request(app) + .put('/api/auth/profile') + .set('Authorization', `Bearer ${authToken}`) + .send({ firstName: 'TestUpdate' }); + + expect(response.status).toBe(200); + expect(response.body.data.user.passwordHash).toBeUndefined(); + expect(response.body.data.user.id).toBeDefined(); + expect(response.body.data.user.email).toBeDefined(); + expect(response.body.data.user.createdAt).toBeDefined(); + expect(response.body.data.user.updatedAt).toBeDefined(); + }); + }); +}); \ No newline at end of file From cf2530d575199b36ce7ebaa584385bef01a4931e Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 27 Jul 2026 07:35:16 +0100 Subject: [PATCH 6/7] implemneted the authenticated profile --- Backend/tests/integration/profile.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/tests/integration/profile.integration.test.ts b/Backend/tests/integration/profile.integration.test.ts index ea20858..16b1c7a 100644 --- a/Backend/tests/integration/profile.integration.test.ts +++ b/Backend/tests/integration/profile.integration.test.ts @@ -29,7 +29,7 @@ describe('Profile Integration Tests', () => { // Login to get auth token const loginResponse = await request(app) - .post('/api/auth/login') + .post('/api/v1/auth/login') .send({ email: testEmail, password: testPassword }); authToken = loginResponse.body.data.accessToken; From e53eb2db3ac4568f4ab10b38e8a5e67a6e7aaf9f Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 27 Jul 2026 07:35:36 +0100 Subject: [PATCH 7/7] implemneted the authenticated profile --- .../integration/profile.integration.test.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Backend/tests/integration/profile.integration.test.ts b/Backend/tests/integration/profile.integration.test.ts index 16b1c7a..7c1555d 100644 --- a/Backend/tests/integration/profile.integration.test.ts +++ b/Backend/tests/integration/profile.integration.test.ts @@ -39,10 +39,10 @@ describe('Profile Integration Tests', () => { await prisma.user.deleteMany({ where: { email: testEmail } }); }); - describe('GET /api/auth/profile', () => { + describe('GET /api/v1/auth/profile', () => { it('should return user profile when authenticated', async () => { const response = await request(app) - .get('/api/auth/profile') + .get('/api/v1/auth/profile') .set('Authorization', `Bearer ${authToken}`); expect(response.status).toBe(200); @@ -53,15 +53,15 @@ describe('Profile Integration Tests', () => { }); it('should reject unauthenticated requests', async () => { - const response = await request(app).get('/api/auth/profile'); + const response = await request(app).get('/api/v1/auth/profile'); expect(response.status).toBe(401); }); }); - describe('PUT /api/auth/profile', () => { + describe('PUT /api/v1/auth/profile', () => { it('should update first and last name successfully', async () => { const response = await request(app) - .put('/api/auth/profile') + .put('/api/v1/auth/profile') .set('Authorization', `Bearer ${authToken}`) .send({ firstName: 'Updated', lastName: 'User' }); @@ -76,7 +76,7 @@ describe('Profile Integration Tests', () => { const normalizedPhone = normalizePhoneNumber(phoneNumber); const response = await request(app) - .put('/api/auth/profile') + .put('/api/v1/auth/profile') .set('Authorization', `Bearer ${authToken}`) .send({ phoneNumber }); @@ -101,7 +101,7 @@ describe('Profile Integration Tests', () => { // Try to update first user's phone to the same number const response = await request(app) - .put('/api/auth/profile') + .put('/api/v1/auth/profile') .set('Authorization', `Bearer ${authToken}`) .send({ phoneNumber: secondPhone }); @@ -125,7 +125,7 @@ describe('Profile Integration Tests', () => { // Try to update to the same number again const response = await request(app) - .put('/api/auth/profile') + .put('/api/v1/auth/profile') .set('Authorization', `Bearer ${authToken}`) .send({ phoneNumber }); @@ -135,7 +135,7 @@ describe('Profile Integration Tests', () => { it('should reject invalid phone number', async () => { const response = await request(app) - .put('/api/auth/profile') + .put('/api/v1/auth/profile') .set('Authorization', `Bearer ${authToken}`) .send({ phoneNumber: 'invalidphone' }); @@ -145,7 +145,7 @@ describe('Profile Integration Tests', () => { it('should return the same safe user shape as GET profile', async () => { const response = await request(app) - .put('/api/auth/profile') + .put('/api/v1/auth/profile') .set('Authorization', `Bearer ${authToken}`) .send({ firstName: 'TestUpdate' });