From 292072fec9f8e8e5809eecea5567b00887c75a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20S=C3=B6nnergaard?= Date: Thu, 27 Feb 2025 09:42:56 +0100 Subject: [PATCH 01/11] Create Profile model --- src/domain/profile.js | 115 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/domain/profile.js diff --git a/src/domain/profile.js b/src/domain/profile.js new file mode 100644 index 00000000..906ba2fb --- /dev/null +++ b/src/domain/profile.js @@ -0,0 +1,115 @@ +import dbClient from '../utils/dbClient' + +export default class Profile { + static fromDb(profile) { + return new Profile( + profile.id, + profile.userId, + profile.firstName, + profile.lastName, + profile.bio, + profile.githubUrl + ) + } + + static async fromJson(json) { + const { userId, firstName, lastName, bio, githubUrl } = json + + return new Profile(null, userId, firstName, lastName, bio, githubUrl) + } + + constructor(id, userId, firstName, lastName, bio, githubUrl) { + this.id = id + this.userId = userId + this.firstName = firstName + this.lastName = lastName + this.bio = bio + this.githubUrl = githubUrl + } + + toJson() { + return { + profile: { + id: this.id, + user_id: this.userId, + firstName: this.firstName, + lastName: this.lastName, + bio: this.bio, + githubUrl: this.githubUrl + } + } + } + + async save() { + const data = { + firstName: this.firstName, + lastName: this.lastName, + bio: this.bio, + githubUrl: this.githubUrl + } + + if (this.userId) { + data.user = { + connectOrCreate: { + id: this.userId + } + } + } + + const createdProfile = await dbClient.profile.create({ + data, + include: { + user: true + } + }) + return Profile.fromDb(createdProfile) + } + + static async findById(id) { + return Profile._findByUnique('id', id) + } + + static async findAll() { + return Profile._findMany() + } + + static async findByUserId(userId) { + return Profile._findByUnique('userId', userId) + } + + static async _findByUnique(key, value) { + const foundProfile = await dbClient.profile.findUnique({ + where: { + [key]: value + }, + include: { + user: true + } + }) + + if (foundProfile) { + return Profile.fromDb(foundProfile) + } + + return null + } + + static async _findMany(key, value) { + const query = { + include: { + user: true + } + } + + if (key && value) { + query.where = { + user: { + [key]: value + } + } + } + + const foundProfiles = await dbClient.profile.findMany(query) + return foundProfiles.map((p) => Profile.fromDb(p)) + } +} From fd5836ab3fc0b72dd49c8e9e0f36eb53cafcebc5 Mon Sep 17 00:00:00 2001 From: Wilmer Winkler Date: Thu, 27 Feb 2025 10:21:16 +0100 Subject: [PATCH 02/11] Profile controller made --- .env.example | 4 ++-- src/controllers/profile.js | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/controllers/profile.js diff --git a/.env.example b/.env.example index 932b9f1e..d3c91282 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ PORT=4000 -DATABASE_URL="?schema=prisma" -SHADOW_DATABASE_URL="?schema=shadow" +DATABASE_URL= +SHADOW_DATABASE_URL= JWT_SECRET="somesecurestring" JWT_EXPIRY="24h" \ No newline at end of file diff --git a/src/controllers/profile.js b/src/controllers/profile.js new file mode 100644 index 00000000..cca9b0d8 --- /dev/null +++ b/src/controllers/profile.js @@ -0,0 +1,48 @@ +import Profile from '../domain/profile.js' +import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' + +export const create = async (req, res) => { + const profileToCreate = await Profile.fromJson(req.body) + + try { + const existingProfileWithUser = await Profile.findByUserId( + profileToCreate.userId + ) + + if (existingProfileWithUser) { + return sendDataResponse(res, 400, { email: 'You already have a profile' }) + } + + const createdProfile = await profileToCreate.save() + + return sendDataResponse(res, 201, createdProfile) + } catch (error) { + return sendMessageResponse(res, 500, 'Unable to create new profile') + } +} + +export const getById = async (req, res) => { + const id = parseInt(req.params.id) + + try { + const foundProfile = await Profile.findById(id) + + if (!foundProfile) { + return sendDataResponse(res, 404, { id: 'User not found' }) + } + + return sendDataResponse(res, 200, foundProfile) + } catch (e) { + return sendMessageResponse(res, 500, 'Unable to get user') + } +} + +export const updateById = async (req, res) => { + const { user_id: userId } = req.body + + if (!userId) { + return sendDataResponse(res, 400, { user_id: 'User ID is required' }) + } + + return sendDataResponse(res, 201, { user: { user_id: userId } }) +} From 4100f108faee9edec0c12d99ab925afcb6ce3f2e Mon Sep 17 00:00:00 2001 From: Wilmer Winkler Date: Thu, 27 Feb 2025 10:33:26 +0100 Subject: [PATCH 03/11] Profile routing with simple authentication. --- src/routes/profile.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/routes/profile.js diff --git a/src/routes/profile.js b/src/routes/profile.js new file mode 100644 index 00000000..a5ed0648 --- /dev/null +++ b/src/routes/profile.js @@ -0,0 +1,12 @@ +import { Router } from 'express' +import { create, getById, updateById } from '../controllers/profile.js' +import { validateAuthentication } from '../middleware/auth.js' + +const router = Router() + +// Need to validate +router.post('/', validateAuthentication, create) +router.get('/:id', validateAuthentication, getById) +router.put('/:id', validateAuthentication, updateById) + +export default router From d45a6f4910870c7567ca2d3cefb5203e8521a585 Mon Sep 17 00:00:00 2001 From: Martin Lind Date: Thu, 27 Feb 2025 10:40:19 +0100 Subject: [PATCH 04/11] added password validation --- src/controllers/user.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 40ff0f1c..d7c2be7b 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -3,14 +3,18 @@ import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' export const create = async (req, res) => { const userToCreate = await User.fromJson(req.body) - try { const existingUser = await User.findByEmail(userToCreate.email) + // eslint-disable-next-line + const regex = /(?=.*[a-z])(?=.*[A-Z{1,}])(?=.*[0-9{1,}])(?=.*[!-\/:-@[-`{-~{1,}]).{8,}/ if (existingUser) { return sendDataResponse(res, 400, { email: 'Email already in use' }) } + if (!regex.test(req.body.password)) { + return sendDataResponse(res, 400, { email: 'Invalid password' }) + } const createdUser = await userToCreate.save() return sendDataResponse(res, 201, createdUser) From 6c5e6009e9d40a1880debde04c05b111629036da Mon Sep 17 00:00:00 2001 From: Wilmer Winkler Date: Thu, 27 Feb 2025 11:04:35 +0100 Subject: [PATCH 05/11] Profile routes fixed --- src/domain/profile.js | 2 +- src/routes/profile.js | 2 +- src/server.js | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/domain/profile.js b/src/domain/profile.js index 906ba2fb..bb8ac84a 100644 --- a/src/domain/profile.js +++ b/src/domain/profile.js @@ -1,4 +1,4 @@ -import dbClient from '../utils/dbClient' +import dbClient from '../utils/dbClient.js' export default class Profile { static fromDb(profile) { diff --git a/src/routes/profile.js b/src/routes/profile.js index a5ed0648..3c06f06d 100644 --- a/src/routes/profile.js +++ b/src/routes/profile.js @@ -7,6 +7,6 @@ const router = Router() // Need to validate router.post('/', validateAuthentication, create) router.get('/:id', validateAuthentication, getById) -router.put('/:id', validateAuthentication, updateById) +router.patch('/:id', validateAuthentication, updateById) export default router diff --git a/src/server.js b/src/server.js index a3f67eeb..4f99d33e 100644 --- a/src/server.js +++ b/src/server.js @@ -7,6 +7,7 @@ import cors from 'cors' import userRouter from './routes/user.js' import postRouter from './routes/post.js' import authRouter from './routes/auth.js' +import profileRouter from './routes/profile.js' import cohortRouter from './routes/cohort.js' import deliveryLogRouter from './routes/deliveryLog.js' @@ -21,6 +22,7 @@ const docFile = fs.readFileSync('./docs/openapi.yml', 'utf8') const swaggerDoc = YAML.parse(docFile) app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc)) +app.use('/profile', profileRouter) app.use('/users', userRouter) app.use('/posts', postRouter) app.use('/cohorts', cohortRouter) From a52de200000166abc14f67b2fc73755424a2b82d Mon Sep 17 00:00:00 2001 From: Martin Lind Date: Thu, 27 Feb 2025 11:33:21 +0100 Subject: [PATCH 06/11] added email validation --- src/controllers/user.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index d7c2be7b..74801379 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -6,15 +6,21 @@ export const create = async (req, res) => { try { const existingUser = await User.findByEmail(userToCreate.email) // eslint-disable-next-line - const regex = /(?=.*[a-z])(?=.*[A-Z{1,}])(?=.*[0-9{1,}])(?=.*[!-\/:-@[-`{-~{1,}]).{8,}/ + const passwordRegex = /(?=.*[a-z])(?=.*[A-Z{1,}])(?=.*[0-9{1,}])(?=.*[!-\/:-@[-`{-~{1,}]).{8,}/ + const emailRegex = /[^@]{1,}[@]{1}[^@]{1,}/ if (existingUser) { return sendDataResponse(res, 400, { email: 'Email already in use' }) } - if (!regex.test(req.body.password)) { - return sendDataResponse(res, 400, { email: 'Invalid password' }) + if (!passwordRegex.test(req.body.password)) { + return sendDataResponse(res, 400, { password: 'Invalid password' }) } + + if (!emailRegex.test(req.body.email)) { + return sendDataResponse(res, 400, { email: 'Invalid email' }) + } + const createdUser = await userToCreate.save() return sendDataResponse(res, 201, createdUser) From b8edc9b70afd7edc9f6384ff76a37928b507ed97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20S=C3=B6nnergaard?= Date: Thu, 27 Feb 2025 13:36:54 +0100 Subject: [PATCH 07/11] Update prisma schema and seed --- .../20250227123420_better_posts/migration.sql | 45 +++++++++++++++++++ prisma/schema.prisma | 35 +++++++++++++-- prisma/seed.js | 10 ++++- 3 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 prisma/migrations/20250227123420_better_posts/migration.sql diff --git a/prisma/migrations/20250227123420_better_posts/migration.sql b/prisma/migrations/20250227123420_better_posts/migration.sql new file mode 100644 index 00000000..ba5f5aa8 --- /dev/null +++ b/prisma/migrations/20250227123420_better_posts/migration.sql @@ -0,0 +1,45 @@ +/* + Warnings: + + - Added the required column `createdAt` to the `Post` table without a default value. This is not possible if the table is not empty. + - 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, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- CreateTable +CREATE TABLE "Like" ( + "id" SERIAL NOT NULL, + "postId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Like_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Comment" ( + "id" SERIAL NOT NULL, + "postId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Like" ADD CONSTRAINT "Like_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Like" ADD CONSTRAINT "Like_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 72ec5632..25758f38 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,6 +26,8 @@ model User { cohort Cohort? @relation(fields: [cohortId], references: [id]) posts Post[] deliveryLogs DeliveryLog[] + comments Comment[] + likes Like[] } model Profile { @@ -45,10 +47,35 @@ 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]) + likes Like[] + comments Comment[] + createdAt DateTime + updatedAt DateTime +} + +model Like { + id Int @id @default(autoincrement()) + postId Int + post Post @relation(fields: [postId], references: [id]) + userId Int + user User @relation(fields: [userId], references: [id]) + createdAt DateTime + updatedAt DateTime +} + +model Comment { + id Int @id @default(autoincrement()) + postId Int + post Post @relation(fields: [postId], references: [id]) + userId Int + user User @relation(fields: [userId], references: [id]) + content String + createdAt DateTime + updatedAt DateTime } model DeliveryLog { diff --git a/prisma/seed.js b/prisma/seed.js index 21684795..386f04fe 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -34,8 +34,12 @@ async function seed() { async function createPost(userId, content) { const post = await prisma.post.create({ data: { - userId, - content + userId: userId, + content: content, + // likes: [], + // comments: [], + createdAt: new Date(), + updatedAt: new Date() }, include: { user: true @@ -81,6 +85,8 @@ async function createUser( githubUrl } } + // comments: [], + // likes: [] }, include: { profile: true From 85b4497fe936b30a6ff5468dcad567b456800592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20S=C3=B6nnergaard?= Date: Thu, 27 Feb 2025 16:04:27 +0100 Subject: [PATCH 08/11] Create Like model --- src/domain/like.js | 105 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/domain/like.js diff --git a/src/domain/like.js b/src/domain/like.js new file mode 100644 index 00000000..a60ba5e0 --- /dev/null +++ b/src/domain/like.js @@ -0,0 +1,105 @@ +import dbClient from '../utils/dbClient' + +export default class Like { + static fromDb(like) { + return new Like( + like.id, + like.postId, + like.userId, + like.createdAt, + like.updatedAt + ) + } + + static async fromJson(json) { + const { postId, userId, createdAt, updatedAt } = json + return new Like(null, postId, userId, createdAt, updatedAt) + } + + constructor(id, postId, userId, createdAt, updatedAt) { + this.id = id + this.postId = postId + this.userId = userId + this.createdAt = createdAt + this.updatedAt = updatedAt + } + + toJson() { + return { + like: { + id: this.id, + postId: this.postId, + userId: this.userId, + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + } + } + + async save() { + const data = { + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + + if (this.postId) { + data.postId = { + connectOrCreate: { + id: this.postId + } + } + } + + if (this.userId) { + data.user = { + connectOrCreate: { + id: this.userId + } + } + } + + const createdLike = await dbClient.like.create({ + data, + include: { + post: true, + user: true + } + }) + return Like.fromDb(createdLike) + } + + static async findAll() { + return Like._findMany() + } + + static async findByPostId(postId) { + return Like._findMany('postId', postId) + } + + static async findByUserId(userId) { + return Like._findMany('userId', userId) + } + + static async _findMany(key, value) { + const query = { + include: { + post: true, + user: true + } + } + + if (key && value) { + query.where = { + post: { + [key]: value + }, + user: { + [key]: value + } + } + } + + const foundLikes = await dbClient.like.findMany(query) + return foundLikes.map((like) => Like.fromDb(like)) + } +} From 63b049470a228f9df132ee1778d3cefb4f743489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20S=C3=B6nnergaard?= Date: Fri, 28 Feb 2025 09:43:22 +0100 Subject: [PATCH 09/11] Create like model and controller --- src/controllers/like.js | 14 ++++++++++++++ src/domain/like.js | 6 +----- 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 src/controllers/like.js diff --git a/src/controllers/like.js b/src/controllers/like.js new file mode 100644 index 00000000..320a2eb2 --- /dev/null +++ b/src/controllers/like.js @@ -0,0 +1,14 @@ +import { sendDataResponse } from '../utils/responses' + +export const create = async (req, res) => { + const { postId, userId } = req.body + + try { + const post = await Post.findById + if (!(postId && userId)) + return sendDataResponse(res, 400, { content: 'Must provide post ID' }) + + } catch (error) {} + } + +} \ No newline at end of file diff --git a/src/domain/like.js b/src/domain/like.js index a60ba5e0..f0705b95 100644 --- a/src/domain/like.js +++ b/src/domain/like.js @@ -83,8 +83,7 @@ export default class Like { static async _findMany(key, value) { const query = { include: { - post: true, - user: true + post: true } } @@ -92,9 +91,6 @@ export default class Like { query.where = { post: { [key]: value - }, - user: { - [key]: value } } } From 4e7ab0c31b52729d8f0b16c30b8a7b56f35884fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20S=C3=B6nnergaard?= Date: Fri, 28 Feb 2025 09:44:16 +0100 Subject: [PATCH 10/11] Create temporary empty comment model --- src/domain/comment.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/domain/comment.js diff --git a/src/domain/comment.js b/src/domain/comment.js new file mode 100644 index 00000000..994d475b --- /dev/null +++ b/src/domain/comment.js @@ -0,0 +1 @@ +export default class Comment {} From a3f334a503c15db57fd7fa3b62f297398dd6996e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20S=C3=B6nnergaard?= Date: Fri, 28 Feb 2025 09:44:30 +0100 Subject: [PATCH 11/11] Create post model and controller --- src/controllers/post.js | 47 ++++++++++++++++--------- src/domain/post.js | 76 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 src/domain/post.js diff --git a/src/controllers/post.js b/src/controllers/post.js index 7b168039..31dacf84 100644 --- a/src/controllers/post.js +++ b/src/controllers/post.js @@ -1,28 +1,41 @@ +import Post from '../domain/post.js' +import User from '../domain/user.js' import { sendDataResponse } from '../utils/responses.js' export const create = async (req, res) => { - const { content } = req.body + const timestamp = new Date() + const postToCreate = await Post.fromJson({ + ...req.body, + createdAt: timestamp, + updatedAt: timestamp + }) + + if (!User.findById(postToCreate.userId)) + return sendDataResponse(res, 400, { content: 'No user with ID' }) + + // if (!postToCreate.content) { + // return sendDataResponse(res, 400, { content: 'Must provide content' }) + // } - if (!content) { - return sendDataResponse(res, 400, { content: 'Must provide content' }) - } + const createdPost = await postToCreate.save() - return sendDataResponse(res, 201, { post: { id: 1, content } }) + return sendDataResponse(res, 201, { post: createdPost }) +} + +export const getAllByUserId = async (req, res) => { + const foundPosts = (await Post.findByUserId(req.body)).map((post) => { + return { ...post.toJson().post } + }) + return sendDataResponse(res, 200, { + posts: foundPosts + }) } export const getAll = async (req, res) => { + const foundPosts = (await Post.findAll()).map((post) => { + return { ...post.toJson().post } + }) return sendDataResponse(res, 200, { - posts: [ - { - id: 1, - content: 'Hello world!', - author: { ...req.user } - }, - { - id: 2, - content: 'Hello from the void!', - author: { ...req.user } - } - ] + posts: foundPosts }) } diff --git a/src/domain/post.js b/src/domain/post.js new file mode 100644 index 00000000..4dc2ae0a --- /dev/null +++ b/src/domain/post.js @@ -0,0 +1,76 @@ +import dbClient from '../utils/dbClient.js' + +export default class Post { + static fromDb(post) { + return new Post( + post.id, + post.userId, + post.content, + post.createdAt, + post.updatedAt + ) + } + + static async fromJson(json) { + const { userId, content, createdAt, updatedAt } = json + return new Post(null, userId, content, createdAt, updatedAt) + } + + constructor(id, userId, content, createdAt, updatedAt) { + this.id = id + this.userId = userId + this.content = content + this.createdAt = createdAt + this.updatedAt = updatedAt + } + + toJson() { + return { + post: { + id: this.id, + userId: this.userId, + content: this.content, + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + } + } + + async save() { + const data = { + content: this.content, + userId: this.userId, + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + + const createdPost = await dbClient.post.create({ + data, + include: { + user: true + } + }) + return Post.fromDb(createdPost) + } + + static async findAll() { + return Post._findMany() + } + + static async findByUserId(userId) { + return Post._findMany('userId', userId) + } + + static async _findMany(key, value) { + const query = {} + + if (key && value) { + query.where = { + [key]: value + } + } + + const foundPosts = await dbClient.post.findMany(query) + return foundPosts.map((p) => Post.fromDb(p)) + } +}