diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ee8ccd3..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "json.schemaDownload.enable": true -} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 6b2246c..65c6eb4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,6 +10,9 @@ import router from './routes' const app = express() // TODO: test socket io, emit notifications when create one. +// TODO: test update name, email and password routes. +// TODO: start to create the client, or a barebone to test socket io +// TODO: see how we create authentication with socket io. app.use(express.json()) app.use(express.urlencoded({ extended: true })) diff --git a/src/controllers/users-router.ts b/src/controllers/users-router.ts index 4ed6242..2ce5bf7 100644 --- a/src/controllers/users-router.ts +++ b/src/controllers/users-router.ts @@ -29,6 +29,8 @@ usersRouter.put( uploadFile, user.uploadPicture ) -usersRouter.put('/update', authenticated, user.update) +usersRouter.put('/update-email', authenticated, user.updateEmail) +usersRouter.put('/update-name', authenticated, user.updateName) +usersRouter.put('/update-password', authenticated, user.updatePassword) export default usersRouter diff --git a/src/controllers/users/index.ts b/src/controllers/users/index.ts index 6db78ac..7eba408 100644 --- a/src/controllers/users/index.ts +++ b/src/controllers/users/index.ts @@ -7,7 +7,9 @@ import userLikeCommentController from './like-comment' import userLikePostController from './like-post' import userSearchController from './search-user' import userSignupController from './signup' -import userUpdateController from './update' +import userUpdateEmailController from './update-email' +import userUpdateNameController from './update-name' +import userUpdatePasswordController from './update-password' import userUploadPictureController from './upload-picture' const user = { @@ -20,7 +22,9 @@ const user = { likePost: userLikePostController, searchUser: userSearchController, signup: userSignupController, - update: userUpdateController, + updateEmail: userUpdateEmailController, + updateName: userUpdateNameController, + updatePassword: userUpdatePasswordController, uploadPicture: userUploadPictureController } as const diff --git a/src/controllers/users/update.ts b/src/controllers/users/update-email.ts similarity index 56% rename from src/controllers/users/update.ts rename to src/controllers/users/update-email.ts index 9c61b0f..554b42f 100644 --- a/src/controllers/users/update.ts +++ b/src/controllers/users/update-email.ts @@ -2,16 +2,16 @@ import user from 'services/users' import type { Request, Response } from 'express' import handleResponse from 'helpers/handle-response' -async function userUpdateController ( +async function userUpdateEmailController ( req: Request, res: Response ): Promise { - const { email, displayName, username } = req.body + const { email } = req.body const id = req.user?.id ?? '' - const result = await user.update({ id, email, displayName, username }) + const result = await user.updateEmail({ id, email }) handleResponse(res, result) } -export default userUpdateController +export default userUpdateEmailController diff --git a/src/controllers/users/update-name.ts b/src/controllers/users/update-name.ts new file mode 100644 index 0000000..f595df4 --- /dev/null +++ b/src/controllers/users/update-name.ts @@ -0,0 +1,17 @@ +import user from 'services/users' +import type { Request, Response } from 'express' +import handleResponse from 'helpers/handle-response' + +async function userUpdateNameController ( + req: Request, + res: Response +): Promise { + const { displayName, username } = req.body + const id = req.user?.id ?? '' + + const result = await user.updateName({ id, displayName, username }) + + handleResponse(res, result) +} + +export default userUpdateNameController diff --git a/src/controllers/users/update-password.ts b/src/controllers/users/update-password.ts new file mode 100644 index 0000000..2eb7892 --- /dev/null +++ b/src/controllers/users/update-password.ts @@ -0,0 +1,17 @@ +import user from 'services/users' +import type { Request, Response } from 'express' +import handleResponse from 'helpers/handle-response' + +async function userUpdatePasswordController ( + req: Request, + res: Response +): Promise { + const { currentPassword, newPassword } = req.body + const id = req.user?.id ?? '' + + const result = await user.updatePassword(id, currentPassword, newPassword) + + handleResponse(res, result) +} + +export default userUpdatePasswordController diff --git a/src/services/users/index.ts b/src/services/users/index.ts index 909e924..1f30ea9 100644 --- a/src/services/users/index.ts +++ b/src/services/users/index.ts @@ -7,7 +7,9 @@ import userLikeCommentService from './like-comment' import userLikePostService from './like-post' import userSearchService from './search-user' import userSignupService from './signup' -import userUpdateService from './update' +import userUpdateEmailService from './update-email' +import userUpdateNameService from './update-name' +import userUpdatePasswordService from './update-password' import userUploadPictureService from './upload-picture' const user = { @@ -20,7 +22,9 @@ const user = { likePost: userLikePostService, searchUser: userSearchService, signup: userSignupService, - update: userUpdateService, + updateEmail: userUpdateEmailService, + updateName: userUpdateNameService, + updatePassword: userUpdatePasswordService, uploadPicture: userUploadPictureService } as const diff --git a/src/services/users/signup.ts b/src/services/users/signup.ts index c0112f9..eb24f09 100644 --- a/src/services/users/signup.ts +++ b/src/services/users/signup.ts @@ -21,10 +21,6 @@ async function userSignupService ({ ) } - if (password.trim().length < 8) { - return new Error('Password too short') - } - if (!usernameRegex.test(username)) { return new Error( 'Username not allowed. Only alphanumerics characters (uppercase and lowercase words), underscore, dots and it must be between 5 and 15 characters' diff --git a/src/services/users/update-email.ts b/src/services/users/update-email.ts new file mode 100644 index 0000000..cb803a8 --- /dev/null +++ b/src/services/users/update-email.ts @@ -0,0 +1,42 @@ +import type User from 'interfaces/user' +import prisma from 'clients/prisma-client' + +async function userUpdateEmailService ({ + id, + email +}: User): Promise | Error> { + const user = await prisma.user.findFirst({ where: { id } }) + + if (user === null) { + return new Error('User not found') + } + + if (user.id !== id) { + return new Error('Forbidden') + } + + if (email !== undefined && email.trim() !== user.email) { + const existingUser = await prisma.user.findFirst({ where: { email } }) + if (existingUser != null && existingUser.email !== user.email) { + return new Error('Email already in use') + } + } + + await prisma.user.update({ + where: { + id + }, + data: { + email: email ?? user.email + }, + select: { + displayName: true, + username: true, + createdAt: true + } + }) + + return { message: 'Successfully updated user email' } +} + +export default userUpdateEmailService diff --git a/src/services/users/update.ts b/src/services/users/update-name.ts similarity index 60% rename from src/services/users/update.ts rename to src/services/users/update-name.ts index 6a46546..b2a04e9 100644 --- a/src/services/users/update.ts +++ b/src/services/users/update-name.ts @@ -1,9 +1,8 @@ import type User from 'interfaces/user' import prisma from 'clients/prisma-client' -async function userUpdateService ({ +async function userUpdateNameService ({ id, - email, displayName, username }: User): Promise | Error> { @@ -13,15 +12,8 @@ async function userUpdateService ({ return new Error('User not found') } - // Check if the provided email or username is different from the current user's data - // if they are different, additional queries are made to check if the new email or username is already in use. - - if (email !== undefined && email.trim() !== user.email) { - const existingUser = await prisma.user.findFirst({ where: { email } }) - if (existingUser != null && existingUser.email !== user.email) { - return new Error('Email already in use') - } - } + // Check if the provided username is different from the current user's data + // if different, queries are made to check if the new username is already in use. if (username !== undefined && username.trim() !== user.username) { const existingUser = await prisma.user.findFirst({ where: { username } }) @@ -34,14 +26,11 @@ async function userUpdateService ({ return new Error('Forbidden') } - // TODO: /user/change-password | /user/change-email - const updatedUser = await prisma.user.update({ where: { id }, data: { - email: email ?? user.email, displayName: displayName ?? user.displayName, username: username ?? user.username }, @@ -55,4 +44,4 @@ async function userUpdateService ({ return updatedUser } -export default userUpdateService +export default userUpdateNameService diff --git a/src/services/users/update-password.ts b/src/services/users/update-password.ts new file mode 100644 index 0000000..bc9a1ef --- /dev/null +++ b/src/services/users/update-password.ts @@ -0,0 +1,56 @@ +import * as bcrypt from 'bcrypt' +import prisma from 'clients/prisma-client' + +const passwordRegex = /^(?=.*[0-9])(?=.*[!@#$%^&*_])[a-zA-Z0-9!@#$%^&*_]{8,}$/ + +async function userUpdatePasswordService ( + id: string, + currentPassword: string, + newPassword: string +): Promise | Error> { + if (!passwordRegex.test(newPassword)) { + return new Error( + 'New password must have at least 8 characters, one number and one special character.' + ) + } + + const user = await prisma.user.findFirst({ where: { id } }) + + if (user === null) { + return new Error('User not found') + } + + if (user.id !== id) { + return new Error('Forbidden') + } + + const validPassword = await bcrypt.compare( + currentPassword.replace(/ /g, ''), + user.password + ) + + if (!validPassword) { + return new Error('Invalid password') + } + + const salt = await bcrypt.genSalt(15) + const hashedPassword = await bcrypt.hash(newPassword.replace(/ /g, ''), salt) + + await prisma.user.update({ + where: { + id + }, + data: { + password: hashedPassword + }, + select: { + displayName: true, + username: true, + createdAt: true + } + }) + + return { message: 'Successfully updated user password' } +} + +export default userUpdatePasswordService diff --git a/src/tests/post/post-create.spec.ts b/src/tests/post/post-create.spec.ts index 44e43e5..3d6b935 100644 --- a/src/tests/post/post-create.spec.ts +++ b/src/tests/post/post-create.spec.ts @@ -12,7 +12,8 @@ describe('POST /post/create', () => { }) afterAll(async () => { - await deleteUser(user.username ?? '') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await deleteUser(user.username!) }) it('should respond with 200 status code if the user send the token and the content', async () => { diff --git a/src/tests/post/post-delete.spec.ts b/src/tests/post/post-delete.spec.ts index bf388ba..0ff7618 100644 --- a/src/tests/post/post-delete.spec.ts +++ b/src/tests/post/post-delete.spec.ts @@ -12,7 +12,8 @@ describe('DELETE /post/delete', () => { }) afterAll(async () => { - await deleteUser(user.username ?? '') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await deleteUser(user.username!) }) it('should delete the post successfully', async () => { diff --git a/src/tests/post/post-info.spec.ts b/src/tests/post/post-info.spec.ts index 65a80bc..f3c7eee 100644 --- a/src/tests/post/post-info.spec.ts +++ b/src/tests/post/post-info.spec.ts @@ -26,7 +26,8 @@ describe('POST /post/info', () => { }) afterAll(async () => { - await deleteUser(user.username ?? '') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await deleteUser(user.username!) }) it('should respond with 200 status code and return some info about the post', async () => { diff --git a/src/tests/post/post-update.spec.ts b/src/tests/post/post-update.spec.ts index 47b4402..ca2e63d 100644 --- a/src/tests/post/post-update.spec.ts +++ b/src/tests/post/post-update.spec.ts @@ -12,7 +12,8 @@ describe('PUT /post/update', () => { }) afterAll(async () => { - await deleteUser(user.username ?? '') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await deleteUser(user.username!) }) it('should create a new post and update the content of it', async () => { diff --git a/src/tests/user/user-auth.spec.ts b/src/tests/user/user-auth.spec.ts index 8ff04af..c38ffea 100644 --- a/src/tests/user/user-auth.spec.ts +++ b/src/tests/user/user-auth.spec.ts @@ -12,7 +12,8 @@ describe('POST /user/auth', () => { }) afterAll(async () => { - await deleteUser(user.username ?? '') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await deleteUser(user.username!) }) it('should respond with a error if the user does not exists', async () => { diff --git a/src/tests/user/user-info.spec.ts b/src/tests/user/user-info.spec.ts index 8a0e109..54e6339 100644 --- a/src/tests/user/user-info.spec.ts +++ b/src/tests/user/user-info.spec.ts @@ -12,7 +12,8 @@ describe('POST /user/info', () => { }) afterAll(async () => { - await deleteUser(user.username ?? '') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await deleteUser(user.username!) }) it('should respond with 200 status code and return the user data', async () => { diff --git a/src/tests/user/user-signup.spec.ts b/src/tests/user/user-signup.spec.ts index cae1e49..e60e801 100644 --- a/src/tests/user/user-signup.spec.ts +++ b/src/tests/user/user-signup.spec.ts @@ -13,7 +13,8 @@ describe('POST /user/signup', () => { }) afterAll(async () => { - await deleteUser(user.username ?? '') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await deleteUser(user.username!) }) it('should respond with a 400 status code if sent any invalid data', async () => { diff --git a/src/tests/user/user-update.spec.ts b/src/tests/user/user-update.spec.ts deleted file mode 100644 index c359690..0000000 --- a/src/tests/user/user-update.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import request from 'supertest' -import app from '../../app' -import signUpNewUser from '../utils/create-user' -import deleteUser from '../utils/delete-user' -import type User from 'interfaces/user' - -let user: User - -describe('PUT /user/update', () => { - beforeAll(async () => { - user = await signUpNewUser() - }) - - afterAll(async () => { - await deleteUser(user.username ?? '') - }) - - it('should update the user successfully', async () => { - const fieldsToUpdate = { - displayName: 'Cookie' - } - - const response = await request(app) - .put('/user/update') - .send(fieldsToUpdate) - .set('Authorization', `Bearer ${user.token ?? ''}`) - .expect(200) - - expect(response.body).toEqual( - expect.objectContaining({ - displayName: expect.any(String), - username: expect.any(String), - createdAt: expect.any(String) - }) - ) - }) -})