From 91ae33327b51bfa964f8ecc3db7dfe1483f19555 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Wed, 26 Feb 2025 10:13:41 +0100 Subject: [PATCH 01/28] added password validation check --- .env.example | 5 ----- src/controllers/user.js | 10 ++++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 932b9f1e..00000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -PORT=4000 -DATABASE_URL="?schema=prisma" -SHADOW_DATABASE_URL="?schema=shadow" -JWT_SECRET="somesecurestring" -JWT_EXPIRY="24h" \ No newline at end of file diff --git a/src/controllers/user.js b/src/controllers/user.js index 40ff0f1c..b93f4ef5 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -3,6 +3,16 @@ import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' export const create = async (req, res) => { const userToCreate = await User.fromJson(req.body) + const password = await req.body.password + + /* eslint-disable */ + if (!password.match(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)) { + return sendDataResponse(res, 400, { + password: + 'Password must be at least 8 characters long, contain at least one letter, one number, and one special character' + }) +} + /* eslint-enable */ try { const existingUser = await User.findByEmail(userToCreate.email) From 6c5e059c5dba666651aff79ab259d8890683e472 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Wed, 26 Feb 2025 10:21:12 +0100 Subject: [PATCH 02/28] eslint issue --- src/controllers/user.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index b93f4ef5..e861b5b1 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -5,17 +5,16 @@ export const create = async (req, res) => { const userToCreate = await User.fromJson(req.body) const password = await req.body.password - /* eslint-disable */ - if (!password.match(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)) { - return sendDataResponse(res, 400, { - password: - 'Password must be at least 8 characters long, contain at least one letter, one number, and one special character' - }) -} - /* eslint-enable */ - try { const existingUser = await User.findByEmail(userToCreate.email) + /* eslint-disable */ + if (!password.match(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)) { + return sendDataResponse(res, 400, { + password: + 'Password must be at least 8 characters long, contain at least one letter, one number, and one special character' + }) + } + /* eslint-enable */ if (existingUser) { return sendDataResponse(res, 400, { email: 'Email already in use' }) From af08bfff19d932c39a4abe869e0ca5a16755e70b Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:21:03 +0100 Subject: [PATCH 03/28] Implemented API - Update User #3 --- src/controllers/user.js | 101 ++++++++++++++++++++++++++++++++++++++-- src/domain/user.js | 53 +++++++++++++++++++++ src/routes/user.js | 7 +-- 3 files changed, 151 insertions(+), 10 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 40ff0f1c..cbfe51ee 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -1,5 +1,6 @@ import User from '../domain/user.js' import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' +import bcrypt from 'bcrypt' export const create = async (req, res) => { const userToCreate = await User.fromJson(req.body) @@ -57,11 +58,101 @@ export const getAll = async (req, res) => { } export const updateById = async (req, res) => { - const { cohort_id: cohortId } = req.body + const userId = parseInt(req.params.id) + const { + firstName, + lastName, + email, + biography, + githubUrl, + password, + cohort_id: cohortIdSnake, + cohortId: cohortIdCamel, + role + } = req.body + + const cohortId = cohortIdSnake ?? cohortIdCamel - if (!cohortId) { - return sendDataResponse(res, 400, { cohort_id: 'Cohort ID is required' }) - } + try { + const userToUpdate = await User.findById(userId) + + if (!userToUpdate) { + return sendDataResponse(res, 404, { id: 'User not found' }) + } + + // Check if user is updating their own profile or is a teacher + const isOwnProfile = req.user.id === userId + const isTeacher = req.user.role === 'TEACHER' - return sendDataResponse(res, 201, { user: { cohort_id: cohortId } }) + if (!isOwnProfile && !isTeacher) { + return sendDataResponse(res, 403, { + authorization: 'You are not authorized to update this profile' + }) + } + + // Check if student is trying to update restricted fields + if (!isTeacher && (cohortId || role)) { + return sendDataResponse(res, 403, { + authorization: + 'Students cannot modify cohort or role information. Please contact a teacher for these changes.' + }) + } + + // Create update data object + const updateData = {} + + // Helper function to validate and add field if it has a valid value + const addValidField = (field, value) => { + if ( + value !== undefined && + value !== null && + value !== '' && + value !== 'string' + ) { + updateData[field] = value + } + } + + // Profile fields any user can update on their own profile + if (isOwnProfile || isTeacher) { + addValidField('firstName', firstName) + addValidField('lastName', lastName) + addValidField('bio', biography) + addValidField('githubUrl', githubUrl) + addValidField('email', email) + if (password) { + updateData.passwordHash = await bcrypt.hash(password, 8) + } + } + + // Fields only teachers can update + if (isTeacher) { + if (cohortId !== undefined) { + const cohortIdInt = parseInt(cohortId, 10) + if (!isNaN(cohortIdInt)) { + updateData.cohortId = cohortIdInt + } + } + if (role === 'STUDENT' || role === 'TEACHER') { + updateData.role = role + } + } + + // If no valid fields to update + if (Object.keys(updateData).length === 0) { + return sendDataResponse(res, 400, { + fields: 'No valid fields provided for update' + }) + } + + const updatedUser = await userToUpdate.update(updateData) + + return sendDataResponse(res, 200, updatedUser) + } catch (error) { + return sendMessageResponse( + res, + 500, + `Unable to update user: ${error.message}` + ) + } } diff --git a/src/domain/user.js b/src/domain/user.js index fd7734c7..eb393def 100644 --- a/src/domain/user.js +++ b/src/domain/user.js @@ -170,4 +170,57 @@ export default class User { return foundUsers.map((user) => User.fromDb(user)) } + + async update(data) { + const updateData = { + where: { + id: this.id + }, + data: {}, + include: { + profile: true + } + } + + // Handle user-level updates + if (data.email && data.email !== 'string') + updateData.data.email = data.email + if (data.passwordHash) updateData.data.password = data.passwordHash + if (data.role && (data.role === 'STUDENT' || data.role === 'TEACHER')) { + updateData.data.role = data.role + } + if (typeof data.cohortId === 'number') { + updateData.data.cohortId = data.cohortId + } + + // Handle profile-related updates + const profileUpdates = {} + let hasProfileUpdates = false + + if (data.firstName && data.firstName !== 'string') { + profileUpdates.firstName = data.firstName + hasProfileUpdates = true + } + if (data.lastName && data.lastName !== 'string') { + profileUpdates.lastName = data.lastName + hasProfileUpdates = true + } + if (data.bio && data.bio !== 'string') { + profileUpdates.bio = data.bio + hasProfileUpdates = true + } + if (data.githubUrl && data.githubUrl !== 'string') { + profileUpdates.githubUrl = data.githubUrl + hasProfileUpdates = true + } + + if (hasProfileUpdates) { + updateData.data.profile = { + update: profileUpdates + } + } + + const updatedUser = await dbClient.user.update(updateData) + return User.fromDb(updatedUser) + } } diff --git a/src/routes/user.js b/src/routes/user.js index 9f63d162..c2714f9b 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -1,15 +1,12 @@ import { Router } from 'express' import { create, getById, getAll, updateById } from '../controllers/user.js' -import { - validateAuthentication, - validateTeacherRole -} from '../middleware/auth.js' +import { validateAuthentication } from '../middleware/auth.js' const router = Router() router.post('/', create) router.get('/', validateAuthentication, getAll) router.get('/:id', validateAuthentication, getById) -router.patch('/:id', validateAuthentication, validateTeacherRole, updateById) +router.patch('/:id', validateAuthentication, updateById) export default router From 30639c72a9ccae2bca6098b94a84b070257be149 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:30:03 +0100 Subject: [PATCH 04/28] Implemented only owners of a profile can edit their own password --- src/controllers/user.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index cbfe51ee..1ba59896 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -114,13 +114,8 @@ export const updateById = async (req, res) => { } // Profile fields any user can update on their own profile - if (isOwnProfile || isTeacher) { - addValidField('firstName', firstName) - addValidField('lastName', lastName) - addValidField('bio', biography) - addValidField('githubUrl', githubUrl) - addValidField('email', email) - if (password) { + if (isOwnProfile) { + if (password && typeof password === 'string') { updateData.passwordHash = await bcrypt.hash(password, 8) } } @@ -138,6 +133,15 @@ export const updateById = async (req, res) => { } } + // Profile fields any user can update on their own profile + if (isOwnProfile || isTeacher) { + addValidField('firstName', firstName) + addValidField('lastName', lastName) + addValidField('bio', biography) + addValidField('githubUrl', githubUrl) + addValidField('email', email) + } + // If no valid fields to update if (Object.keys(updateData).length === 0) { return sendDataResponse(res, 400, { From 835d1f907860170ba54fcc41bc2957b913527645 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Wed, 26 Feb 2025 11:32:18 +0100 Subject: [PATCH 05/28] added additional checks depending on what was missing in the password requirements --- src/controllers/user.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index e861b5b1..6f2edf7d 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -4,16 +4,31 @@ import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' export const create = async (req, res) => { const userToCreate = await User.fromJson(req.body) const password = await req.body.password + const missingPasswordMatch = [] try { const existingUser = await User.findByEmail(userToCreate.email) /* eslint-disable */ - if (!password.match(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)) { - return sendDataResponse(res, 400, { - password: - 'Password must be at least 8 characters long, contain at least one letter, one number, and one special character' - }) + if (password.length < 8) { + missingPasswordMatch.push('Password must be at least 8 characters long') } + if (!/[A-Za-z]/.test(password)) { + missingPasswordMatch.push('Password must contain at least one letter') + } + if (!/[A-Z]/.test(password)) { + missingPasswordMatch.push('Password must contain at least one uppercase letter') + } + if (!/\d/.test(password)) { + missingPasswordMatch.push('Password must contain at least one number') + } + if (!/[@$!%*?&]/.test(password)) { + missingPasswordMatch.push('Password must contain at least one special character') + } + + if (missingPasswordMatch.length > 0) { + return sendDataResponse(res, 400, { password: missingPasswordMatch }) + } + /* eslint-enable */ if (existingUser) { From dd17a6654b36af48a833127b2eaadf3563090ffd Mon Sep 17 00:00:00 2001 From: Kristoffer Blucher Date: Wed, 26 Feb 2025 11:39:55 +0100 Subject: [PATCH 06/28] updated email validation in user.js --- package-lock.json | 15 +++++++++++++++ package.json | 1 + src/controllers/user.js | 5 +++++ 3 files changed, 21 insertions(+) diff --git a/package-lock.json b/package-lock.json index 044145e5..5886dc3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "express": "^4.17.3", "jsonwebtoken": "^8.5.1", "swagger-ui-express": "^5.0.0", + "validator": "^13.12.0", "yaml": "^2.3.4" }, "devDependencies": { @@ -3491,6 +3492,15 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6155,6 +6165,11 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index b5997d22..f5f04abe 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "express": "^4.17.3", "jsonwebtoken": "^8.5.1", "swagger-ui-express": "^5.0.0", + "validator": "^13.12.0", "yaml": "^2.3.4" } } diff --git a/src/controllers/user.js b/src/controllers/user.js index 40ff0f1c..d69e4c78 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -1,3 +1,4 @@ +import validator from 'validator' import User from '../domain/user.js' import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' @@ -5,6 +6,10 @@ export const create = async (req, res) => { const userToCreate = await User.fromJson(req.body) try { + if (!validator.isEmail(userToCreate.email)) { + return sendDataResponse(res, 400, { email: 'Invalid email format' }) + } + const existingUser = await User.findByEmail(userToCreate.email) if (existingUser) { From 78c510bef892d9c5c0cc7f603e52ee38a6d0c31b Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:11:27 +0100 Subject: [PATCH 07/28] Added RegExp to validate all forms of input --- src/controllers/user.js | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/controllers/user.js b/src/controllers/user.js index 1ba59896..1e16558b 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -71,6 +71,69 @@ export const updateById = async (req, res) => { role } = req.body + // Input validation patterns + const validationPatterns = { + name: /^[a-zA-Z\s-']{2,50}$/, + email: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, + password: + /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/, + biography: /^[\s\S]{0,500}$/, + githubUrl: /^https:\/\/github\.com\/[a-zA-Z0-9-]+$/, + role: /^(STUDENT|TEACHER)$/ + } + + // Validation helper function + const validateField = (field, value, pattern) => { + if (value === undefined || value === null || value === '') return true + return pattern.test(value) + } + + // Validate input fields + const validationErrors = {} + + if ( + firstName && + !validateField('firstName', firstName, validationPatterns.name) + ) { + validationErrors.firstName = 'Invalid first name format' + } + if ( + lastName && + !validateField('lastName', lastName, validationPatterns.name) + ) { + validationErrors.lastName = 'Invalid last name format' + } + if (email && !validateField('email', email, validationPatterns.email)) { + validationErrors.email = 'Invalid email format' + } + if ( + biography && + !validateField('biography', biography, validationPatterns.biography) + ) { + validationErrors.biography = 'Biography must not exceed 500 characters' + } + if ( + githubUrl && + !validateField('githubUrl', githubUrl, validationPatterns.githubUrl) + ) { + validationErrors.githubUrl = 'Invalid GitHub URL format' + } + if ( + password && + !validateField('password', password, validationPatterns.password) + ) { + validationErrors.password = + 'Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character (@$!%*#?&)' + } + if (role && !validateField('role', role, validationPatterns.role)) { + validationErrors.role = 'Invalid role' + } + + // Check for validation errors + if (Object.keys(validationErrors).length > 0) { + return sendDataResponse(res, 400, { validation: validationErrors }) + } + const cohortId = cohortIdSnake ?? cohortIdCamel try { From d0d88323e64b2b9f2f9ef89b979d85f75f731920 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:36:31 +0100 Subject: [PATCH 08/28] Implemented API - Retrieve users by cohort #8 --- docs/openapi.yml | 66 +++++++++++++++++++++++++++++++++++++++ src/controllers/cohort.js | 52 ++++++++++++++++++++++++++++++ src/middleware/auth.js | 22 +++++++++++++ src/routes/cohort.js | 11 +++++-- 4 files changed, 149 insertions(+), 2 deletions(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index 5f2a05f2..67b1c1a1 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -285,6 +285,72 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /cohorts/{id}/members: + get: + tags: + - cohort + summary: Get all members in a cohort + description: Get all students and teachers in a specific cohort. User must be a teacher or a member of the cohort. + operationId: getCohortMembers + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: Cohort ID + required: true + schema: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + type: object + properties: + members: + type: object + properties: + students: + type: array + items: + type: object + properties: + id: + type: integer + firstName: + type: string + lastName: + type: string + teachers: + type: array + items: + type: object + properties: + id: + type: integer + firstName: + type: string + lastName: + type: string + '403': + description: Unauthorized access + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Cohort not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' components: securitySchemes: diff --git a/src/controllers/cohort.js b/src/controllers/cohort.js index cc39365b..99f8f11a 100644 --- a/src/controllers/cohort.js +++ b/src/controllers/cohort.js @@ -1,5 +1,6 @@ import { createCohort } from '../domain/cohort.js' import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' +import dbClient from '../utils/dbClient.js' export const create = async (req, res) => { try { @@ -10,3 +11,54 @@ export const create = async (req, res) => { return sendMessageResponse(res, 500, 'Unable to create cohort') } } + +export const getCohortMembers = async (req, res) => { + const cohortId = parseInt(req.params.id) + + try { + const cohortMembers = await dbClient.user.findMany({ + where: { + cohortId: cohortId + }, + select: { + id: true, + role: true, + profile: { + select: { + firstName: true, + lastName: true + } + } + } + }) + + if (!cohortMembers.length) { + return sendDataResponse(res, 404, { + cohort: 'No members found in this cohort' + }) + } + + const formattedMembers = { + students: [], + teachers: [] + } + + cohortMembers.forEach((member) => { + const formattedMember = { + id: member.id, + firstName: member.profile.firstName, + lastName: member.profile.lastName + } + + if (member.role === 'TEACHER') { + formattedMembers.teachers.push(formattedMember) + } else { + formattedMembers.students.push(formattedMember) + } + }) + + return sendDataResponse(res, 200, { members: formattedMembers }) + } catch (error) { + return sendMessageResponse(res, 500, 'Unable to fetch cohort members') + } +} diff --git a/src/middleware/auth.js b/src/middleware/auth.js index baffff47..490fca40 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -72,3 +72,25 @@ function validateTokenType(type) { return true } + +export async function validateCohortAccess(req, res, next) { + const cohortId = parseInt(req.params.id) + + if (!req.user) { + return sendMessageResponse(res, 500, 'Unable to verify user') + } + + // Teachers can access any cohort + if (req.user.role === 'TEACHER') { + return next() + } + + // Students can only access their own cohort + if (req.user.cohortId !== cohortId) { + return sendDataResponse(res, 403, { + authorization: 'You are not authorized to access this cohort' + }) + } + + next() +} diff --git a/src/routes/cohort.js b/src/routes/cohort.js index 3cc7813d..d0df9de8 100644 --- a/src/routes/cohort.js +++ b/src/routes/cohort.js @@ -1,12 +1,19 @@ import { Router } from 'express' -import { create } from '../controllers/cohort.js' +import { create, getCohortMembers } from '../controllers/cohort.js' import { validateAuthentication, - validateTeacherRole + validateTeacherRole, + validateCohortAccess } from '../middleware/auth.js' const router = Router() router.post('/', validateAuthentication, validateTeacherRole, create) +router.get( + '/:id/members', + validateAuthentication, + validateCohortAccess, + getCohortMembers +) export default router From b0ee452c10bbbb9b0f61fabaa62b329705ba1741 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Wed, 26 Feb 2025 15:35:49 +0100 Subject: [PATCH 09/28] added filtering for user search --- docs/openapi.yml | 7 ++++++- src/controllers/user.js | 16 +++++++++++++--- src/domain/user.js | 18 +++++++++++++++--- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index 5f2a05f2..938810f1 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -35,7 +35,7 @@ paths: get: tags: - user - summary: Get all users by first name if provided + summary: Get all users by first name or last name if provided description: '' operationId: getAllUsers security: @@ -46,6 +46,11 @@ paths: description: Search all users by first name if provided (case-sensitive and exact string matches only) schema: type: string + - name: lastName + in: query + description: Search all users by last name if provided (case-insensitive) + schema: + type: string responses: '200': description: successful operation diff --git a/src/controllers/user.js b/src/controllers/user.js index 5b2df831..b585137b 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -67,12 +67,22 @@ export const getById = async (req, res) => { export const getAll = async (req, res) => { // eslint-disable-next-line camelcase - const { first_name: firstName } = req.query + const { firstName, lastName } = req.query let foundUsers - - if (firstName) { + let firstNameUsers + let lastNameUsers + + if (firstName && lastName) { + firstNameUsers = await User.findManyByFirstName(firstName) + lastNameUsers = await User.findManyByLastName(lastName) + foundUsers = firstNameUsers.filter((user) => + lastNameUsers.some((u) => u.id === user.id) + ) + } else if (firstName) { foundUsers = await User.findManyByFirstName(firstName) + } else if (lastName) { + foundUsers = await User.findManyByLastName(lastName) } else { foundUsers = await User.findAll() } diff --git a/src/domain/user.js b/src/domain/user.js index eb393def..659ed0a6 100644 --- a/src/domain/user.js +++ b/src/domain/user.js @@ -130,6 +130,10 @@ export default class User { return User._findMany('firstName', firstName) } + static async findManyByLastName(lastName) { + return User._findMany('lastName', lastName) + } + static async findAll() { return User._findMany() } @@ -153,15 +157,23 @@ export default class User { static async _findMany(key, value) { const query = { - include: { - profile: true + select: { + id: true, + cohortId: true, + role: true, + profile: { + select: { + firstName: true, + lastName: true + } + } } } if (key !== undefined && value !== undefined) { query.where = { profile: { - [key]: value + [key]: { equals: value, mode: 'insensitive' } } } } From 69efa198b5de516a377e69153cb61728ac769998 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:44:44 +0100 Subject: [PATCH 10/28] Closes #12 --- docs/openapi.yml | 66 ------------------- .../migration.sql | 5 ++ prisma/schema.prisma | 7 ++ prisma/seed.js | 4 +- src/controllers/cohort.js | 24 ++++++- src/domain/cohort.js | 10 ++- 6 files changed, 45 insertions(+), 71 deletions(-) create mode 100644 prisma/migrations/20250226143315_add_cohort_type/migration.sql diff --git a/docs/openapi.yml b/docs/openapi.yml index 67b1c1a1..5f2a05f2 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -285,72 +285,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' - /cohorts/{id}/members: - get: - tags: - - cohort - summary: Get all members in a cohort - description: Get all students and teachers in a specific cohort. User must be a teacher or a member of the cohort. - operationId: getCohortMembers - security: - - bearerAuth: [] - parameters: - - name: id - in: path - description: Cohort ID - required: true - schema: - type: integer - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - properties: - status: - type: string - data: - type: object - properties: - members: - type: object - properties: - students: - type: array - items: - type: object - properties: - id: - type: integer - firstName: - type: string - lastName: - type: string - teachers: - type: array - items: - type: object - properties: - id: - type: integer - firstName: - type: string - lastName: - type: string - '403': - description: Unauthorized access - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '404': - description: Cohort not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' components: securitySchemes: diff --git a/prisma/migrations/20250226143315_add_cohort_type/migration.sql b/prisma/migrations/20250226143315_add_cohort_type/migration.sql new file mode 100644 index 00000000..4c29d4e2 --- /dev/null +++ b/prisma/migrations/20250226143315_add_cohort_type/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "CohortType" AS ENUM ('SOFTWARE_DEVELOPMENT', 'FRONTEND_DEVELOPMENT', 'DATA_ANALYTICS'); + +-- AlterTable +ALTER TABLE "Cohort" ADD COLUMN "type" "CohortType" NOT NULL DEFAULT E'SOFTWARE_DEVELOPMENT'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 72ec5632..9f32ed38 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,8 +38,15 @@ model Profile { githubUrl String? } +enum CohortType { + SOFTWARE_DEVELOPMENT + FRONTEND_DEVELOPMENT + DATA_ANALYTICS +} + model Cohort { id Int @id @default(autoincrement()) + type CohortType @default(SOFTWARE_DEVELOPMENT) users User[] deliveryLogs DeliveryLog[] } diff --git a/prisma/seed.js b/prisma/seed.js index 21684795..55d2cc63 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -49,7 +49,9 @@ async function createPost(userId, content) { async function createCohort() { const cohort = await prisma.cohort.create({ - data: {} + data: { + type: 'SOFTWARE_DEVELOPMENT' + } }) console.info('Cohort created', cohort) diff --git a/src/controllers/cohort.js b/src/controllers/cohort.js index 99f8f11a..d27ea9ce 100644 --- a/src/controllers/cohort.js +++ b/src/controllers/cohort.js @@ -2,6 +2,12 @@ import { createCohort } from '../domain/cohort.js' import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' import dbClient from '../utils/dbClient.js' +export const cohortTypeDisplay = { + SOFTWARE_DEVELOPMENT: 'Software Development', + FRONTEND_DEVELOPMENT: 'Front-End Development', + DATA_ANALYTICS: 'Data Analytics' +} + export const create = async (req, res) => { try { const createdCohort = await createCohort() @@ -32,6 +38,16 @@ export const getCohortMembers = async (req, res) => { } }) + const cohort = await dbClient.cohort.findUnique({ + where: { + id: cohortId + }, + select: { + id: true, + type: true + } + }) + if (!cohortMembers.length) { return sendDataResponse(res, 404, { cohort: 'No members found in this cohort' @@ -57,7 +73,13 @@ export const getCohortMembers = async (req, res) => { } }) - return sendDataResponse(res, 200, { members: formattedMembers }) + return sendDataResponse(res, 200, { + cohort: { + id: cohort.id, + type: cohortTypeDisplay[cohort.type] + }, + members: formattedMembers + }) } catch (error) { return sendMessageResponse(res, 500, 'Unable to fetch cohort members') } diff --git a/src/domain/cohort.js b/src/domain/cohort.js index abdda73b..5df64357 100644 --- a/src/domain/cohort.js +++ b/src/domain/cohort.js @@ -6,21 +6,25 @@ import dbClient from '../utils/dbClient.js' */ export async function createCohort() { const createdCohort = await dbClient.cohort.create({ - data: {} + data: { + type: 'SOFTWARE_DEVELOPMENT' // Default type + } }) return new Cohort(createdCohort.id) } export class Cohort { - constructor(id = null) { + constructor(id = null, type = null) { this.id = id + this.type = type } toJSON() { return { cohort: { - id: this.id + id: this.id, + type: this.type } } } From 2541b560e7259e7ee1af568c13aacbb3ed716a3d Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:47:30 +0100 Subject: [PATCH 11/28] Add OpenAPI specification for retrieving cohort members --- docs/openapi.yml | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/docs/openapi.yml b/docs/openapi.yml index 5f2a05f2..dde3a1b8 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -285,6 +285,85 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /cohorts/{id}/members: + get: + tags: + - cohort + summary: Get members of a specific cohort + description: Returns all members (teachers and students) of a specific cohort. Teachers can access any cohort, students can only access their own cohort. + operationId: getCohortMembers + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: ID of the cohort to get members from + required: true + schema: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + type: object + properties: + cohort: + type: object + properties: + id: + type: integer + type: + type: string + members: + type: object + properties: + students: + type: array + items: + type: object + properties: + id: + type: integer + firstName: + type: string + lastName: + type: string + teachers: + type: array + items: + type: object + properties: + id: + type: integer + firstName: + type: string + lastName: + type: string + '403': + description: Unauthorized access to cohort + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: No members found in cohort + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' components: securitySchemes: From 966562144d1bd0511dcb8f37f92aef5a0aaf4c29 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Wed, 26 Feb 2025 15:52:51 +0100 Subject: [PATCH 12/28] updated error checks and messages --- docs/openapi.yml | 2 +- src/controllers/user.js | 63 +++++++++++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index 938810f1..23f77ee3 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -43,7 +43,7 @@ paths: parameters: - name: firstName in: query - description: Search all users by first name if provided (case-sensitive and exact string matches only) + description: Search all users by first name if provided (case-insensitive) schema: type: string - name: lastName diff --git a/src/controllers/user.js b/src/controllers/user.js index b585137b..05b6cf2e 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -73,27 +73,54 @@ export const getAll = async (req, res) => { let firstNameUsers let lastNameUsers - if (firstName && lastName) { - firstNameUsers = await User.findManyByFirstName(firstName) - lastNameUsers = await User.findManyByLastName(lastName) - foundUsers = firstNameUsers.filter((user) => - lastNameUsers.some((u) => u.id === user.id) - ) - } else if (firstName) { - foundUsers = await User.findManyByFirstName(firstName) - } else if (lastName) { - foundUsers = await User.findManyByLastName(lastName) - } else { - foundUsers = await User.findAll() - } + const namePattern = /^[a-zA-Z\s-']{1,50}$/ - const formattedUsers = foundUsers.map((user) => { - return { - ...user.toJSON().user + try { + if ( + firstName && + !namePattern.test(firstName) && + lastName && + !namePattern.test(lastName) + ) { + return sendDataResponse(res, 400, { + error: 'Invalid first name and last name format.' + }) + } + if (firstName && !namePattern.test(firstName)) { + return sendDataResponse(res, 400, { error: 'Invalid first name format.' }) + } + if (lastName && !namePattern.test(lastName)) { + return sendDataResponse(res, 400, { error: 'Invalid last name format.' }) } - }) - return sendDataResponse(res, 200, { users: formattedUsers }) + if (firstName && lastName) { + firstNameUsers = await User.findManyByFirstName(firstName) + lastNameUsers = await User.findManyByLastName(lastName) + foundUsers = firstNameUsers.filter((user) => + lastNameUsers.some((u) => u.id === user.id) + ) + } else if (firstName) { + foundUsers = await User.findManyByFirstName(firstName) + } else if (lastName) { + foundUsers = await User.findManyByLastName(lastName) + } else { + foundUsers = await User.findAll() + } + + if (!foundUsers || foundUsers.length === 0) { + return sendDataResponse(res, 404, { error: 'No users found.' }) + } + const formattedUsers = foundUsers.map((user) => { + return { + ...user.toJSON().user + } + }) + return sendDataResponse(res, 200, { users: formattedUsers }) + } catch (error) { + return sendDataResponse(res, 500, { + error: 'An unexpected error occurred.' + }) + } } export const updateById = async (req, res) => { From 795039dc7362bbcef1aeb177e2024d27d31933bc Mon Sep 17 00:00:00 2001 From: Kristoffer Blucher Date: Wed, 26 Feb 2025 16:09:26 +0100 Subject: [PATCH 13/28] Added endpoint for retrieving user-progress --- docs/openapi.yml | 84 +++++++++++++++++++++++++++++++++++++++++ src/controllers/user.js | 35 +++++++++++++++++ src/routes/user.js | 9 ++++- 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index 5f2a05f2..cb991346 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -164,6 +164,90 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /users/{id}/progress: + get: + tags: + - User + summary: Retrieve user progress + description: Returns progress data for a user, accessible by the user or a teacher. + operationId: getUserProgress + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the user whose progress is being retrieved. + schema: + type: string + responses: + '200': + description: Successfully retrieved user progress. + content: + application/json: + schema: + type: object + properties: + CompletedModules: + type: object + properties: + completed: + type: integer + example: 2 + total: + type: integer + example: 7 + description: "Number of modules completed out of the total modules." + CompletedUnits: + type: object + properties: + completed: + type: integer + example: 4 + total: + type: integer + example: 10 + description: "Number of units completed out of the total units." + CompletedExercises: + type: object + properties: + completed: + type: integer + example: 34 + total: + type: integer + example: 58 + description: "Number of exercises completed out of the total exercises." + '403': + description: Access denied - user does not have permission to view this progress. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Access Denied: You can only view your own or your students progress.' + '401': + description: Unauthorized - Missing or invalid authentication token. + content: + application/json: + schema: + type: object + properties: + authorization: + type: string + example: 'Missing Authorization header' + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Internal Server Error: Unable to retrieve user progress' /posts: post: tags: diff --git a/src/controllers/user.js b/src/controllers/user.js index 5b2df831..c8c09af7 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -252,3 +252,38 @@ export const updateById = async (req, res) => { ) } } + +// Function for retrieving user progress +export const getUserProgress = async (req, res) => { + const userId = req.params.id + const currentUser = req.user + + // Check if user is authorized to view progress + if (currentUser.role !== 'TEACHER' && currentUser.id !== userId) { + return sendDataResponse(res, 403, { + error: 'You are not authorized to view this user progress' + }) + } + + try { + // Using json sample-data as requested + const progressData = { + CompletedModules: { + completed: 2, + total: 7 + }, + CompletedUnits: { + completed: 4, + total: 10 + }, + CompletedExercises: { + completed: 34, + total: 58 + } + } + return sendDataResponse(res, 200, progressData) + } catch (error) { + console.error('Failed to retrieve user progress:', error) + return sendMessageResponse(res, 500, 'Failed to retrieve user progress') + } +} diff --git a/src/routes/user.js b/src/routes/user.js index c2714f9b..9587d7fe 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -1,5 +1,11 @@ import { Router } from 'express' -import { create, getById, getAll, updateById } from '../controllers/user.js' +import { + create, + getById, + getAll, + updateById, + getUserProgress +} from '../controllers/user.js' import { validateAuthentication } from '../middleware/auth.js' const router = Router() @@ -8,5 +14,6 @@ router.post('/', create) router.get('/', validateAuthentication, getAll) router.get('/:id', validateAuthentication, getById) router.patch('/:id', validateAuthentication, updateById) +router.get('/:id/progress', validateAuthentication, getUserProgress) export default router From 01b43ded54e108b283de377c4569714495f3fe82 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:16:48 +0100 Subject: [PATCH 14/28] Closes #15 + updated seed.js to have more data. --- docs/openapi.yml | 31 ++--- .../migration.sql | 49 ++++++++ prisma/schema.prisma | 39 +++++- prisma/seed.js | 111 +++++++++++++++--- src/controllers/post.js | 64 ++++++++-- 5 files changed, 246 insertions(+), 48 deletions(-) create mode 100644 prisma/migrations/20250227080825_add_created_at/migration.sql diff --git a/docs/openapi.yml b/docs/openapi.yml index 8d30ac09..968b8ecc 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -491,30 +491,31 @@ components: type: string createdAt: type: string - format: string - updatedAt: - type: string - format: string + format: date-time author: type: object properties: id: type: integer - cohortId: - type: integer - role: - type: string firstName: type: string lastName: type: string - bio: - type: string - githubUrl: - type: string - profileImageUrl: - type: string - + cohort: + type: object + properties: + id: + type: integer + type: + type: string + stats: + type: object + properties: + likes: + type: integer + comments: + type: integer + CreatedUser: type: object properties: diff --git a/prisma/migrations/20250227080825_add_created_at/migration.sql b/prisma/migrations/20250227080825_add_created_at/migration.sql new file mode 100644 index 00000000..809be098 --- /dev/null +++ b/prisma/migrations/20250227080825_add_created_at/migration.sql @@ -0,0 +1,49 @@ +/* + Warnings: + + - Added the required column `updatedAt` to the `Post` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Post" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- CreateTable +CREATE TABLE "PostLike" ( + "id" SERIAL NOT NULL, + "postId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PostLike_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PostComment" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "postId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PostComment_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PostLike_postId_userId_key" ON "PostLike"("postId", "userId"); + +-- CreateIndex +CREATE INDEX "Post_createdAt_idx" ON "Post"("createdAt" DESC); + +-- AddForeignKey +ALTER TABLE "PostLike" ADD CONSTRAINT "PostLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostLike" ADD CONSTRAINT "PostLike_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9f32ed38..1c9be77d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,6 +3,7 @@ generator client { provider = "prisma-client-js" + previewFeatures = ["extendedIndexes"] } datasource db { @@ -26,6 +27,8 @@ model User { cohort Cohort? @relation(fields: [cohortId], references: [id]) posts Post[] deliveryLogs DeliveryLog[] + postLikes PostLike[] + postComments PostComment[] } model Profile { @@ -52,10 +55,38 @@ model Cohort { } model Post { - id Int @id @default(autoincrement()) - content String - userId Int - user User @relation(fields: [userId], references: [id]) + id Int @id @default(autoincrement()) + content String + userId Int + user User @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + likes PostLike[] + comments PostComment[] + + @@index([createdAt(sort: Desc)]) +} + +model PostLike { + id Int @id @default(autoincrement()) + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + postId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + createdAt DateTime @default(now()) + + @@unique([postId, userId]) +} + +model PostComment { + id Int @id @default(autoincrement()) + content String + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + postId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model DeliveryLog { diff --git a/prisma/seed.js b/prisma/seed.js index 55d2cc63..317c3377 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -3,30 +3,96 @@ import bcrypt from 'bcrypt' const prisma = new PrismaClient() async function seed() { - const cohort = await createCohort() + // Create 3 cohorts + const cohort1 = await createCohort() + const cohort2 = await createCohort() + const cohort3 = await createCohort() - const student = await createUser( - 'student@test.com', + // Create 3 students + const student1 = await createUser( + 'student1@test.com', 'Testpassword1!', - cohort.id, - 'Joe', - 'Bloggs', - 'Hello, world!', + cohort1.id, + 'firstName1', + 'lastName1', + 'Student 1 bio', 'student1' ) - const teacher = await createUser( - 'teacher@test.com', + const student2 = await createUser( + 'student2@test.com', + 'Testpassword1!', + cohort2.id, + 'firstName2', + 'lastName2', + 'Student 2 bio', + 'student2' + ) + const student3 = await createUser( + 'student3@test.com', + 'Testpassword1!', + cohort3.id, + 'firstName3', + 'lastName3', + 'Student 3 bio', + 'student3' + ) + + // Create 3 teachers + const teacher1 = await createUser( + 'teacher1@test.com', 'Testpassword1!', null, - 'Rick', - 'Sanchez', - 'Hello there!', + 'teacherFirst1', + 'teacherLast1', + 'Teacher 1 bio', 'teacher1', 'TEACHER' ) + const teacher2 = await createUser( + 'teacher2@test.com', + 'Testpassword1!', + null, + 'teacherFirst2', + 'teacherLast2', + 'Teacher 2 bio', + 'teacher2', + 'TEACHER' + ) + const teacher3 = await createUser( + 'teacher3@test.com', + 'Testpassword1!', + null, + 'teacherFirst3', + 'teacherLast3', + 'Teacher 3 bio', + 'teacher3', + 'TEACHER' + ) + + // Create 3 posts for each user + await createPost(student1.id, 'Student 1 first post!') + await createPost(student1.id, 'Student 1 second post!') + await createPost(student1.id, 'Student 1 third post!') + + await createPost(student2.id, 'Student 2 first post!') + await createPost(student2.id, 'Student 2 second post!') + await createPost(student2.id, 'Student 2 third post!') - await createPost(student.id, 'My first post!') - await createPost(teacher.id, 'Hello, students') + await createPost(student3.id, 'Student 3 first post!') + await createPost(student3.id, 'Student 3 second post!') + await createPost(student3.id, 'Student 3 third post!') + + await createPost(teacher1.id, 'Teacher 1 first post!') + await createPost(teacher1.id, 'Teacher 1 second post!') + await createPost(teacher1.id, 'Teacher 1 third post!') + + await createPost(teacher2.id, 'Teacher 2 first post!') + await createPost(teacher2.id, 'Teacher 2 second post!') + await createPost(teacher2.id, 'Teacher 2 third post!') + + await createPost(teacher3.id, 'Teacher 3 first post!') + await createPost(teacher3.id, 'Teacher 3 second post!') + await createPost(teacher3.id, 'Teacher 3 third post!') process.exit(0) } @@ -35,10 +101,23 @@ async function createPost(userId, content) { const post = await prisma.post.create({ data: { userId, - content + content, + likes: { + create: [{ userId: userId === 1 ? 2 : 1 }] + }, + comments: { + create: [ + { + content: 'Great post!', + userId: userId === 1 ? 2 : 1 + } + ] + } }, include: { - user: true + user: true, + likes: true, + comments: true } }) diff --git a/src/controllers/post.js b/src/controllers/post.js index 7b168039..758dc810 100644 --- a/src/controllers/post.js +++ b/src/controllers/post.js @@ -1,4 +1,5 @@ -import { sendDataResponse } from '../utils/responses.js' +import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' +import dbClient from '../utils/dbClient.js' export const create = async (req, res) => { const { content } = req.body @@ -11,18 +12,55 @@ export const create = async (req, res) => { } export const getAll = async (req, res) => { - return sendDataResponse(res, 200, { - posts: [ - { - id: 1, - content: 'Hello world!', - author: { ...req.user } + try { + const posts = await dbClient.post.findMany({ + orderBy: { + createdAt: 'desc' }, - { - id: 2, - content: 'Hello from the void!', - author: { ...req.user } + include: { + user: { + include: { + profile: { + select: { + firstName: true, + lastName: true + } + }, + cohort: { + select: { + id: true, + type: true + } + } + } + }, + _count: { + select: { + likes: true, + comments: true + } + } } - ] - }) + }) + + const formattedPosts = posts.map((post) => ({ + id: post.id, + content: post.content, + createdAt: post.createdAt, + author: { + id: post.user.id, + firstName: post.user.profile.firstName, + lastName: post.user.profile.lastName, + cohort: post.user.cohort + }, + stats: { + likes: post._count.likes, + comments: post._count.comments + } + })) + + return sendDataResponse(res, 200, { posts: formattedPosts }) + } catch (error) { + return sendMessageResponse(res, 500, 'Unable to fetch posts') + } } From cfc43b086e022bb25b88139024bb2c5238d3ca2f Mon Sep 17 00:00:00 2001 From: Kristoffer Blucher Date: Thu, 27 Feb 2025 09:28:13 +0100 Subject: [PATCH 15/28] Added new route for user progress --- docs/openapi.yml | 68 ++++++++++++++++++++--------------------- src/controllers/user.js | 2 +- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index cb991346..6bc27c01 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -184,40 +184,40 @@ paths: '200': description: Successfully retrieved user progress. content: - application/json: - schema: - type: object - properties: - CompletedModules: - type: object - properties: - completed: - type: integer - example: 2 - total: - type: integer - example: 7 - description: "Number of modules completed out of the total modules." - CompletedUnits: - type: object - properties: - completed: - type: integer - example: 4 - total: - type: integer - example: 10 - description: "Number of units completed out of the total units." - CompletedExercises: - type: object - properties: - completed: - type: integer - example: 34 - total: - type: integer - example: 58 - description: "Number of exercises completed out of the total exercises." + application/json: + schema: + type: object + properties: + CompletedModules: + type: object + properties: + completed: + type: integer + example: 2 + total: + type: integer + example: 7 + description: "Number of modules completed out of the total modules." + CompletedUnits: + type: object + properties: + completed: + type: integer + example: 4 + total: + type: integer + example: 10 + description: "Number of units completed out of the total units." + CompletedExercises: + type: object + properties: + completed: + type: integer + example: 34 + total: + type: integer + example: 58 + description: "Number of exercises completed out of the total exercises." '403': description: Access denied - user does not have permission to view this progress. content: diff --git a/src/controllers/user.js b/src/controllers/user.js index c8c09af7..d6d32c7e 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -255,7 +255,7 @@ export const updateById = async (req, res) => { // Function for retrieving user progress export const getUserProgress = async (req, res) => { - const userId = req.params.id + const userId = parseInt(req.params.id) const currentUser = req.user // Check if user is authorized to view progress From 12b64a854272e224ff3b84451c772e2588b967f1 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Thu, 27 Feb 2025 09:49:47 +0100 Subject: [PATCH 16/28] Removed some migrations --- .../migration.sql | 29 ---- .../migration.sql | 12 -- .../migration.sql | 5 - .../migration.sql | 11 -- .../migration.sql | 27 ---- .../migration.sql | 5 - .../migration.sql | 49 ------- .../20250227083654_migration_1/migration.sql | 130 ++++++++++++++++++ 8 files changed, 130 insertions(+), 138 deletions(-) delete mode 100644 prisma/migrations/20220407150157_create_user_profile/migration.sql delete mode 100644 prisma/migrations/20220408083401_add_cohort_model/migration.sql delete mode 100644 prisma/migrations/20220408085613_add_user_role/migration.sql delete mode 100644 prisma/migrations/20220408104349_add_post_model/migration.sql delete mode 100644 prisma/migrations/20220408160514_add_delivery_log_models/migration.sql delete mode 100644 prisma/migrations/20250226143315_add_cohort_type/migration.sql delete mode 100644 prisma/migrations/20250227080825_add_created_at/migration.sql create mode 100644 prisma/migrations/20250227083654_migration_1/migration.sql diff --git a/prisma/migrations/20220407150157_create_user_profile/migration.sql b/prisma/migrations/20220407150157_create_user_profile/migration.sql deleted file mode 100644 index ac6a38a8..00000000 --- a/prisma/migrations/20220407150157_create_user_profile/migration.sql +++ /dev/null @@ -1,29 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Profile" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "firstName" TEXT NOT NULL, - "lastName" TEXT NOT NULL, - "bio" TEXT, - "githubUrl" TEXT, - - CONSTRAINT "Profile_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId"); - --- AddForeignKey -ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20220408083401_add_cohort_model/migration.sql b/prisma/migrations/20220408083401_add_cohort_model/migration.sql deleted file mode 100644 index a052ca37..00000000 --- a/prisma/migrations/20220408083401_add_cohort_model/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ --- AlterTable -ALTER TABLE "User" ADD COLUMN "cohortId" INTEGER; - --- CreateTable -CREATE TABLE "Cohort" ( - "id" SERIAL NOT NULL, - - CONSTRAINT "Cohort_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "User" ADD CONSTRAINT "User_cohortId_fkey" FOREIGN KEY ("cohortId") REFERENCES "Cohort"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20220408085613_add_user_role/migration.sql b/prisma/migrations/20220408085613_add_user_role/migration.sql deleted file mode 100644 index 306fa4b0..00000000 --- a/prisma/migrations/20220408085613_add_user_role/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- CreateEnum -CREATE TYPE "Role" AS ENUM ('STUDENT', 'TEACHER'); - --- AlterTable -ALTER TABLE "User" ADD COLUMN "role" "Role" NOT NULL DEFAULT E'STUDENT'; diff --git a/prisma/migrations/20220408104349_add_post_model/migration.sql b/prisma/migrations/20220408104349_add_post_model/migration.sql deleted file mode 100644 index 0be857f3..00000000 --- a/prisma/migrations/20220408104349_add_post_model/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ --- CreateTable -CREATE TABLE "Post" ( - "id" SERIAL NOT NULL, - "content" TEXT NOT NULL, - "userId" INTEGER NOT NULL, - - CONSTRAINT "Post_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "Post" ADD CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20220408160514_add_delivery_log_models/migration.sql b/prisma/migrations/20220408160514_add_delivery_log_models/migration.sql deleted file mode 100644 index 3bf9dece..00000000 --- a/prisma/migrations/20220408160514_add_delivery_log_models/migration.sql +++ /dev/null @@ -1,27 +0,0 @@ --- CreateTable -CREATE TABLE "DeliveryLog" ( - "id" SERIAL NOT NULL, - "date" TIMESTAMP(3) NOT NULL, - "userId" INTEGER NOT NULL, - "cohortId" INTEGER NOT NULL, - - CONSTRAINT "DeliveryLog_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "DeliveryLogLine" ( - "id" SERIAL NOT NULL, - "content" TEXT NOT NULL, - "logId" INTEGER NOT NULL, - - CONSTRAINT "DeliveryLogLine_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "DeliveryLog" ADD CONSTRAINT "DeliveryLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "DeliveryLog" ADD CONSTRAINT "DeliveryLog_cohortId_fkey" FOREIGN KEY ("cohortId") REFERENCES "Cohort"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "DeliveryLogLine" ADD CONSTRAINT "DeliveryLogLine_logId_fkey" FOREIGN KEY ("logId") REFERENCES "DeliveryLog"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250226143315_add_cohort_type/migration.sql b/prisma/migrations/20250226143315_add_cohort_type/migration.sql deleted file mode 100644 index 4c29d4e2..00000000 --- a/prisma/migrations/20250226143315_add_cohort_type/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- CreateEnum -CREATE TYPE "CohortType" AS ENUM ('SOFTWARE_DEVELOPMENT', 'FRONTEND_DEVELOPMENT', 'DATA_ANALYTICS'); - --- AlterTable -ALTER TABLE "Cohort" ADD COLUMN "type" "CohortType" NOT NULL DEFAULT E'SOFTWARE_DEVELOPMENT'; diff --git a/prisma/migrations/20250227080825_add_created_at/migration.sql b/prisma/migrations/20250227080825_add_created_at/migration.sql deleted file mode 100644 index 809be098..00000000 --- a/prisma/migrations/20250227080825_add_created_at/migration.sql +++ /dev/null @@ -1,49 +0,0 @@ -/* - Warnings: - - - Added the required column `updatedAt` to the `Post` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Post" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, -ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; - --- CreateTable -CREATE TABLE "PostLike" ( - "id" SERIAL NOT NULL, - "postId" INTEGER NOT NULL, - "userId" INTEGER NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "PostLike_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "PostComment" ( - "id" SERIAL NOT NULL, - "content" TEXT NOT NULL, - "postId" INTEGER NOT NULL, - "userId" INTEGER NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "PostComment_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "PostLike_postId_userId_key" ON "PostLike"("postId", "userId"); - --- CreateIndex -CREATE INDEX "Post_createdAt_idx" ON "Post"("createdAt" DESC); - --- AddForeignKey -ALTER TABLE "PostLike" ADD CONSTRAINT "PostLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "PostLike" ADD CONSTRAINT "PostLike_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250227083654_migration_1/migration.sql b/prisma/migrations/20250227083654_migration_1/migration.sql new file mode 100644 index 00000000..bb675b66 --- /dev/null +++ b/prisma/migrations/20250227083654_migration_1/migration.sql @@ -0,0 +1,130 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('STUDENT', 'TEACHER'); + +-- CreateEnum +CREATE TYPE "CohortType" AS ENUM ('SOFTWARE_DEVELOPMENT', 'FRONTEND_DEVELOPMENT', 'DATA_ANALYTICS'); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "Role" NOT NULL DEFAULT E'STUDENT', + "cohortId" INTEGER, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Profile" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "bio" TEXT, + "githubUrl" TEXT, + + CONSTRAINT "Profile_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Cohort" ( + "id" SERIAL NOT NULL, + "type" "CohortType" NOT NULL DEFAULT E'SOFTWARE_DEVELOPMENT', + + CONSTRAINT "Cohort_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Post" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Post_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PostLike" ( + "id" SERIAL NOT NULL, + "postId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PostLike_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PostComment" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "postId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PostComment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DeliveryLog" ( + "id" SERIAL NOT NULL, + "date" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + "cohortId" INTEGER NOT NULL, + + CONSTRAINT "DeliveryLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DeliveryLogLine" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "logId" INTEGER NOT NULL, + + CONSTRAINT "DeliveryLogLine_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId"); + +-- CreateIndex +CREATE INDEX "Post_createdAt_idx" ON "Post"("createdAt" DESC); + +-- CreateIndex +CREATE UNIQUE INDEX "PostLike_postId_userId_key" ON "PostLike"("postId", "userId"); + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_cohortId_fkey" FOREIGN KEY ("cohortId") REFERENCES "Cohort"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Post" ADD CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostLike" ADD CONSTRAINT "PostLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostLike" ADD CONSTRAINT "PostLike_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DeliveryLog" ADD CONSTRAINT "DeliveryLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DeliveryLog" ADD CONSTRAINT "DeliveryLog_cohortId_fkey" FOREIGN KEY ("cohortId") REFERENCES "Cohort"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DeliveryLogLine" ADD CONSTRAINT "DeliveryLogLine_logId_fkey" FOREIGN KEY ("logId") REFERENCES "DeliveryLog"("id") ON DELETE RESTRICT ON UPDATE CASCADE; From b97906d880d638fbdccb561e17bd635f9be6d747 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:12:28 +0100 Subject: [PATCH 17/28] Closes #18 --- docs/openapi.yml | 69 +++++++++++++++++++++++++++++++++++++++++ src/controllers/post.js | 68 ++++++++++++++++++++++++++++++++++++++++ src/routes/post.js | 3 +- 3 files changed, 139 insertions(+), 1 deletion(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index 968b8ecc..0f807d88 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -221,6 +221,60 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /posts/{id}/toggle-like: + post: + tags: + - post + summary: Toggle like on a post + description: Like or unlike a post. If the post is already liked by the user, it will unlike it, and vice versa. Requires authentication. + operationId: togglePostLike + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: ID of the post to toggle like + required: true + schema: + type: integer + responses: + '200': + description: Successfully toggled like status + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + data: + type: object + properties: + liked: + type: boolean + description: Indicates whether the post is now liked (true) or unliked (false) + likeCount: + type: integer + description: The updated total number of likes for the post + '401': + description: Unauthorized - User must be logged in + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Post not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /logs: post: tags: @@ -620,3 +674,18 @@ components: type: integer content: type: string + + ToggleLikeResponse: + type: object + properties: + status: + type: string + data: + type: object + properties: + liked: + type: boolean + description: Indicates whether the post is now liked (true) or unliked (false) + likeCount: + type: integer + description: The updated total number of likes for the post diff --git a/src/controllers/post.js b/src/controllers/post.js index 758dc810..ed09b05c 100644 --- a/src/controllers/post.js +++ b/src/controllers/post.js @@ -64,3 +64,71 @@ export const getAll = async (req, res) => { return sendMessageResponse(res, 500, 'Unable to fetch posts') } } + +export const toggleLike = async (req, res) => { + const postId = parseInt(req.params.id) + const userId = req.user.id + + try { + // Check if user is authenticated + if (!req.user) { + return sendDataResponse(res, 401, { + authorization: 'You must be logged in to perform this action' + }) + } + + // Check if post exists + const post = await dbClient.post.findUnique({ + where: { id: postId } + }) + + if (!post) { + return sendDataResponse(res, 404, { error: 'Post not found' }) + } + + // Check if user has already liked the post + const existingLike = await dbClient.postLike.findUnique({ + where: { + postId_userId: { + postId, + userId + } + } + }) + + if (existingLike) { + // Unlike: Remove the like + await dbClient.postLike.delete({ + where: { + postId_userId: { + postId, + userId + } + } + }) + } else { + // Like: Create new like + await dbClient.postLike.create({ + data: { + postId, + userId + } + }) + } + + // Get updated like count + const likeCount = await dbClient.postLike.count({ + where: { + postId + } + }) + + return sendDataResponse(res, 200, { + liked: !existingLike, + likeCount + }) + } catch (error) { + console.error('Error toggling post like:', error) + return sendMessageResponse(res, 500, 'Unable to process like/unlike action') + } +} diff --git a/src/routes/post.js b/src/routes/post.js index a7fbbfb3..9577f043 100644 --- a/src/routes/post.js +++ b/src/routes/post.js @@ -1,10 +1,11 @@ import { Router } from 'express' -import { create, getAll } from '../controllers/post.js' +import { create, getAll, toggleLike } from '../controllers/post.js' import { validateAuthentication } from '../middleware/auth.js' const router = Router() router.post('/', validateAuthentication, create) router.get('/', validateAuthentication, getAll) +router.post('/:id/toggle-like', validateAuthentication, toggleLike) export default router From ff0fe945101fd798dfdbf5d461c64b1271f70c69 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Thu, 27 Feb 2025 12:21:40 +0100 Subject: [PATCH 18/28] implemented edit post by id --- docs/openapi.yml | 48 +++++++++++++++++++++++++++++++++++++++++ src/controllers/post.js | 32 +++++++++++++++++++++++++++ src/routes/post.js | 3 ++- 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index 2671fce5..1ee8c31b 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -305,6 +305,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /posts/{id}: + patch: + tags: + - post + summary: Update a post + description: Only the author of the post can update it. + operationId: postEdit + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 'The post id that needs to be updated' + required: true + schema: + type: string + requestBody: + description: The post info + content: + application/json: + schema: + $ref: '#/components/schemas/UpdatePost' + responses: + '201': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Post' + '401': + description: Unauthorized access to edit post + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Post not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /logs: post: tags: @@ -536,6 +578,12 @@ components: password: type: string + UpdatePost: + type: object + properties: + content: + type: string + UpdateUser: type: object properties: diff --git a/src/controllers/post.js b/src/controllers/post.js index 758dc810..09fdcb62 100644 --- a/src/controllers/post.js +++ b/src/controllers/post.js @@ -64,3 +64,35 @@ export const getAll = async (req, res) => { return sendMessageResponse(res, 500, 'Unable to fetch posts') } } + +export const editPostById = async (req, res) => { + const { id } = req.params + const { content } = req.body + + const currPost = await dbClient.post.findUnique({ + where: { + id: parseInt(id) + } + }) + + const userId = req.user.id + + if (currPost.userId !== userId) { + return sendDataResponse(res, 403, { error: 'Unauthorized' }) + } + + if (currPost === null) { + return sendDataResponse(res, 404, { content: 'Not found' }) + } + + try { + const updatedPost = await dbClient.post.update({ + where: { id: parseInt(id) }, + data: { content } + }) + + return sendDataResponse(res, 201, { post: updatedPost }) + } catch (error) { + return sendMessageResponse(res, 500, 'Unable to update post') + } +} diff --git a/src/routes/post.js b/src/routes/post.js index a7fbbfb3..c5035b68 100644 --- a/src/routes/post.js +++ b/src/routes/post.js @@ -1,10 +1,11 @@ import { Router } from 'express' -import { create, getAll } from '../controllers/post.js' +import { create, getAll, editPostById } from '../controllers/post.js' import { validateAuthentication } from '../middleware/auth.js' const router = Router() router.post('/', validateAuthentication, create) router.get('/', validateAuthentication, getAll) +router.patch('/:id', validateAuthentication, editPostById) export default router From f300345568dfbdc42b12e0b16e666485cf5c7a33 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Thu, 27 Feb 2025 12:34:47 +0100 Subject: [PATCH 19/28] edit --- prisma/seed.js | 1 - 1 file changed, 1 deletion(-) diff --git a/prisma/seed.js b/prisma/seed.js index 317c3377..f90ea7b4 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -36,7 +36,6 @@ async function seed() { 'Student 3 bio', 'student3' ) - // Create 3 teachers const teacher1 = await createUser( 'teacher1@test.com', From 1b5c1895df278f52c2fad38c44945e0568cc6281 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Thu, 27 Feb 2025 12:55:06 +0100 Subject: [PATCH 20/28] Merged --- src/routes/post.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/routes/post.js b/src/routes/post.js index 391011cc..126f5837 100644 --- a/src/routes/post.js +++ b/src/routes/post.js @@ -1,5 +1,10 @@ import { Router } from 'express' -import { create, getAll, editPostById, toggleLike } from '../controllers/post.js' +import { + create, + getAll, + editPostById, + toggleLike +} from '../controllers/post.js' import { validateAuthentication } from '../middleware/auth.js' const router = Router() From 402f402e144562605ac6d6b6e94489634e9197d5 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:05:43 +0100 Subject: [PATCH 21/28] Closes #19 --- docs/openapi.yml | 84 ++++++++++++++++++++++++++++++++++++++ src/controllers/comment.js | 66 ++++++++++++++++++++++++++++++ src/routes/comment.js | 9 ++++ src/server.js | 2 + 4 files changed, 161 insertions(+) create mode 100644 src/controllers/comment.js create mode 100644 src/routes/comment.js diff --git a/docs/openapi.yml b/docs/openapi.yml index bdee8ded..373eaf90 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -507,6 +507,90 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /posts/{postId}/comments: + post: + tags: + - post + summary: Create a comment on a post + description: Add a new comment to a specific post. Requires authentication. + operationId: createComment + security: + - bearerAuth: [] + parameters: + - name: postId + in: path + description: ID of the post to comment on + required: true + schema: + type: integer + requestBody: + description: Comment content + required: true + content: + application/json: + schema: + type: object + properties: + content: + type: string + description: The content of the comment + responses: + '201': + description: Comment created successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + data: + type: object + properties: + comment: + type: object + properties: + id: + type: integer + content: + type: string + createdAt: + type: string + format: date-time + author: + type: object + properties: + id: + type: integer + firstName: + type: string + lastName: + type: string + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized - User must be logged in + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Post not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' components: securitySchemes: diff --git a/src/controllers/comment.js b/src/controllers/comment.js new file mode 100644 index 00000000..42340dc1 --- /dev/null +++ b/src/controllers/comment.js @@ -0,0 +1,66 @@ +import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' +import dbClient from '../utils/dbClient.js' + +export const createComment = async (req, res) => { + const { content } = req.body + const postId = parseInt(req.params.postId) + const userId = req.user.id + + try { + // Input validation + if (!content || content.trim().length === 0) { + return sendDataResponse(res, 400, { + content: 'Comment content cannot be empty' + }) + } + + // Check if post exists + const post = await dbClient.post.findUnique({ + where: { id: postId } + }) + + if (!post) { + return sendDataResponse(res, 404, { + post: 'Post not found' + }) + } + + // Create comment + const comment = await dbClient.postComment.create({ + data: { + content: content.trim(), + postId, + userId + }, + include: { + user: { + include: { + profile: { + select: { + firstName: true, + lastName: true + } + } + } + } + } + }) + + // Format response + const formattedComment = { + id: comment.id, + content: comment.content, + createdAt: comment.createdAt, + author: { + id: comment.user.id, + firstName: comment.user.profile.firstName, + lastName: comment.user.profile.lastName + } + } + + return sendDataResponse(res, 201, { comment: formattedComment }) + } catch (error) { + console.error('Error creating comment:', error) + return sendMessageResponse(res, 500, 'Unable to create comment') + } +} diff --git a/src/routes/comment.js b/src/routes/comment.js new file mode 100644 index 00000000..28d70cbf --- /dev/null +++ b/src/routes/comment.js @@ -0,0 +1,9 @@ +import { Router } from 'express' +import { createComment } from '../controllers/comment.js' +import { validateAuthentication } from '../middleware/auth.js' + +const router = Router() + +router.post('/posts/:postId/comments', validateAuthentication, createComment) + +export default router diff --git a/src/server.js b/src/server.js index a3f67eeb..3dfb96b3 100644 --- a/src/server.js +++ b/src/server.js @@ -9,6 +9,7 @@ import postRouter from './routes/post.js' import authRouter from './routes/auth.js' import cohortRouter from './routes/cohort.js' import deliveryLogRouter from './routes/deliveryLog.js' +import commentRouter from './routes/comment.js' const app = express() app.disable('x-powered-by') @@ -26,6 +27,7 @@ app.use('/posts', postRouter) app.use('/cohorts', cohortRouter) app.use('/logs', deliveryLogRouter) app.use('/', authRouter) +app.use('/', commentRouter) app.get('*', (req, res) => { res.status(404).json({ From 2e04a36b887ca9c2aa8ad46206c9254dabf4baa9 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:52:35 +0100 Subject: [PATCH 22/28] Updated documentation --- docs/openapi.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index 3e981d87..9db2e7a6 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -139,7 +139,7 @@ paths: tags: - user summary: Update a user - description: Only users with a TEACHER role can update the cohortId or role. Users with Students role can only update their own details. + description: Only users with a TEACHER role can update the cohortId or role. Users with Students role can only update their own details. All fields are optional - only specified fields will be updated. operationId: userUpdate security: - bearerAuth: [] @@ -151,13 +151,13 @@ paths: schema: type: string requestBody: - description: The profile info + description: The profile info - all fields are optional content: application/json: schema: $ref: '#/components/schemas/UpdateUser' responses: - '201': + '200': description: Successful operation content: application/json: @@ -643,20 +643,28 @@ components: properties: email: type: string + nullable: true password: type: string + nullable: true cohortId: type: integer + nullable: true role: type: string + nullable: true firstName: type: string + nullable: true lastName: type: string + nullable: true bio: type: string + nullable: true githubUrl: type: string + nullable: true Posts: type: object From 0492211ac25db1fc68ceeaa4d76d74cc2a44e366 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:01:21 +0100 Subject: [PATCH 23/28] Added maximum comment length in both the controller and database schema. Length limit is set at 2560. --- .../20250227125913_add_comment_length_limit/migration.sql | 8 ++++++++ prisma/schema.prisma | 2 +- src/controllers/comment.js | 7 +++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20250227125913_add_comment_length_limit/migration.sql diff --git a/prisma/migrations/20250227125913_add_comment_length_limit/migration.sql b/prisma/migrations/20250227125913_add_comment_length_limit/migration.sql new file mode 100644 index 00000000..03cf0f91 --- /dev/null +++ b/prisma/migrations/20250227125913_add_comment_length_limit/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to alter the column `content` on the `PostComment` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(2560)`. + +*/ +-- AlterTable +ALTER TABLE "PostComment" ALTER COLUMN "content" SET DATA TYPE VARCHAR(2560); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1c9be77d..574875c8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -80,7 +80,7 @@ model PostLike { model PostComment { id Int @id @default(autoincrement()) - content String + content String @db.VarChar(2560) post Post @relation(fields: [postId], references: [id], onDelete: Cascade) postId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) diff --git a/src/controllers/comment.js b/src/controllers/comment.js index 42340dc1..85e9eff1 100644 --- a/src/controllers/comment.js +++ b/src/controllers/comment.js @@ -14,6 +14,13 @@ export const createComment = async (req, res) => { }) } + const MAX_COMMENT_LENGTH = 2560 + if (content.length > MAX_COMMENT_LENGTH) { + return sendDataResponse(res, 400, { + content: `Comment content cannot exceed ${MAX_COMMENT_LENGTH} characters` + }) + } + // Check if post exists const post = await dbClient.post.findUnique({ where: { id: postId } From cbe9cb244367c0bee9e05e71b35246efb5698cb8 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:07:52 +0100 Subject: [PATCH 24/28] Updated documentation further --- docs/openapi.yml | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index 9db2e7a6..9c5272c8 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -139,7 +139,12 @@ paths: tags: - user summary: Update a user - description: Only users with a TEACHER role can update the cohortId or role. Users with Students role can only update their own details. All fields are optional - only specified fields will be updated. + description: | + Updates user profile information with the following rules: + - Users can only update their own profile unless they have TEACHER role + - Only TEACHER role can update cohortId and role fields + - All fields are optional and only specified valid fields will be updated + - All fields must pass validation patterns (names, email, password, etc.) operationId: userUpdate security: - bearerAuth: [] @@ -158,13 +163,37 @@ paths: $ref: '#/components/schemas/UpdateUser' responses: '200': - description: Successful operation + description: User successfully updated content: application/json: schema: $ref: '#/components/schemas/CreatedUser' + '400': + description: Validation error or no valid fields provided + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '401': - description: fail + description: Unauthorized - Invalid or missing authentication + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - Insufficient permissions or attempting to modify restricted fields + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Server error content: application/json: schema: From 2b0cff45b330f4c6d597d310cc1daa2a23f89fdb Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Thu, 27 Feb 2025 14:32:32 +0100 Subject: [PATCH 25/28] removed profile logic into user, updated database/schema --- docs/openapi.yml | 18 ++- .../20250227120848_first/migration.sql | 25 ++++ prisma/schema.prisma | 17 +-- prisma/seed.js | 28 ++--- src/domain/user.js | 107 ++++++------------ 5 files changed, 95 insertions(+), 100 deletions(-) create mode 100644 prisma/migrations/20250227120848_first/migration.sql diff --git a/docs/openapi.yml b/docs/openapi.yml index 3e981d87..29dcd2a2 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -601,16 +601,18 @@ components: properties: id: type: integer - email: - type: string - role: - type: string cohortId: type: integer + role: + type: string firstName: type: string lastName: type: string + userName: + type: string + email: + type: string bio: type: string githubUrl: @@ -623,6 +625,8 @@ components: type: string lastName: type: string + userName: + type: string email: type: string bio: @@ -714,8 +718,6 @@ components: properties: id: type: integer - email: - type: string cohortId: type: integer role: @@ -724,6 +726,10 @@ components: type: string lastName: type: string + userName: + type: string + email: + type: string bio: type: string githubUrl: diff --git a/prisma/migrations/20250227120848_first/migration.sql b/prisma/migrations/20250227120848_first/migration.sql new file mode 100644 index 00000000..65a8a40b --- /dev/null +++ b/prisma/migrations/20250227120848_first/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - You are about to drop the `Profile` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `cohortNumber` to the `Cohort` table without a default value. This is not possible if the table is not empty. + - Added the required column `firstName` to the `User` table without a default value. This is not possible if the table is not empty. + - Added the required column `lastName` to the `User` table without a default value. This is not possible if the table is not empty. + - Added the required column `userName` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Profile" DROP CONSTRAINT "Profile_userId_fkey"; + +-- AlterTable +ALTER TABLE "Cohort" ADD COLUMN "cohortNumber" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "bio" TEXT, +ADD COLUMN "firstName" TEXT NOT NULL, +ADD COLUMN "githubUrl" TEXT, +ADD COLUMN "lastName" TEXT NOT NULL, +ADD COLUMN "userName" TEXT NOT NULL; + +-- DropTable +DROP TABLE "Profile"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1c9be77d..58bd6b15 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,8 +21,12 @@ model User { id Int @id @default(autoincrement()) email String @unique password String + userName String + firstName String + lastName String + githubUrl String? + bio String? role Role @default(STUDENT) - profile Profile? cohortId Int? cohort Cohort? @relation(fields: [cohortId], references: [id]) posts Post[] @@ -31,16 +35,6 @@ model User { postComments PostComment[] } -model Profile { - id Int @id @default(autoincrement()) - userId Int @unique - user User @relation(fields: [userId], references: [id]) - firstName String - lastName String - bio String? - githubUrl String? -} - enum CohortType { SOFTWARE_DEVELOPMENT FRONTEND_DEVELOPMENT @@ -51,6 +45,7 @@ model Cohort { id Int @id @default(autoincrement()) type CohortType @default(SOFTWARE_DEVELOPMENT) users User[] + cohortNumber Int deliveryLogs DeliveryLog[] } diff --git a/prisma/seed.js b/prisma/seed.js index f90ea7b4..bda9ce53 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -13,6 +13,7 @@ async function seed() { 'student1@test.com', 'Testpassword1!', cohort1.id, + 'student1username', 'firstName1', 'lastName1', 'Student 1 bio', @@ -22,6 +23,7 @@ async function seed() { 'student2@test.com', 'Testpassword1!', cohort2.id, + 'student2username', 'firstName2', 'lastName2', 'Student 2 bio', @@ -31,6 +33,7 @@ async function seed() { 'student3@test.com', 'Testpassword1!', cohort3.id, + 'student3username', 'firstName3', 'lastName3', 'Student 3 bio', @@ -41,6 +44,7 @@ async function seed() { 'teacher1@test.com', 'Testpassword1!', null, + 'teacher1username', 'teacherFirst1', 'teacherLast1', 'Teacher 1 bio', @@ -51,6 +55,7 @@ async function seed() { 'teacher2@test.com', 'Testpassword1!', null, + 'teacher2username', 'teacherFirst2', 'teacherLast2', 'Teacher 2 bio', @@ -61,6 +66,7 @@ async function seed() { 'teacher3@test.com', 'Testpassword1!', null, + 'teacher3username', 'teacherFirst3', 'teacherLast3', 'Teacher 3 bio', @@ -128,7 +134,8 @@ async function createPost(userId, content) { async function createCohort() { const cohort = await prisma.cohort.create({ data: { - type: 'SOFTWARE_DEVELOPMENT' + type: 'SOFTWARE_DEVELOPMENT', + cohortNumber: 1 } }) @@ -141,6 +148,7 @@ async function createUser( email, password, cohortId, + userName, firstName, lastName, bio, @@ -151,19 +159,13 @@ async function createUser( data: { email, password: await bcrypt.hash(password, 8), - role, cohortId, - profile: { - create: { - firstName, - lastName, - bio, - githubUrl - } - } - }, - include: { - profile: true + userName, + firstName, + lastName, + bio, + githubUrl, + role } }) diff --git a/src/domain/user.js b/src/domain/user.js index 659ed0a6..21164592 100644 --- a/src/domain/user.js +++ b/src/domain/user.js @@ -7,34 +7,45 @@ export default class User { * take as inputs, what types they return, and other useful information that JS doesn't have built in * @tutorial https://www.valentinog.com/blog/jsdoc * - * @param { { id: int, cohortId: int, email: string, profile: { firstName: string, lastName: string, bio: string, githubUrl: string } } } user + * @param { { id: int, cohortId: int, firstName: string, lastName: string, userName: string, email: string bio: string, githubUrl: string } } user * @returns {User} */ static fromDb(user) { return new User( user.id, user.cohortId, - user.profile?.firstName, - user.profile?.lastName, + user.role, + user.firstName, + user.lastName, + user.userName, user.email, - user.profile?.bio, - user.profile?.githubUrl, - user.password, - user.role + user.bio, + user.githubUrl, + user.password ) } static async fromJson(json) { // eslint-disable-next-line camelcase - const { firstName, lastName, email, biography, githubUrl, password } = json + const { + firstName, + lastName, + userName, + email, + biography, + githubUrl, + password + } = json const passwordHash = await bcrypt.hash(password, 8) return new User( null, null, + 'STUDENT', firstName, lastName, + userName, email, biography, githubUrl, @@ -45,23 +56,25 @@ export default class User { constructor( id, cohortId, + role = 'STUDENT', firstName, lastName, + userName, email, bio, githubUrl, - passwordHash = null, - role = 'STUDENT' + passwordHash = null ) { this.id = id this.cohortId = cohortId + this.role = role this.firstName = firstName this.lastName = lastName + this.userName = userName this.email = email this.bio = bio this.githubUrl = githubUrl this.passwordHash = passwordHash - this.role = role } toJSON() { @@ -72,6 +85,7 @@ export default class User { role: this.role, firstName: this.firstName, lastName: this.lastName, + userName: this.userName, email: this.email, biography: this.bio, githubUrl: this.githubUrl @@ -85,9 +99,14 @@ export default class User { */ async save() { const data = { + role: this.role, + firstName: this.firstName, + lastName: this.lastName, + userName: this.userName, email: this.email, - password: this.passwordHash, - role: this.role + bio: this.bio, + githubUrl: this.githubUrl, + password: this.passwordHash } if (this.cohortId) { @@ -98,21 +117,8 @@ export default class User { } } - if (this.firstName && this.lastName) { - data.profile = { - create: { - firstName: this.firstName, - lastName: this.lastName, - bio: this.bio, - githubUrl: this.githubUrl - } - } - } const createdUser = await dbClient.user.create({ - data, - include: { - profile: true - } + data }) return User.fromDb(createdUser) @@ -142,9 +148,6 @@ export default class User { const foundUser = await dbClient.user.findUnique({ where: { [key]: value - }, - include: { - profile: true } }) @@ -161,20 +164,14 @@ export default class User { id: true, cohortId: true, role: true, - profile: { - select: { - firstName: true, - lastName: true - } - } + firstName: true, + lastName: true } } if (key !== undefined && value !== undefined) { query.where = { - profile: { - [key]: { equals: value, mode: 'insensitive' } - } + [key]: { equals: value, mode: 'insensitive' } } } @@ -188,10 +185,7 @@ export default class User { where: { id: this.id }, - data: {}, - include: { - profile: true - } + data: {} } // Handle user-level updates @@ -205,33 +199,6 @@ export default class User { updateData.data.cohortId = data.cohortId } - // Handle profile-related updates - const profileUpdates = {} - let hasProfileUpdates = false - - if (data.firstName && data.firstName !== 'string') { - profileUpdates.firstName = data.firstName - hasProfileUpdates = true - } - if (data.lastName && data.lastName !== 'string') { - profileUpdates.lastName = data.lastName - hasProfileUpdates = true - } - if (data.bio && data.bio !== 'string') { - profileUpdates.bio = data.bio - hasProfileUpdates = true - } - if (data.githubUrl && data.githubUrl !== 'string') { - profileUpdates.githubUrl = data.githubUrl - hasProfileUpdates = true - } - - if (hasProfileUpdates) { - updateData.data.profile = { - update: profileUpdates - } - } - const updatedUser = await dbClient.user.update(updateData) return User.fromDb(updatedUser) } From 2afbb323c01dc1d61b65892417bd4606d7579e86 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Fri, 28 Feb 2025 08:37:34 +0100 Subject: [PATCH 26/28] fixed small profile related issue in post --- src/controllers/post.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/controllers/post.js b/src/controllers/post.js index b2891124..d4957c57 100644 --- a/src/controllers/post.js +++ b/src/controllers/post.js @@ -20,12 +20,6 @@ export const getAll = async (req, res) => { include: { user: { include: { - profile: { - select: { - firstName: true, - lastName: true - } - }, cohort: { select: { id: true, @@ -49,8 +43,8 @@ export const getAll = async (req, res) => { createdAt: post.createdAt, author: { id: post.user.id, - firstName: post.user.profile.firstName, - lastName: post.user.profile.lastName, + firstName: post.user.firstName, + lastName: post.user.lastName, cohort: post.user.cohort }, stats: { From a1f0ca093b97e32db8eb24be8e962926284f0157 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Fri, 28 Feb 2025 08:59:03 +0100 Subject: [PATCH 27/28] fixed the comment issue with old profile logic still existing --- src/controllers/comment.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/controllers/comment.js b/src/controllers/comment.js index 85e9eff1..18f28ff9 100644 --- a/src/controllers/comment.js +++ b/src/controllers/comment.js @@ -41,13 +41,9 @@ export const createComment = async (req, res) => { }, include: { user: { - include: { - profile: { - select: { - firstName: true, - lastName: true - } - } + select: { + firstName: true, + lastName: true } } } @@ -60,8 +56,8 @@ export const createComment = async (req, res) => { createdAt: comment.createdAt, author: { id: comment.user.id, - firstName: comment.user.profile.firstName, - lastName: comment.user.profile.lastName + firstName: comment.user.firstName, + lastName: comment.user.lastName } } From 7460c0828705eb50e892c523675e15885c732382 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Fri, 28 Feb 2025 09:09:19 +0100 Subject: [PATCH 28/28] Closes #23 --- .../20250228080802_add_mobile_field/migration.sql | 2 ++ prisma/schema.prisma | 1 + src/controllers/user.js | 10 ++++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20250228080802_add_mobile_field/migration.sql diff --git a/prisma/migrations/20250228080802_add_mobile_field/migration.sql b/prisma/migrations/20250228080802_add_mobile_field/migration.sql new file mode 100644 index 00000000..c964a50a --- /dev/null +++ b/prisma/migrations/20250228080802_add_mobile_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "mobile" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d262c538..fff962c9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,6 +24,7 @@ model User { userName String firstName String lastName String + mobile String? githubUrl String? bio String? role Role @default(STUDENT) diff --git a/src/controllers/user.js b/src/controllers/user.js index a73f59a4..36e961dc 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -134,7 +134,8 @@ export const updateById = async (req, res) => { password, cohort_id: cohortIdSnake, cohortId: cohortIdCamel, - role + role, + mobile } = req.body // Input validation patterns @@ -145,7 +146,8 @@ export const updateById = async (req, res) => { /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/, biography: /^[\s\S]{0,500}$/, githubUrl: /^https:\/\/github\.com\/[a-zA-Z0-9-]+$/, - role: /^(STUDENT|TEACHER)$/ + role: /^(STUDENT|TEACHER)$/, + mobile: /^\+?[1-9]\d{1,14}$/ } // Validation helper function @@ -194,6 +196,9 @@ export const updateById = async (req, res) => { if (role && !validateField('role', role, validationPatterns.role)) { validationErrors.role = 'Invalid role' } + if (mobile && !validateField('mobile', mobile, validationPatterns.mobile)) { + validationErrors.mobile = 'Invalid mobile number format' + } // Check for validation errors if (Object.keys(validationErrors).length > 0) { @@ -269,6 +274,7 @@ export const updateById = async (req, res) => { addValidField('bio', biography) addValidField('githubUrl', githubUrl) addValidField('email', email) + addValidField('mobile', mobile) } // If no valid fields to update