diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 79297ed5..00000000 --- a/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -Dockerfile -.dockerignore -node_modules -.git 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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56cddc0a..1e2ff854 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,5 @@ name: CI on: [push, pull_request] -env: - DATABASE_URL: ${{ secrets.DATABASE_URL }} - JWT_TOKEN: ${{ secrets.JWT_TOKEN }} - JWT_EXPIRY: ${{ secrets.JWT_EXPIRY }} jobs: test: runs-on: ubuntu-latest @@ -14,4 +10,3 @@ jobs: node-version: 'lts/*' - run: npm ci - run: npx eslint src - - run: npx prisma migrate reset --force --skip-seed diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 471052aa..00000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Fly Deploy -on: - push: - branches: - - main -env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - DATABASE_URL: ${{ secrets.DATABASE_URL }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - JWT_EXPIRY: ${{ secrets.JWT_EXPIRY }} -jobs: - deploy: - name: Deploy app to fly.io - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: superfly/flyctl-actions/setup-flyctl@master - - run: echo DATABASE_URL=$DATABASE_URL >> .env - - run: echo JWT_SECRET=$JWT_SECRET >> .env - - run: echo JWT_EXPIRY=$JWT_EXPIRY >> .env - - run: flyctl deploy --remote-only diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index a41979b3..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx eslint src \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b0c8ba77..00000000 --- a/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -FROM debian:bullseye as builder - -ARG NODE_VERSION=18.12.1 - -RUN apt-get update; apt install -y curl -RUN curl https://get.volta.sh | bash -ENV VOLTA_HOME /root/.volta -ENV PATH /root/.volta/bin:$PATH -RUN volta install node@${NODE_VERSION} - -####################################################################### - -RUN mkdir /app -WORKDIR /app - -# NPM will not install any package listed in "devDependencies" when NODE_ENV is set to "production", -# to install all modules: "npm install --production=false". -# Ref: https://docs.npmjs.com/cli/v9/commands/npm-install#description - -ENV NODE_ENV production - -COPY . . - -RUN npm install -FROM debian:bullseye - -LABEL fly_launch_runtime="nodejs" - -COPY --from=builder /root/.volta /root/.volta -COPY --from=builder /app /app - -WORKDIR /app -ENV NODE_ENV production -ENV PATH /root/.volta/bin:$PATH - -CMD [ "npm", "run", "start" ] diff --git a/docs/openapi.yml b/docs/openapi.yml index 5f2a05f2..9a064a1d 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -2,10 +2,10 @@ openapi: 3.0.3 info: title: Team Dev Server API description: |- - version: 1.0 + version: "1.0" servers: - - url: http://localhost:4000/ + - url: "http://localhost:4000/" tags: - name: user - name: post @@ -32,6 +32,12 @@ paths: application/json: schema: $ref: '#/components/schemas/CreatedUser' + '400': + description: Invalid email/password supplied + content: + application/json: + schema: + $ref: '#/components/schemas/Error' get: tags: - user @@ -85,10 +91,8 @@ paths: application/json: schema: $ref: '#/components/schemas/loginRes' - '400': description: Invalid username/password supplied - /users/{id}: get: tags: @@ -117,7 +121,6 @@ paths: type: string data: $ref: '#/components/schemas/User' - '400': description: fail content: @@ -164,6 +167,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + '400': + description: Invalid email/password supplied + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /posts: post: tags: @@ -189,8 +198,14 @@ paths: application/json: schema: $ref: '#/components/schemas/Post' - 400: - description: fail + 401: + description: Unauthorised + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error content: application/json: schema: @@ -211,7 +226,140 @@ paths: schema: $ref: '#/components/schemas/Posts' '401': - description: fail + description: Unauthorised + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /posts/{id}: + get: + tags: + - post + summary: Get a post by id + description: get a post + operationId: getPostById + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 'The post id that needs to be updated' + required: true + schema: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Posts' + '401': + description: Unauthorised + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + patch: + tags: + - post + summary: Patch a post by id + description: patch a post + operationId: updatePostById + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 'The post id that needs to be updated' + required: true + schema: + type: integer + requestBody: + description: The post description + content: + application/json: + schema: + $ref: '#/components/schemas/UpdatePost' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Posts' + '401': + description: Unauthorised + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + tags: + - post + summary: Delete a post by id + description: delete a post + operationId: deletePostById + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 'The post id that needs to be updated' + required: true + schema: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Posts' + '401': + description: Unauthorised + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Server error content: application/json: schema: @@ -314,10 +462,10 @@ components: type: integer createdAt: type: string - format: string + format: date-time updatedAt: type: string - format: string + format: date-time AllUsers: type: object @@ -351,6 +499,20 @@ components: type: string githubUrl: type: string + username: + type: string + mobile: + type: string + specialism: + type: string + startDate: + type: string + format: date-time + endDate: + type: string + format: date-time + profileImage: + type: string CreateUser: type: object @@ -365,8 +527,22 @@ components: type: string githubUrl: type: string + username: + type: string + mobile: + type: string + specialism: + type: string + startDate: + type: string + format: date-time + endDate: + type: string + format: date-time password: type: string + profileImage: + type: string UpdateUser: type: object @@ -387,6 +563,20 @@ components: type: string githubUrl: type: string + username: + type: string + mobile: + type: string + specialism: + type: string + startDate: + type: string + format: date-time + endDate: + type: string + format: date-time + profileImage: + type: string Posts: type: object @@ -407,10 +597,10 @@ components: type: string createdAt: type: string - format: string + format: date-time updatedAt: type: string - format: string + format: date-time author: type: object properties: @@ -428,9 +618,27 @@ components: type: string githubUrl: type: string - profileImageUrl: + username: + type: string + mobile: + type: string + specialism: + type: string + startDate: + type: string + format: date-time + endDate: type: string - + format: date-time + profileImage: + type: string + + UpdatePost: + type: object + properties: + content: + type: string + CreatedUser: type: object properties: @@ -457,6 +665,21 @@ components: type: string githubUrl: type: string + username: + type: string + mobile: + type: string + specialism: + type: string + startDate: + type: string + format: date-time + endDate: + type: string + format: date-time + profileImage: + type: string + login: type: object properties: @@ -492,6 +715,21 @@ components: type: string githubUrl: type: string + username: + type: string + mobile: + type: string + specialism: + type: string + startDate: + type: string + format: date-time + endDate: + type: string + format: date-time + profileImage: + type: string + Error: type: object properties: diff --git a/fly.toml b/fly.toml deleted file mode 100644 index eac058e7..00000000 --- a/fly.toml +++ /dev/null @@ -1,42 +0,0 @@ -# fly.toml file generated for team-dev-backend-api on 2022-11-30T11:30:34Z - -app = "team-dev-backend-api" -kill_signal = "SIGINT" -kill_timeout = 5 -processes = [] - -[env] - PORT = "8080" - -[experimental] - allowed_public_ports = [] - auto_rollback = true - -[deploy] - release_command = "npx prisma migrate deploy" - -[[services]] - http_checks = [] - internal_port = 8080 - processes = ["app"] - protocol = "tcp" - script_checks = [] - [services.concurrency] - hard_limit = 25 - soft_limit = 20 - type = "connections" - - [[services.ports]] - force_https = true - handlers = ["http"] - port = 80 - - [[services.ports]] - handlers = ["tls", "http"] - port = 443 - - [[services.tcp_checks]] - grace_period = "1s" - interval = "15s" - restart_limit = 0 - timeout = "2s" diff --git a/package.json b/package.json index b5997d22..3021b44f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js", - "prepare": "husky install", "db-reset": "prisma migrate reset", "lint": "eslint .", "lint:fix": "eslint --fix", @@ -36,7 +35,6 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-promise": "^5.1.0", - "husky": "^7.0.4", "nodemon": "^2.0.15", "prettier": "^2.6.2", "prisma": "^3.12.0" diff --git a/prisma/migrations/20241029082651_add_profile_fields/migration.sql b/prisma/migrations/20241029082651_add_profile_fields/migration.sql new file mode 100644 index 00000000..4ca11f21 --- /dev/null +++ b/prisma/migrations/20241029082651_add_profile_fields/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "Profile" ADD COLUMN "endDate" TIMESTAMP(3), +ADD COLUMN "mobile" TEXT NOT NULL DEFAULT E'', +ADD COLUMN "specialism" TEXT NOT NULL DEFAULT E'', +ADD COLUMN "startDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "username" TEXT NOT NULL DEFAULT E''; diff --git a/prisma/migrations/20241029090014_add_profile_image/migration.sql b/prisma/migrations/20241029090014_add_profile_image/migration.sql new file mode 100644 index 00000000..b47489b4 --- /dev/null +++ b/prisma/migrations/20241029090014_add_profile_image/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Profile" ADD COLUMN "profileImage" TEXT; diff --git a/prisma/migrations/20241030083020_new_user_values/migration.sql b/prisma/migrations/20241030083020_new_user_values/migration.sql new file mode 100644 index 00000000..af5102c8 --- /dev/null +++ b/prisma/migrations/20241030083020_new_user_values/migration.sql @@ -0,0 +1 @@ +-- This is an empty migration. \ No newline at end of file diff --git a/prisma/migrations/20241030125841_add_created_updated_at_columns_to_post/migration.sql b/prisma/migrations/20241030125841_add_created_updated_at_columns_to_post/migration.sql new file mode 100644 index 00000000..d19aa2d3 --- /dev/null +++ b/prisma/migrations/20241030125841_add_created_updated_at_columns_to_post/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Post" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 72ec5632..1f331713 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,6 +36,12 @@ model Profile { lastName String bio String? githubUrl String? + username String @default("") + mobile String @default("") + specialism String @default("") + startDate DateTime @default(now()) + endDate DateTime? + profileImage String? } model Cohort { @@ -49,6 +55,8 @@ model Post { content String userId Int user User @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) } model DeliveryLog { diff --git a/prisma/seed.js b/prisma/seed.js index 21684795..0d22470c 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -7,22 +7,35 @@ async function seed() { const student = await createUser( 'student@test.com', - 'Testpassword1!', cohort.id, 'Joe', 'Bloggs', 'Hello, world!', - 'student1' + 'student1@github.com', + 'student1', + '123-456-7890', // mobile + 'Software Engineering', // specialism + new Date('2023-01-01'), // startDate + new Date('2023-12-31'), + null, + 'STUDENT', + 'Testpassword1!' ) const teacher = await createUser( 'teacher@test.com', - 'Testpassword1!', null, 'Rick', 'Sanchez', 'Hello there!', + 'teacher1@git.com', 'teacher1', - 'TEACHER' + '987-654-3210', + 'Teaching', + new Date('2022-01-01'), + new Date('2022-12-31'), + null, + 'TEACHER', + 'Testpassword1!' ) await createPost(student.id, 'My first post!') @@ -59,13 +72,19 @@ async function createCohort() { async function createUser( email, - password, cohortId, firstName, lastName, bio, githubUrl, - role = 'STUDENT' + username, + mobile, + specialism, + startDate, + endDate, + profileImage, + role = 'STUDENT', + password ) { const user = await prisma.user.create({ data: { @@ -78,7 +97,13 @@ async function createUser( firstName, lastName, bio, - githubUrl + githubUrl, + profileImage, + username, + mobile, + specialism, + startDate: new Date(startDate), + endDate: new Date(endDate) } } }, diff --git a/src/controllers/cohort.js b/src/controllers/cohort.js index cc39365b..818f14d4 100644 --- a/src/controllers/cohort.js +++ b/src/controllers/cohort.js @@ -1,12 +1,12 @@ -import { createCohort } from '../domain/cohort.js' -import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' - -export const create = async (req, res) => { - try { - const createdCohort = await createCohort() - - return sendDataResponse(res, 201, createdCohort) - } catch (e) { - return sendMessageResponse(res, 500, 'Unable to create cohort') - } -} +import { createCohort } from '../domain/cohort.js' +import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' + +export const create = async (req, res) => { + try { + const createdCohort = await createCohort() + + return sendDataResponse(res, 201, createdCohort) + } catch (e) { + return sendMessageResponse(res, 500, 'Unable to create cohort') + } +} diff --git a/src/controllers/post.js b/src/controllers/post.js index 7b168039..4500634e 100644 --- a/src/controllers/post.js +++ b/src/controllers/post.js @@ -1,28 +1,89 @@ import { sendDataResponse } from '../utils/responses.js' +import Post from '../domain/post.js' +import User from '../domain/user.js' export const create = async (req, res) => { const { content } = req.body + const user = await User.findById(req.user.id) if (!content) { return sendDataResponse(res, 400, { content: 'Must provide content' }) } - return sendDataResponse(res, 201, { post: { id: 1, content } }) + try { + const post = await Post.createPost(content, user) + if (post) { + return sendDataResponse(res, 201, { post }) + } else { + return sendDataResponse(res, 500, { content: 'Failed to create post' }) + } + } catch (error) { + return sendDataResponse(res, 500, { content: 'Internal server error' }) + } } export const getAll = async (req, res) => { - return sendDataResponse(res, 200, { - posts: [ - { - id: 1, - content: 'Hello world!', - author: { ...req.user } - }, - { - id: 2, - content: 'Hello from the void!', - author: { ...req.user } - } - ] - }) + const postsUnformatted = await Post.getAllPosts() + if (!postsUnformatted) { + return sendDataResponse(res, 500, { + content: 'Internal server error' + }) + } + + const posts = postsUnformatted.map((post) => post.toJSON()) + return sendDataResponse(res, 200, { posts }) +} + +export const getById = async (req, res) => { + const { id } = req.params + const post = await Post.getPostById(Number(id)) + + if (!post) { + return sendDataResponse(res, 404, { + content: `Post with id ${id} not found` + }) + } + + return sendDataResponse(res, 200, { post: post.toJSON() }) +} + +export const updateById = async (req, res) => { + const { id } = req.params + const { content } = req.body + + try { + const post = await Post.getPostById(Number(id)) + if (post) { + const updatedPost = await Post.updateContentById(Number(id), content) + return sendDataResponse(res, 200, { post: updatedPost }) + } else { + return sendDataResponse(res, 404, { + content: `Post with id ${id} not found` + }) + } + } catch (error) { + return sendDataResponse(res, 500, { + content: 'Internal server error' + }) + } +} + +export const deleteById = async (req, res) => { + const { id } = req.params + + try { + const post = await Post.getPostById(Number(id)) + if (post) { + const deletedPost = await Post.deletePostById(Number(id)) + return sendDataResponse(res, 200, { post: deletedPost }) + } else { + return sendDataResponse(res, 404, { + content: `Post with id ${id} not found` + }) + } + } catch (error) { + return sendDataResponse(res, 500, { + content: 'Internal server error' + }) + } } diff --git a/src/controllers/user.js b/src/controllers/user.js index 40ff0f1c..72421118 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -1,10 +1,11 @@ import User from '../domain/user.js' import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' +/* CREATES A NEW USER */ export const create = async (req, res) => { - const userToCreate = await User.fromJson(req.body) - try { + const userToCreate = await User.fromJson(req.body) + const existingUser = await User.findByEmail(userToCreate.email) if (existingUser) { @@ -15,10 +16,11 @@ export const create = async (req, res) => { return sendDataResponse(res, 201, createdUser) } catch (error) { + console.error('Error creating user:', error) return sendMessageResponse(res, 500, 'Unable to create new user') } } - +/* GETS A USER BY ID */ export const getById = async (req, res) => { const id = parseInt(req.params.id) @@ -35,6 +37,7 @@ export const getById = async (req, res) => { } } +/* GETS ALL USERS */ export const getAll = async (req, res) => { // eslint-disable-next-line camelcase const { first_name: firstName } = req.query @@ -55,13 +58,30 @@ export const getAll = async (req, res) => { return sendDataResponse(res, 200, { users: formattedUsers }) } +/* Updates a user by ID */ export const updateById = async (req, res) => { - const { cohort_id: cohortId } = req.body + const id = parseInt(req.params.id) + const userToUpdate = await User.fromJson(req.body) - if (!cohortId) { - return sendDataResponse(res, 400, { cohort_id: 'Cohort ID is required' }) - } + // Add id, cohortId and role (could be done in the domain) + userToUpdate.id = id + userToUpdate.cohortId = req.body.cohortId + userToUpdate.role = req.body.role + + console.log(userToUpdate) - return sendDataResponse(res, 201, { user: { cohort_id: cohortId } }) + try { + if (!userToUpdate.cohortId) { + return sendDataResponse(res, 400, { cohort_id: 'Cohort ID is required' }) + } + const updatedUser = await userToUpdate.update() + + return sendDataResponse(res, 201, updatedUser) + } catch (error) { + console.log(error) + return sendMessageResponse(res, 500, 'Unable to update user') + } } + +/* Test Commit statement */ diff --git a/src/domain/post.js b/src/domain/post.js new file mode 100644 index 00000000..0f992f10 --- /dev/null +++ b/src/domain/post.js @@ -0,0 +1,100 @@ +import dbClient from '../utils/dbClient.js' + +export default class Post { + constructor( + id = null, + content = '', + user = null, + createdAt = null, + updatedAt = null + ) { + this.id = id + this.content = content + this.user = user + this.createdAt = createdAt + this.updatedAt = updatedAt + } + + toJSON() { + return { + id: this.id, + content: this.content, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + author: { + id: this.user.id, + cohortId: this.user.cohortId, + role: this.user.role, + firstName: this.user.profile.firstName, + lastName: this.user.profile.lastName, + bio: this.user.profile.bio, + githubUrl: this.user.profile.githubUrl, + username: this.user.profile.username, + mobile: this.user.profile.mobile, + specialism: this.user.profile.specialism, + startDate: this.user.profile.startDate, + endDate: this.user.profile.endDate, + profileImage: this.user.profile.profileImage + } + } + } + + static async createPost(content, user) { + return dbClient.post.create({ + data: { content: content, userId: user.id } + }) + } + + static async getAllPosts() { + const posts = await dbClient.post.findMany({ + include: { + user: { + include: { profile: true } + } + } + }) + return posts.map( + (post) => + new Post( + post.id, + post.content, + post.user, + post.createdAt, + post.updatedAt + ) + ) + } + + static async getPostById(id) { + const post = await dbClient.post.findUnique({ + where: { id }, + include: { + user: { + include: { profile: true } + } + } + }) + return post + ? new Post( + post.id, + post.content, + post.user, + post.createdAt, + post.updatedAt + ) + : null + } + + static async updateContentById(id, content) { + return dbClient.post.update({ + where: { id: id }, + data: { content: content } + }) + } + + static async deletePostById(id) { + return dbClient.post.delete({ + where: { id: id } + }) + } +} diff --git a/src/domain/user.js b/src/domain/user.js index fd7734c7..51b2fc42 100644 --- a/src/domain/user.js +++ b/src/domain/user.js @@ -7,10 +7,11 @@ 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, email: string, role: string, profile: { firstName: string, lastName: string, bio: string, githubUrl: string, username:string, mobile, profileImage: string } } } user * @returns {User} */ static fromDb(user) { + console.log(user) return new User( user.id, user.cohortId, @@ -19,14 +20,33 @@ export default class User { user.email, user.profile?.bio, user.profile?.githubUrl, + user.profile?.username, + user.profile?.mobile, + user.profile?.specialism, + user.profile?.startDate, + user.profile?.endDate, user.password, + user.profile?.profileImage, user.role ) } static async fromJson(json) { // eslint-disable-next-line camelcase - const { firstName, lastName, email, biography, githubUrl, password } = json + const { + firstName, + lastName, + email, + bio, + githubUrl, + username, + mobile, + specialism, + startDate, + endDate, + password, + profileImage + } = json const passwordHash = await bcrypt.hash(password, 8) @@ -36,9 +56,15 @@ export default class User { firstName, lastName, email, - biography, + bio, githubUrl, - passwordHash + username, + mobile, + specialism, + startDate, + endDate, + passwordHash, + profileImage ) } @@ -50,7 +76,13 @@ export default class User { email, bio, githubUrl, + username, + mobile, + specialism, + startDate, + endDate, passwordHash = null, + profileImage = null, role = 'STUDENT' ) { this.id = id @@ -60,11 +92,18 @@ export default class User { this.email = email this.bio = bio this.githubUrl = githubUrl + this.username = username + this.mobile = mobile + this.specialism = specialism + this.startDate = startDate + this.endDate = endDate this.passwordHash = passwordHash this.role = role + this.profileImage = profileImage } toJSON() { + console.log(this) return { user: { id: this.id, @@ -73,8 +112,14 @@ export default class User { firstName: this.firstName, lastName: this.lastName, email: this.email, - biography: this.bio, - githubUrl: this.githubUrl + bio: this.bio, + githubUrl: this.githubUrl, + profileImage: this.profileImage, + username: this.username, + mobile: this.mobile, + specialism: this.specialism, + startDate: this.startDate, + endDate: this.endDate } } } @@ -104,7 +149,13 @@ export default class User { firstName: this.firstName, lastName: this.lastName, bio: this.bio, - githubUrl: this.githubUrl + githubUrl: this.githubUrl, + profileImage: this.profileImage, + username: this.username, + mobile: this.mobile, + specialism: this.specialism, + startDate: this.startDate, + endDate: this.endDate } } } @@ -118,6 +169,42 @@ export default class User { return User.fromDb(createdUser) } + /** + * @returns {User} + * A user instance containing the updated user data + */ + async update() { + const updatedUser = await dbClient.user.update({ + where: { + id: this.id + }, + data: { + email: this.email, + password: this.passwordHash, + role: this.role, + cohortId: this.cohortId, + profile: { + update: { + firstName: this.firstName, + lastName: this.lastName, + bio: this.bio, + githubUrl: this.githubUrl, + username: this.username, + mobile: this.mobile, + specialism: this.specialism, + startDate: this.startDate, + endDate: this.endDate + } + } + }, + include: { + profile: true + } + }) + + return User.fromDb(updatedUser) + } + static async findByEmail(email) { return User._findByUnique('email', email) } diff --git a/src/middleware/auth.js b/src/middleware/auth.js index baffff47..2d4f505b 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -4,6 +4,9 @@ import jwt from 'jsonwebtoken' import User from '../domain/user.js' export async function validateTeacherRole(req, res, next) { + if (res.locals.skipTeacherValidation) { + return next() + } if (!req.user) { return sendMessageResponse(res, 500, 'Unable to verify user') } @@ -17,6 +20,29 @@ export async function validateTeacherRole(req, res, next) { next() } +// Function that checks if the currently logged in user is the same +// one that is being requested, if so, we skip the teacher validation +export async function validateLoggedInUser(req, res, next) { + if (!req.user) { + return sendMessageResponse(res, 500, 'Unable to verify user') + } + + if (req.user.id === parseInt(req.params.id)) { + // Skip teacher validation if the user is updating their own profile + res.locals.skipTeacherValidation = true + + // Overwrite the request body with pre-existing values for cohortId and role, + // if the logged in user is a STUDENT + if (req.user.role === 'STUDENT') { + const existingUser = await User.findById(parseInt(req.params.id)) + req.body.cohortId = existingUser.cohortId + req.body.role = existingUser.role + } + } + + next() +} + export async function validateAuthentication(req, res, next) { const header = req.header('authorization') diff --git a/src/middleware/user.js b/src/middleware/user.js new file mode 100644 index 00000000..07accf30 --- /dev/null +++ b/src/middleware/user.js @@ -0,0 +1,49 @@ +import { sendDataResponse } from '../utils/responses.js' + +export async function validateUser(req, res, next) { + const validateEmail = (email) => { + if ( + email.length < 7 || + email.indexOf('@') <= 0 || + email.slice(-4) !== '.com' || + (email.match(/@/g) || []).length > 1 || + email.charAt(email.length - 5) === '@' + ) { + return 'Email is invalid' + } + return null + } + + const emailError = validateEmail(req.body.email) + if (emailError) { + return sendDataResponse(res, 400, { email: emailError }) + } + + const validatePassword = (password) => { + const minLength = 8 + const hasUpperCase = /[A-Z]/.test(password) + const hasNumber = /\d/.test(password) + const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password) + + if (password.length < minLength) { + return 'Password must be at least 8 characters long' + } + if (!hasUpperCase) { + return 'Password must contain at least one uppercase letter' + } + if (!hasNumber) { + return 'Password must contain at least one number' + } + if (!hasSpecialChar) { + return 'Password must contain at least one special character' + } + return null + } + + const passwordError = validatePassword(req.body.password) + if (passwordError) { + return sendDataResponse(res, 400, { password: passwordError }) + } + + next() +} diff --git a/src/routes/post.js b/src/routes/post.js index a7fbbfb3..38f2b81b 100644 --- a/src/routes/post.js +++ b/src/routes/post.js @@ -1,10 +1,19 @@ import { Router } from 'express' -import { create, getAll } from '../controllers/post.js' +import { + create, + getAll, + updateById, + getById, + deleteById +} from '../controllers/post.js' import { validateAuthentication } from '../middleware/auth.js' const router = Router() router.post('/', validateAuthentication, create) router.get('/', validateAuthentication, getAll) +router.get('/:id', validateAuthentication, getById) +router.patch('/:id', validateAuthentication, updateById) +router.delete('/:id', validateAuthentication, deleteById) export default router diff --git a/src/routes/user.js b/src/routes/user.js index 9f63d162..90287bdf 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -2,14 +2,23 @@ import { Router } from 'express' import { create, getById, getAll, updateById } from '../controllers/user.js' import { validateAuthentication, - validateTeacherRole + validateTeacherRole, + validateLoggedInUser } from '../middleware/auth.js' +import { validateUser } from '../middleware/user.js' const router = Router() -router.post('/', create) +router.post('/', validateUser, create) router.get('/', validateAuthentication, getAll) router.get('/:id', validateAuthentication, getById) -router.patch('/:id', validateAuthentication, validateTeacherRole, updateById) +router.patch( + '/:id', + validateAuthentication, + validateUser, + validateLoggedInUser, + validateTeacherRole, + updateById +) export default router