From 91ae33327b51bfa964f8ecc3db7dfe1483f19555 Mon Sep 17 00:00:00 2001 From: Axel Ahlander Date: Wed, 26 Feb 2025 10:13:41 +0100 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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 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 09/10] 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 10/10] 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: