diff --git a/backend/src/controller/auth.controller.ts b/backend/src/controller/auth.controller.ts index 4a385aa..8a315db 100644 --- a/backend/src/controller/auth.controller.ts +++ b/backend/src/controller/auth.controller.ts @@ -5,10 +5,11 @@ import bcrypt from "bcrypt"; import generateToken, { type jwtPayload } from "../utils/generateToken.js"; import redisClient from "../config/redis.js"; import jwt from "jsonwebtoken"; -import blackListToken from "../utils/blacklistToken.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import invalidateTokens from "../utils/invalidateTokens.js"; //Authentication (Auth Controller) -export const registerUser = async (req: Request, res: Response) => { +export const registerUser = asyncHandler(async (req: Request, res: Response) => { const parsedData = registerSchema.safeParse(req.body); if(!parsedData.success) { return res.status(400).json({ @@ -16,54 +17,46 @@ export const registerUser = async (req: Request, res: Response) => { }) } - try { - const { username, email, name, password } = parsedData.data; - - const findUser = await prismaClient.user.findFirst({ - where: { - OR: [ { username }, { email } ] - } - }) - - if(findUser) { - return res.status(400).json({ - messasge: "User already exists", - }) + const { username, email, name, password } = parsedData.data; + + const findUser = await prismaClient.user.findFirst({ + where: { + OR: [ { username }, { email } ] } - - const salt = await bcrypt.genSalt(5); - const hash = await bcrypt.hash(password, salt); - - const user = await prismaClient.user.create({ - data: { - username, - email, - name, - password: hash - } + }) + + if(findUser) { + return res.status(400).json({ + messasge: "User already exists", }) + } - generateToken(user.id, res); //token seted + const salt = await bcrypt.genSalt(5); + const hash = await bcrypt.hash(password, salt); - res.status(201).json({ - message: "User created successfully", - data: { - username: user.username, - email: user.email, - name: user.name, - verified: false, - } - }) + const user = await prismaClient.user.create({ + data: { + username, + email, + name, + password: hash + } + }) - } catch (error) { - console.log("Error in user regiser: ", error); - res.status(500).json({ - message: "Error in signing" - }) - } -} + generateToken(user.id, res); -export const loginUser = async (req: Request, res: Response) => { + res.status(201).json({ + message: "User created successfully", + data: { + username: user.username, + email: user.email, + name: user.name, + verified: false, + } + }) +}); + +export const loginUser = asyncHandler(async (req: Request, res: Response) => { const parsedData = loginSchema.safeParse(req.body); if(!parsedData.success) { return res.status(400).json({ @@ -71,50 +64,42 @@ export const loginUser = async (req: Request, res: Response) => { }) } - try { - const { username, password } = parsedData.data; + const { username, password } = parsedData.data; - const user = await prismaClient.user.findUnique({ - where: { - username - } - }) - if(!user) { - return res.status(404).json({ - message: "User doesn't exist" - }) + const user = await prismaClient.user.findUnique({ + where: { + username } - - const isValid = await bcrypt.compare(password, user.password); - - if(!isValid) { - return res.status(401).json({ - message: "wrong password" - }) - } - - generateToken(user.id, res); - - return res.status(200).json({ - message: "successfully loged in", - user: { - id: user.id, - username: user.username, - email: user.email, - name: user.name, - } + }) + if(!user) { + return res.status(404).json({ + message: "User doesn't exist" }) + } - } catch (error) { - console.log("Erorr in login endpoint: ", error); - res.status(500).json({ - message: "Error in login" + const isValid = await bcrypt.compare(password, user.password); + + if(!isValid) { + return res.status(401).json({ + message: "wrong password" }) } -} + + generateToken(user.id, res); + + return res.status(200).json({ + message: "successfully loged in", + user: { + id: user.id, + username: user.username, + email: user.email, + name: user.name, + } + }) +}); -export const refreshAccessToken = async (req: Request, res: Response) => { +export const refreshAccessToken = asyncHandler(async (req: Request, res: Response) => { const accessToken = req.cookies.accessToken; const refreshToken = req.cookies.refreshToken; @@ -124,39 +109,28 @@ export const refreshAccessToken = async (req: Request, res: Response) => { }) } - try { - const decodeRefreshToken = await jwt.verify(refreshToken, process.env.JWT_SECRET!) as jwtPayload; - const decodeAccessToken = await jwt.verify(accessToken, process.env.JWT_SECRET!) as jwtPayload; - - const isRefreshTokenBlacklisted = await redisClient.get(decodeRefreshToken.jti); //jti is unique - const isAccessTokenBlacklisted = await redisClient.get(decodeAccessToken.jti); - - if(isRefreshTokenBlacklisted || isAccessTokenBlacklisted) { - return res.status(400).json({ - message: "Token is blacklisted, can't refresh the token" - }) - } - - blackListToken(decodeRefreshToken.jti, 604800); - blackListToken(decodeAccessToken.jti, 900); + const decodeRefreshToken = await jwt.verify(refreshToken, process.env.JWT_SECRET!) as jwtPayload; + const decodeAccessToken = await jwt.verify(accessToken, process.env.JWT_SECRET!) as jwtPayload; - generateToken(decodeRefreshToken.id, res); + const isRefreshTokenBlacklisted = await redisClient.get(decodeRefreshToken.jti); + const isAccessTokenBlacklisted = await redisClient.get(decodeAccessToken.jti); - res.status(200).json({ - message: "Token refreshed successfully" - }) - - } catch (error) { - console.log("Error in token refreshing: ", error); - res.status(500).json({ - message: "Error in token refreshing" + if(isRefreshTokenBlacklisted || isAccessTokenBlacklisted) { + return res.status(400).json({ + message: "Token is blacklisted, can't refresh the token" }) } -} + invalidateTokens(accessToken, refreshToken, res); + generateToken(decodeRefreshToken.id, res); + res.status(200).json({ + message: "Token refreshed successfully" + }) +}); -export const logoutUser = (req: Request, res: Response) => { + +export const logoutUser = asyncHandler(async (req: Request, res: Response) => { const refreshToken = req.cookies.refreshToken; const accessToken = req.cookies.accessToken; @@ -166,26 +140,9 @@ export const logoutUser = (req: Request, res: Response) => { }) } - try { - const decodeRefreshToken = jwt.verify(refreshToken, process.env.JWT_SECRET!) as jwtPayload; - const decodeAccessToken = jwt.verify(accessToken, process.env.JWT_SECRET!) as jwtPayload; - - //blacklist both tokens - blackListToken(decodeRefreshToken.jti, 604800); - blackListToken(decodeAccessToken.jti, 900); - - res.clearCookie("refreshToken"); - res.clearCookie("accessToken"); + invalidateTokens(accessToken, refreshToken, res); - res.status(200).json({ - message: "Successully loged out" - }) - - } catch (error) { - console.log("Error in logout funtion: ", error); - - res.status(500).json({ - message: "Error in server", - }) - } -} \ No newline at end of file + res.status(200).json({ + message: "Successully loged out" + }) +}); diff --git a/backend/src/controller/comment.controller.ts b/backend/src/controller/comment.controller.ts index dd7589d..34153c3 100644 --- a/backend/src/controller/comment.controller.ts +++ b/backend/src/controller/comment.controller.ts @@ -2,9 +2,11 @@ import type { Request, Response } from "express"; import { addCommentParams, addCommentSchema, deleteCommentParams } from "../validators/commentSchema.js"; import prismaClient from "../config/db.js"; import { VISIBILITY } from "../generated/prisma/enums.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import { commentDetailSelect } from "../utils/prismaSelects.js"; -export const addComment = async (req: Request, res: Response) => { +export const addComment = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id; if(!userId) { return res.status(400).json({ @@ -26,60 +28,43 @@ export const addComment = async (req: Request, res: Response) => { }) } - - try { - const { repo, owner, issueId } = parsedParams.data; + const { repo, owner, issueId } = parsedParams.data; - //first find the repo - const targetRepo = await prismaClient.repository.findFirst({ - where: { - name: repo, - owner: { - username: owner - } - }, - select: { - id: true + const targetRepo = await prismaClient.repository.findFirst({ + where: { + name: repo, + owner: { + username: owner } - }) - - if(!targetRepo) { - return res.status(400).json({ - message: "Can't find the repo" - }) + }, + select: { + id: true } - - const addComment = await prismaClient.comments.create({ - data: { - comment: parseData.data.comment, - authorId: userId, - issueId: issueId, - repoId: targetRepo.id - }, - select: { - id: true, - author: { - select: { username: true } - }, - comment: true, - createdAt: true, - } - }) - - return res.status(201).json({ - message: "Comment added successfully", - addComment - }) - - } catch (error) { - console.log("Error in addComment: ", error); - res.status(500).json({ - message: "Error in server" + }) + + if(!targetRepo) { + return res.status(400).json({ + message: "Can't find the repo" }) } -} -export const getComments = async (req: Request, res: Response) => { + const newComment = await prismaClient.comments.create({ + data: { + comment: parseData.data.comment, + authorId: userId, + issueId: issueId, + repoId: targetRepo.id + }, + select: commentDetailSelect + }) + + return res.status(201).json({ + message: "Comment added successfully", + addComment: newComment + }) +}); + +export const getComments = asyncHandler(async (req: Request, res: Response) => { const parsedParams = addCommentParams.safeParse(req.params); if(!parsedParams.success) { return res.status(400).json({ @@ -88,54 +73,38 @@ export const getComments = async (req: Request, res: Response) => { } const userId = req.user?.id; + const { repo, owner, issueId } = parsedParams.data; - try { - const { repo, owner, issueId } = parsedParams.data; - - const allComments = await prismaClient.comments.findMany({ - where: { - issueId: issueId, - repo: { - name: repo, - owner: { - username: owner - }, - OR: [ - { visibility: VISIBILITY.public }, - { ownerId: (userId as string) } - ] - } - }, - select: { - id: true, - author: { - select: { username: true } + const allComments = await prismaClient.comments.findMany({ + where: { + issueId: issueId, + repo: { + name: repo, + owner: { + username: owner }, - comment: true, - createdAt: true, + OR: [ + { visibility: VISIBILITY.public }, + { ownerId: (userId as string) } + ] } - }) - - if(allComments.length === 0) { - return res.status(200).json({ - message: "No comments on this issue yet" - }) - } + }, + select: commentDetailSelect + }) + if(allComments.length === 0) { return res.status(200).json({ - message: "Comment fetched successfully", - allComments - }) - - } catch (error) { - console.log("Error in getComments: ", error); - res.status(500).json({ - message: "Error in server" + message: "No comments on this issue yet" }) } -} -export const deleteComment = async (req: Request, res: Response) => { + return res.status(200).json({ + message: "Comment fetched successfully", + allComments + }) +}); + +export const deleteComment = asyncHandler(async (req: Request, res: Response) => { const parsedParams = deleteCommentParams.safeParse(req.params); if(!parsedParams.success) { return res.status(400).json({ @@ -150,9 +119,9 @@ export const deleteComment = async (req: Request, res: Response) => { }) } - try { - const { owner, repo, issueId, commentId } = parsedParams.data; + const { owner, repo, issueId, commentId } = parsedParams.data; + try { await prismaClient.comments.delete({ where: { id: commentId, @@ -163,7 +132,7 @@ export const deleteComment = async (req: Request, res: Response) => { } }, issueId: issueId, - OR: [ //user should be the author of comment or can be owner of the repo + OR: [ { authorId: userId }, { repo: { @@ -175,19 +144,12 @@ export const deleteComment = async (req: Request, res: Response) => { }) res.status(200).json({ - messasge: "Comment removed successfully" + message: "Comment removed successfully" }) - } catch (error: any) { - if(error.code === 'P2025') { - return res.status(400).json({ - message: "Can't find the comment to delete" - }) + if (error.code === 'P2025') { + return res.status(404).json({ message: "Comment not found or unauthorized" }) } - - console.log("Error in deleteComment: ", error); - res.status(500).json({ - message: "Error in server" - }) + throw error; } -} \ No newline at end of file +}); diff --git a/backend/src/controller/forkRouter.controller.ts b/backend/src/controller/forkRouter.controller.ts index fd7bc22..8edf00a 100644 --- a/backend/src/controller/forkRouter.controller.ts +++ b/backend/src/controller/forkRouter.controller.ts @@ -3,9 +3,11 @@ import { forkParamsSchema } from "../validators/forkSchema.js"; import prismaClient from "../config/db.js"; import { VISIBILITY } from "../generated/prisma/enums.js"; import copyRepoFilesInS3 from "../utils/copyRepoFiles.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import { findRepoWithAccess } from "../utils/repoHelpers.js"; -export const forkRepository = async (req: Request, res: Response) => { +export const forkRepository = asyncHandler(async (req: Request, res: Response) => { const parsedParams = forkParamsSchema.safeParse(req.params); if(!parsedParams.success) { return res.status(400).json({ @@ -20,123 +22,101 @@ export const forkRepository = async (req: Request, res: Response) => { }) } - try { - const ownerName = parsedParams.data.owner; - const repoName = parsedParams.data.repo; - - const targetRepo = await prismaClient.repository.findFirst({ - where: { - name: repoName, - owner: { - username: ownerName - }, - OR: [ - { visibility: VISIBILITY.public }, - { ownerId: (userId as string) } - ] - } - }) + const ownerName = parsedParams.data.owner; + const repoName = parsedParams.data.repo; - if(!targetRepo) { - return res.status(400).json({ - message: "Repository not found" - }) - } - - //can't fork own repo - if(targetRepo.ownerId == userId) { - return res.status(400).json({ - message: "Can't fork your own repo" - }) - } + const targetRepo = await findRepoWithAccess(ownerName, repoName, userId); - //already forked - const alreadyForked = await prismaClient.fork.findUnique({ - where: { - sourceCodeRepoId_forkedById: { - sourceCodeRepoId: targetRepo.id, - forkedById: (userId as string) - } - } + if(!targetRepo) { + return res.status(400).json({ + message: "Repository not found" }) + } + + if(targetRepo.ownerId == userId) { + return res.status(400).json({ + message: "Can't fork your own repo" + }) + } - if(alreadyForked) { - return res.status(400).json({ - message: "You already forked this repo" - }) + const alreadyForked = await prismaClient.fork.findUnique({ + where: { + sourceCodeRepoId_forkedById: { + sourceCodeRepoId: targetRepo.id, + forkedById: (userId as string) + } } + }) - //**create new repository - - //if name conflicts,then add '-fork' in the last of the new forked repo name - const nameConflict = await prismaClient.repository.findUnique({ - where: { - name_ownerId: { - name: targetRepo.name, - ownerId: userId - } - } + if(alreadyForked) { + return res.status(400).json({ + message: "You already forked this repo" }) + } - const forkedRepoName = nameConflict ? `${targetRepo.name}-fork` : targetRepo.name; - - const newRepo = await prismaClient.repository.create({ - data: { - name: forkedRepoName, - description: targetRepo.description, - visibility: VISIBILITY.public, + const nameConflict = await prismaClient.repository.findUnique({ + where: { + name_ownerId: { + name: targetRepo.name, ownerId: userId } - }) + } + }) - const fork = await prismaClient.fork.create({ - data: { - sourceCodeRepoId: targetRepo.id, - forkedById: userId! - }, - select: { - id: true, - sourceCode: { - select: { - name: true, - owner: { - select: { username: true } - } - } - }, - forkedBy: { - select: { - username: true + const forkedRepoName = nameConflict ? `${targetRepo.name}-fork` : targetRepo.name; + + const newRepo = await prismaClient.repository.create({ + data: { + name: forkedRepoName, + description: targetRepo.description, + visibility: VISIBILITY.public, + ownerId: userId + } + }) + + const fork = await prismaClient.fork.create({ + data: { + sourceCodeRepoId: targetRepo.id, + forkedById: userId! + }, + select: { + id: true, + sourceCode: { + select: { + name: true, + owner: { + select: { username: true } } - }, - createdAt: true - } - }) + } + }, + forkedBy: { + select: { + username: true + } + }, + createdAt: true + } + }) - //copy s3 files * * * - const forkRepoId = await copyRepoFilesInS3(targetRepo.id, (userId as string)); + const forkRepoId = await copyRepoFilesInS3(targetRepo.id, (userId as string)); + try { res.status(201).json({ message: "Repository forked successfully", fork, forkRepoId, }) - } catch (error: any) { if(error.code == 'P2002') { return res.status(400).json({ message: "Already forked" }) } - - console.log("Error in forkRepository: ", error); - res.status(500).json({ - message: "Something went wrong in server" - }) + throw error; } -} +}); -export const getForkedRepositories = async (req: Request, res: Response) => { +export const getForkedRepositories = asyncHandler(async (req: Request, res: Response) => { const parsedParams = forkParamsSchema.safeParse(req.params); if(!parsedParams.success) { return res.status(400).json({ @@ -144,58 +124,50 @@ export const getForkedRepositories = async (req: Request, res: Response) => { }) } - try { - const ownerName = parsedParams.data.owner; - const repoName = parsedParams.data.repo; - - const repo = await prismaClient.repository.findFirst({ - where: { - name: repoName, - owner: { - username: ownerName, - }, - visibility: VISIBILITY.public - }, - select: { - id: true - } - }) - - if(!repo) { - return res.status(400).json({ - message: "Repository not found" - }) - } + const ownerName = parsedParams.data.owner; + const repoName = parsedParams.data.repo; - const forks = await prismaClient.fork.findMany({ - where: { - sourceCodeRepoId: repo.id - }, - select: { - id: true, - forkedBy: { - select: { - username: true, - name: true - } - }, - createdAt: true + const repo = await prismaClient.repository.findFirst({ + where: { + name: repoName, + owner: { + username: ownerName, }, - orderBy: { - createdAt: "desc" - } - }) - - return res.status(200).json({ - message: "Forks fetched successfully", - totalLength: forks.length, - forks - }) - - } catch (error) { - console.log("Error in getForkedRepositories: ", error); - res.status(500).json({ - message: "Something went wrong in Server" + visibility: VISIBILITY.public + }, + select: { + id: true + } + }) + + if(!repo) { + return res.status(400).json({ + message: "Repository not found" }) } -} \ No newline at end of file + + const forks = await prismaClient.fork.findMany({ + where: { + sourceCodeRepoId: repo.id + }, + select: { + id: true, + forkedBy: { + select: { + username: true, + name: true + } + }, + createdAt: true + }, + orderBy: { + createdAt: "desc" + } + }) + + return res.status(200).json({ + message: "Forks fetched successfully", + totalLength: forks.length, + forks + }) +}); diff --git a/backend/src/controller/issue.controller.ts b/backend/src/controller/issue.controller.ts index 6924317..0fb960c 100644 --- a/backend/src/controller/issue.controller.ts +++ b/backend/src/controller/issue.controller.ts @@ -2,10 +2,14 @@ import type { Request, Response } from "express"; import { createIssueSchema, issueByIdSchema, issueParams, issueUpdateSchema, OnlyIssueParams } from "../validators/issueSchema.js"; import prismaClient from "../config/db.js"; import { VISIBILITY } from "../generated/prisma/enums.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import { findRepoWithAccess } from "../utils/repoHelpers.js"; +import { issueDetailSelect } from "../utils/prismaSelects.js"; +import buildUpdateData from "../utils/buildUpdateData.js"; //user's issue's -export const createIssue = async (req: Request, res: Response) => { +export const createIssue = asyncHandler(async (req: Request, res: Response) => { const parsedParams = issueParams.safeParse(req.params); if(!parsedParams.success) { return res.status(400).json({ @@ -23,63 +27,34 @@ export const createIssue = async (req: Request, res: Response) => { const userId = req.user?.id; if(!userId) return; - try { - const ownerName = parsedParams.data.owner; - const repo = parsedParams.data.repo; - const { title, description } = parsedData.data; - - //find the repo - const targetRepo = await prismaClient.repository.findFirst({ - where: { - name: repo, - owner: { - username: ownerName - }, - OR: [ - { visibility: VISIBILITY.public }, - { ownerId: userId } //owner can create issue on private repo's - ] - } - }) - if(!targetRepo) { - return res.status(404).json({ - message: "Repository not found" - }) - } - - const newIssue = await prismaClient.issue.create({ - data: { - title, - description, - repoId: targetRepo.id, - authorId: userId - //statue is default 'open' - }, - select: { - id: true, - title: true, - description: true, - status: true, - repository: { select: { name: true } }, - author: { select: { username: true } }, - createdAt: true - } - }) + const ownerName = parsedParams.data.owner; + const repo = parsedParams.data.repo; + const { title, description } = parsedData.data; - res.status(201).json({ - message: "Issue created successfully", - newIssue - }) - - } catch (error) { - console.log("Error in createIssue: ", error); - res.status(500).json({ - message: "Error in server" + const targetRepo = await findRepoWithAccess(ownerName, repo, userId); + if(!targetRepo) { + return res.status(404).json({ + message: "Repository not found" }) } -} -export const getAllIssuesByRepo = async (req: Request, res: Response) => { + const newIssue = await prismaClient.issue.create({ + data: { + title, + description, + repoId: targetRepo.id, + authorId: userId + }, + select: issueDetailSelect + }) + + res.status(201).json({ + message: "Issue created successfully", + newIssue + }) +}); + +export const getAllIssuesByRepo = asyncHandler(async (req: Request, res: Response) => { const parsedParams = issueParams.safeParse(req.params); if(!parsedParams.success) { return res.status(400).json({ @@ -88,63 +63,53 @@ export const getAllIssuesByRepo = async (req: Request, res: Response) => { } const userId = req.user?.id; - - try { - const owner = parsedParams.data.owner; - const repo = parsedParams.data.repo; - - const allIssues = await prismaClient.issue.findMany({ - where: { - repository: { - name: repo, - owner: { - username: owner - }, - OR: [ - { visibility: VISIBILITY.public }, - //if user is loged in and repo is private then also show issues - { ownerId: (userId as string) }, - ] + const owner = parsedParams.data.owner; + const repo = parsedParams.data.repo; + + const allIssues = await prismaClient.issue.findMany({ + where: { + repository: { + name: repo, + owner: { + username: owner }, + OR: [ + { visibility: VISIBILITY.public }, + { ownerId: (userId as string) }, + ] }, - select: { - id: true, - title: true, - description: true, - status: true, - author: { select: { username: true } }, - comments: { - select: { - comment: true, - author: { - select: { username: true } - } + }, + select: { + id: true, + title: true, + description: true, + status: true, + author: { select: { username: true } }, + comments: { + select: { + comment: true, + author: { + select: { username: true } } - }, - createdAt: true - } - }) - - if(allIssues.length === 0) { - return res.status(200).json({ - message: "No issues or Repository is private" - }) + } + }, + createdAt: true } + }) - res.status(200).json({ - message: "Issues fetched successfully", - allIssues - }) - - } catch (error) { - console.log("Error in getAllIssuesByRepo: ", error); - res.status(500).json({ - message: "Error in server" + if(allIssues.length === 0) { + return res.status(200).json({ + message: "No issues or Repository is private" }) } -} -export const getIssueById = async (req: Request, res: Response) => { + res.status(200).json({ + message: "Issues fetched successfully", + allIssues + }) +}); + +export const getIssueById = asyncHandler(async (req: Request, res: Response) => { const parsedParams = issueByIdSchema.safeParse(req.params); if(!parsedParams.success) { return res.status(400).json({ @@ -153,55 +118,38 @@ export const getIssueById = async (req: Request, res: Response) => { } const userId = req.user?.id; - - try { - const { owner, repo, issueId } = parsedParams.data; - - const issue = await prismaClient.issue.findFirst({ - where: { - id: issueId, - repository: { - name: repo, - owner: { - username: owner - }, - OR: [ - { visibility: VISIBILITY.public }, - { ownerId: (userId as string) }, - ] - } - }, - select: { - id: true, - title: true, - description: true, - status: true, - repository: { select: { name: true } }, - author: { select: { username: true } }, - createdAt: true + const { owner, repo, issueId } = parsedParams.data; + + const issue = await prismaClient.issue.findFirst({ + where: { + id: issueId, + repository: { + name: repo, + owner: { + username: owner + }, + OR: [ + { visibility: VISIBILITY.public }, + { ownerId: (userId as string) }, + ] } - }) - - if(!issue) { - return res.status(404).json({ - message: "Can't find the repo" - }) - } + }, + select: issueDetailSelect + }) - res.status(200).json({ - message: "Issue fetched successfully", - issue - }) - - } catch (error) { - console.log("Error in getIssueById: ", error); - res.status(500).json({ - message: "Error in server" + if(!issue) { + return res.status(404).json({ + message: "Can't find the repo" }) } -} -export const updateIssue = async (req: Request, res: Response) => { + res.status(200).json({ + message: "Issue fetched successfully", + issue + }) +}); + +export const updateIssue = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id; if(!userId) return; @@ -219,28 +167,14 @@ export const updateIssue = async (req: Request, res: Response) => { }) } - try { - const { issueId, owner, repo } = parsedParams.data; - let updateData: any = {}; - - const { title, description, status } = parsedData.data; - if(title) { - updateData.title = title; - } - if(description) { - updateData.description = description; - } - if(status) { - updateData.status = status; - } + const { issueId, owner, repo } = parsedParams.data; + const { title, description, status } = parsedData.data; - if(Object.keys(updateData).length === 0) { //empty body handling - return res.status(400).json({ - message: "Nothing to update" - }) - } + const updateData = buildUpdateData({ title, description, status }, res); + if (!updateData) return; - const updateIssue = await prismaClient.issue.update({ + try { + const updatedIssue = await prismaClient.issue.update({ where: { id: issueId, repository: { @@ -249,37 +183,28 @@ export const updateIssue = async (req: Request, res: Response) => { username: owner } }, - OR: [ //author and repo owner both can update the issue + OR: [ { authorId: userId }, { repository: { owner: { username: owner } } } ] }, data: updateData, - select: { - id: true, - title: true, - description: true, - status: true, - repository: { select: { name: true } }, - author: { select: { username: true } }, - createdAt: true - } + select: issueDetailSelect }) res.status(200).json({ message: "Issue updated successfully", - updateIssue + updateIssue: updatedIssue }) - } catch (error: any) { if(error.code === 'P2025') { return res.status(404).json({ message: "Issue not found or unauthorized" }) } - res.status(500).json({ message: "Error in server" }) + throw error; } -} +}); -export const deleteIssue = async (req: Request, res: Response) => { +export const deleteIssue = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id; if(!userId) return; @@ -290,11 +215,10 @@ export const deleteIssue = async (req: Request, res: Response) => { }) } - try { - const { issueId, owner, repo } = parsedParams.data; + const { issueId, owner, repo } = parsedParams.data; - //only repo owner can delete the issue - const deleteIssue = await prismaClient.issue.delete({ + try { + const deletedIssue = await prismaClient.issue.delete({ where: { id: issueId, repository: { @@ -306,7 +230,7 @@ export const deleteIssue = async (req: Request, res: Response) => { } } }) - if(!deleteIssue) { + if(!deletedIssue) { return res.status(400).json({ message: "Something went wrong" }) @@ -314,62 +238,55 @@ export const deleteIssue = async (req: Request, res: Response) => { res.status(200).json({ message: "Issue deleted successfully", - deleteIssue + deleteIssue: deletedIssue }) - } catch (error: any) { - console.log("Error in deleteIssue: ", error); - res.status(500).json({ message: "Error in server" }) + if (error.code === 'P2025') { + return res.status(404).json({ message: "Issue not found or unauthorized" }) + } + throw error; } -} +}); //own issues -export const getMyIssues = async (req: Request, res: Response) => { +export const getMyIssues = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id; if(!userId) return; - try { - const issuesAcrossAllRepos = await prismaClient.issue.findMany({ - where: { - authorId: (userId as string) - }, - select: { - id: true, - title: true, - status: true, - repository: { - select: { - name: true, - owner: { - select: { username: true } - } + const issuesAcrossAllRepos = await prismaClient.issue.findMany({ + where: { + authorId: (userId as string) + }, + select: { + id: true, + title: true, + status: true, + repository: { + select: { + name: true, + owner: { + select: { username: true } } - }, - createdAt: true - } - }) - - if(!issuesAcrossAllRepos) { - return res.status(404).json({ - message: "You haven't created any issue" - }) + } + }, + createdAt: true } - - res.status(200).json({ - message: "Issues find successfully", - issuesAcrossAllRepos - }) - - } catch (error) { - console.log("Error in getMyIssues: ", error); - res.status(400).json({ - message: "Error in server" + }) + + if(!issuesAcrossAllRepos) { + return res.status(404).json({ + message: "You haven't created any issue" }) } -} -export const updateMyIssuesById = async (req: Request, res: Response) => { + res.status(200).json({ + message: "Issues find successfully", + issuesAcrossAllRepos + }) +}); + +export const updateMyIssuesById = asyncHandler(async (req: Request, res: Response) => { const parseIssueParams = OnlyIssueParams.safeParse(req.params); if(!parseIssueParams.success) { return res.status(400).json({ @@ -387,54 +304,29 @@ export const updateMyIssuesById = async (req: Request, res: Response) => { }) } - try { - let updateData: any = {}; - - const { title, description, status } = parsedData.data; - if(title) { - updateData.title = title; - } - if(description) { - updateData.description = description; - } - if(status) { - updateData.status = status; - } - - if(Object.keys(updateData).length === 0) { - return res.status(400).json({ - message: "Nothing to update" - }) - } + const { title, description, status } = parsedData.data; + const updateData = buildUpdateData({ title, description, status }, res); + if (!updateData) return; - const updateIssue = await prismaClient.issue.update({ + try { + const updatedIssue = await prismaClient.issue.update({ where: { id: parseIssueParams.data.issueId, authorId: userId }, data: updateData, - select: { - id: true, - title: true, - description: true, - status: true, - repository: { select: { name: true } }, - author: { select: { username: true } }, - createdAt: true - } + select: issueDetailSelect }) res.status(200).json({ message: "Issue updated successfully", - updateIssue + updateIssue: updatedIssue }) - } catch (error: any) { if (error.code === 'P2025') { return res.status(404).json({ message: "Issue not found" }) } - console.log("Error in updateMyIssuesById: ", error); - res.status(500).json({ message: "Error in server" }) + throw error; } -} \ No newline at end of file +}); diff --git a/backend/src/controller/repo.controller.ts b/backend/src/controller/repo.controller.ts index dbb2d4b..4940d25 100644 --- a/backend/src/controller/repo.controller.ts +++ b/backend/src/controller/repo.controller.ts @@ -2,10 +2,14 @@ import { type Request, type Response } from "express"; import { createRepoSchema, repoByNameSchema, searchSchema, toggleVisibilitySchema, updateRepoSchema, usernameParamSchema } from "../validators/repoSchema.js"; import prismaClient from "../config/db.js"; import { VISIBILITY } from "../generated/prisma/enums.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import { repoListItemSelect, repoSummarySelect } from "../utils/prismaSelects.js"; +import { verifyRepoOwner } from "../utils/repoHelpers.js"; +import buildUpdateData from "../utils/buildUpdateData.js"; //Public Operations -export const searchRepositories = async (req: Request, res: Response) => { //search repo's +export const searchRepositories = asyncHandler(async (req: Request, res: Response) => { const parseParamsInput = searchSchema.safeParse(req.query); if(!parseParamsInput.success) { return res.status(400).json({ @@ -13,76 +17,47 @@ export const searchRepositories = async (req: Request, res: Response) => { //sea }) } - try { - const userInput = parseParamsInput.data.input; - - const result = await prismaClient.repository.findMany({ - where: { - visibility: VISIBILITY.public, //should be public - OR: [ - { name: { contains: userInput, mode: 'insensitive' } }, - { description: { contains: userInput, mode: 'insensitive' } }, - //{ - // issues: { - // some: { - // OR: [ - // { - // title: { - // contains: userInput, - // mode: 'insensitive' - // }, - // }, - // { - // description: { - // contains: userInput, - // mode: 'insensitive' - // } - // } - // ] - // }, - // } - //} - ] + const userInput = parseParamsInput.data.input; + + const result = await prismaClient.repository.findMany({ + where: { + visibility: VISIBILITY.public, + OR: [ + { name: { contains: userInput, mode: 'insensitive' } }, + { description: { contains: userInput, mode: 'insensitive' } }, + ] + }, + select: { + name: true, + description: true, + visibility: true, + owner: { + select: { + username: true + } }, - select: { - name: true, - description: true, - visibility: true, - owner: { - select: { - username: true - } - }, - _count: { - select: { - staredBy: true, - fork: true - } - }, - createdAt: true - } - }) - if(result.length === 0) { - return res.status(200).json({ - message: `There is nothing with name ${userInput}` - }) + _count: { + select: { + staredBy: true, + fork: true + } + }, + createdAt: true } - - res.status(200).json({ - message: "Repo's find successfully", - result - }) - - } catch (error) { - console.log("Error in searchRepositories: ", error); - res.status(500).json({ - message: "Error in repositories searching" + }) + if(result.length === 0) { + return res.status(200).json({ + message: `There is nothing with name ${userInput}` }) } -} + res.status(200).json({ + message: "Repo's find successfully", + result + }) +}); -export const getUserRepositories = async (req: Request, res: Response) => { //all repos of user +export const getUserRepositories = asyncHandler(async (req: Request, res: Response) => { const parsedResponse = usernameParamSchema.safeParse(req.params); if(!parsedResponse.success) { return res.status(400).json({ @@ -90,50 +65,42 @@ export const getUserRepositories = async (req: Request, res: Response) => { //al }) } - try { - const username = parsedResponse.data.username; - - const userAllRepos = await prismaClient.user.findUnique({ - where: { username: username }, - select: { - repositories: { - where: { visibility: VISIBILITY.public }, //if visibility is public then show only - select: { - name: true, - description: true, - visibility: true, - _count: { - select: { - staredBy: true, - fork: true, - issues: true - } - }, - updatedAt: true - } + const username = parsedResponse.data.username; + + const userAllRepos = await prismaClient.user.findUnique({ + where: { username: username }, + select: { + repositories: { + where: { visibility: VISIBILITY.public }, + select: { + name: true, + description: true, + visibility: true, + _count: { + select: { + staredBy: true, + fork: true, + issues: true + } + }, + updatedAt: true } } - }) - if(!userAllRepos) { - return res.status(404).json({ - message: "User not found" - }) } - - res.status(200).json({ - message: "Repository fetched successfully", - userAllRepos - }) - - } catch (error) { - console.log("Error in getUserRepository: ", error); - res.status(500).json({ - message: "Something wrong in server" + }) + if(!userAllRepos) { + return res.status(404).json({ + message: "User not found" }) } -} -export const getUserStarredRepos = async (req: Request, res: Response) => { //show only publilc stared repo + res.status(200).json({ + message: "Repository fetched successfully", + userAllRepos + }) +}); + +export const getUserStarredRepos = asyncHandler(async (req: Request, res: Response) => { const parsedParams = usernameParamSchema.safeParse(req.params); if(!parsedParams.success) { return res.status(400).json({ @@ -141,44 +108,25 @@ export const getUserStarredRepos = async (req: Request, res: Response) => { //sh }) } - try { - const username = parsedParams.data.username; - const userStaredRepos = await prismaClient.user.findUnique({ - where: { username: username }, - select: { - starRepos: { - where: { visibility: VISIBILITY.public }, - select: { - name: true, - description: true, - _count: { - select: { - staredBy: true, - fork: true, - issues: true - } - }, - updatedAt: true, - } - } + const username = parsedParams.data.username; + const userStaredRepos = await prismaClient.user.findUnique({ + where: { username: username }, + select: { + starRepos: { + where: { visibility: VISIBILITY.public }, + select: repoSummarySelect } - }) - - res.status(200).json({ - message: "Starred repos fetched successfully", - userStaredRepos - }) + } + }) - } catch (error) { - console.log("Error in getUserStarredRepos: ", error); - res.status(500).json({ - message: "Something wrong in server" - }) - } -} + res.status(200).json({ + message: "Starred repos fetched successfully", + userStaredRepos + }) +}); //return's single repo using repo name -export const getRepositoryByFullName = async (req: Request, res: Response) => { +export const getRepositoryByFullName = asyncHandler(async (req: Request, res: Response) => { const parseNames = repoByNameSchema.safeParse(req.params); if(!parseNames.success) { return res.status(400).json({ @@ -186,136 +134,75 @@ export const getRepositoryByFullName = async (req: Request, res: Response) => { }) } - try { - const owenrname = parseNames.data.owner; - const repoName = parseNames.data.repo; + const owenrname = parseNames.data.owner; + const repoName = parseNames.data.repo; - const userRepo = await prismaClient.user.findFirst({ - where: { - username: owenrname, - }, - select: { - repositories: { - where: { - name: repoName, - visibility: VISIBILITY.public //return only public repo's - }, - select: { - name: true, - description: true, - _count: { - select: { - staredBy: true, - fork: true, - issues: true - } - }, - createdAt: true, - updatedAt: true, - } - } + const userRepo = await prismaClient.user.findFirst({ + where: { + username: owenrname, + }, + select: { + repositories: { + where: { + name: repoName, + visibility: VISIBILITY.public + }, + select: repoSummarySelect } - }) - - if(userRepo?.repositories.length === 0) { - return res.status(200).json({ - message: "User doesn't have any repo with this name" - }) } + }) - res.status(200).json({ - message: "Repository searched successfully", - userRepo - }) - - } catch (error) { - console.log("Error in getRepositoryByFullName: ", error); - res.status(500).json({ - message: "Error in server" + if(userRepo?.repositories.length === 0) { + return res.status(200).json({ + message: "User doesn't have any repo with this name" }) } -} + + res.status(200).json({ + message: "Repository searched successfully", + userRepo + }) +}); //Authenticated Operations -export const getMyRepositories = async (req: Request, res: Response) => { +export const getMyRepositories = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id; - try { - const userReopos = await prismaClient.user.findUnique({ - where: { id: (userId as string) }, - select: { - repositories: { - select: { - name: true, - description: true, - visibility: true, - _count: { - select: { - staredBy: true, - fork: true, - issues: true - } - }, - createdAt: true, - updatedAt: true - } - } + const userReopos = await prismaClient.user.findUnique({ + where: { id: (userId as string) }, + select: { + repositories: { + select: repoListItemSelect } - }) - - res.status(200).json({ - message: "User's all repositories fetched successfully", - userReopos - }) - - } catch (error) { - console.log("Error in getMyRepositories: ", error); - res.status(500).json({ - message: "Error in server" - }) - } -} - -export const getMyStarredRepos = async (req: Request, res: Response) => { + } + }) + + res.status(200).json({ + message: "User's all repositories fetched successfully", + userReopos + }) +}); + +export const getMyStarredRepos = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id; - try { - const userStarredReopos = await prismaClient.user.findUnique({ - where: { id: (userId as string) }, - select: { - starRepos: { - select: { - name: true, - description: true, - _count: { - select: { - staredBy: true, - fork: true, - issues: true - } - }, - createdAt: true, - updatedAt: true - } - } + const userStarredReopos = await prismaClient.user.findUnique({ + where: { id: (userId as string) }, + select: { + starRepos: { + select: repoSummarySelect } - }) - - res.status(200).json({ - message: "User's all starred repositories fetched successfully", - userStarredReopos - }) - - } catch (error) { - console.log("Error in getMyStarredRepos: ", error); - res.status(500).json({ - message: "Error in server" - }) - } -} - -export const createRepository = async (req: Request, res: Response) => { + } + }) + + res.status(200).json({ + message: "User's all starred repositories fetched successfully", + userStarredReopos + }) +}); + +export const createRepository = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id; const parsedData = createRepoSchema.safeParse(req.body); if(!parsedData.success) { @@ -324,31 +211,23 @@ export const createRepository = async (req: Request, res: Response) => { }) } - try { - const { name, description, visibility } = parsedData.data; - const repo = await prismaClient.repository.create({ - data: { - name, - description, - visibility, - ownerId: (userId as string) - } - }) + const { name, description, visibility } = parsedData.data; + const repo = await prismaClient.repository.create({ + data: { + name, + description, + visibility, + ownerId: (userId as string) + } + }) - res.status(201).json({ - message: "Repository created successfully", - repo - }) - - } catch (error) { - console.log("Error in createRepo: ", error); - res.status(500).json({ - message: "Something wrong in server" - }) - } -} + res.status(201).json({ + message: "Repository created successfully", + repo + }) +}); -export const updateRepository =async (req: Request, res: Response) => { +export const updateRepository = asyncHandler(async (req: Request, res: Response) => { const parseNames = repoByNameSchema.safeParse(req.params); if(!parseNames.success) { return res.status(400).json({ @@ -363,68 +242,33 @@ export const updateRepository =async (req: Request, res: Response) => { }) } - try { - const owenrname = parseNames.data.owner; - const repoName = parseNames.data.repo; - - // create the update object - let { name, description } = parsedBodyData.data; - let updateData: any = {}; - - if(name) { - updateData.name = name; - } - if(description) { - updateData.description = description; - } - - //find the owner id - const owner = await prismaClient.user.findUnique({ - where: { username: owenrname } - }) + const owenrname = parseNames.data.owner; + const repoName = parseNames.data.repo; - if(!owner) { - return res.status(404).json({ - message: "Owner not found" - }) - } - if (owner.id !== req.user?.id) { - return res.status(403).json({ - message: "You are not the owner of this repo" - }) - } - if(Object.keys(updateData).length === 0) { - return res.status(400).json({ - message: "Nothing to update" - }) - } + const owner = await verifyRepoOwner(owenrname, req, res); + if (!owner) return; - const updateRepo = await prismaClient.repository.update({ - where: { - //repo name in user's account is unique, - //so find using it - name_ownerId: { - name: repoName, - ownerId: owner.id - } - }, - data: updateData - }) + const { name, description } = parsedBodyData.data; + const updateData = buildUpdateData({ name, description }, res); + if (!updateData) return; - res.status(200).json({ - message: "Repository updated successfully", - updateRepo - }) + const updateRepo = await prismaClient.repository.update({ + where: { + name_ownerId: { + name: repoName, + ownerId: owner.id + } + }, + data: updateData + }) - } catch (error) { - console.log("Error in updateRepository: ", error); - res.status(500).json({ - message: "Error in server" - }) - } -} + res.status(200).json({ + message: "Repository updated successfully", + updateRepo + }) +}); -export const deleteRepository = async (req: Request, res: Response) => { +export const deleteRepository = asyncHandler(async (req: Request, res: Response) => { const parseNames = repoByNameSchema.safeParse(req.params); if(!parseNames.success) { return res.status(400).json({ @@ -432,26 +276,13 @@ export const deleteRepository = async (req: Request, res: Response) => { }) } - try { - const owenrname = parseNames.data.owner; - const repoName = parseNames.data.repo; + const owenrname = parseNames.data.owner; + const repoName = parseNames.data.repo; - //find the owner id - const owner = await prismaClient.user.findUnique({ - where: { username: owenrname } - }) - - if(!owner) { - return res.status(404).json({ - message: "Owner not found" - }) - } - if (owner.id !== req.user?.id) { - return res.status(403).json({ - message: "You are not the owner of this repo" - }) - } + const owner = await verifyRepoOwner(owenrname, req, res); + if (!owner) return; + try { const deleteRepo = await prismaClient.repository.delete({ where: { name_ownerId: { @@ -465,17 +296,15 @@ export const deleteRepository = async (req: Request, res: Response) => { message: "Repository deleted successfully", deleteRepo }) - } catch (error: any) { - console.log("Error in deleteRepository: ", error); - if (error.code === 'P2025') { //Prisma's "record not found" error code + if (error.code === 'P2025') { return res.status(404).json({ message: "Repo not found" }) } throw error } -} +}); -export const toggleRepositoryVisibility = async (req: Request, res: Response) => { +export const toggleRepositoryVisibility = asyncHandler(async (req: Request, res: Response) => { const parseNames = repoByNameSchema.safeParse(req.params); if(!parseNames.success) { return res.status(400).json({ @@ -491,55 +320,34 @@ export const toggleRepositoryVisibility = async (req: Request, res: Response) => }) } - try { - const owenrname = parseNames.data.owner; - const repoName = parseNames.data.repo; - - //find the owner id - const owner = await prismaClient.user.findUnique({ - where: { username: owenrname } - }) + const owenrname = parseNames.data.owner; + const repoName = parseNames.data.repo; - if(!owner) { - return res.status(404).json({ - message: "Owner not found" - }) - } - if (owner.id !== req.user?.id) { - return res.status(403).json({ - message: "You are not the owner of this repo" - }) - } + const owner = await verifyRepoOwner(owenrname, req, res); + if (!owner) return; - const visibilityData = visibility.data.visibility; + const visibilityData = visibility.data.visibility; - await prismaClient.repository.update({ - where: { - name_ownerId: { - name: repoName, - ownerId: owner.id - } - }, - data: { - visibility: visibilityData - } - }) + await prismaClient.repository.update({ + where: { + name_ownerId: { + name: repoName, + ownerId: owner.id + } + }, + data: { + visibility: visibilityData + } + }) - res.status(200).json({ - message: `Repository is now ${visibilityData}` - }) - - } catch (error) { - console.log("Error in toggleRepositoryVisibility: ", error); - res.status(500).json({ - message: "Error in server" - }) - } -} + res.status(200).json({ + message: `Repository is now ${visibilityData}` + }) +}); -// start and unstart the repo -export const starRepository = async (req: Request, res: Response) => { +// star and unstar the repo +export const starRepository = asyncHandler(async (req: Request, res: Response) => { const parseNames = repoByNameSchema.safeParse(req.params); if(!parseNames.success) { return res.status(400).json({ @@ -552,66 +360,55 @@ export const starRepository = async (req: Request, res: Response) => { return res.status(400).json({ message: "Can't find user's id" }) } - //add this owner's repo in logged in user's starred array - try { - const owenrname = parseNames.data.owner; - const repoName = parseNames.data.repo; + const owenrname = parseNames.data.owner; + const repoName = parseNames.data.repo; - const targetRepo = await prismaClient.repository.findFirst({ - where: { - //match the repo and owner's name - name: repoName, - owner: { - username: owenrname, - } - }, - select: { - id: true, - ownerId: true, - staredBy: { - where: { id: userId }, //starred by the logged in user - select: { id: true } - } + const targetRepo = await prismaClient.repository.findFirst({ + where: { + name: repoName, + owner: { + username: owenrname, } - }) - - if(!targetRepo) { - return res.status(404).json({ - message: "Repo not found" - }) - } - - if(targetRepo.staredBy.length > 0) { - return res.status(400).json({ - message: "Already starred this repo" - }) - } - - await prismaClient.user.update({ //add - where: { id: userId }, - data: { - starRepos: { - connect: { - id: targetRepo.id - } - } + }, + select: { + id: true, + ownerId: true, + staredBy: { + where: { id: userId }, + select: { id: true } } - }) + } + }) - res.status(200).json({ - message: "Successfully added to starred" + if(!targetRepo) { + return res.status(404).json({ + message: "Repo not found" }) + } - } catch (error) { - console.log("Error in startRepository: ", error); - res.status(500).json({ - message: "Can't add this repo in starred list, Error in server" + if(targetRepo.staredBy.length > 0) { + return res.status(400).json({ + message: "Already starred this repo" }) } -} + await prismaClient.user.update({ + where: { id: userId }, + data: { + starRepos: { + connect: { + id: targetRepo.id + } + } + } + }) -export const unstarRepository = async (req: Request, res: Response) => { + res.status(200).json({ + message: "Successfully added to starred" + }) +}); + +export const unstarRepository = asyncHandler(async (req: Request, res: Response) => { const parseNames = repoByNameSchema.safeParse(req.params); if(!parseNames.success) { return res.status(400).json({ @@ -624,61 +421,52 @@ export const unstarRepository = async (req: Request, res: Response) => { return res.status(400).json({ message: "Can't find user's id" }) } - try { - const owenrname = parseNames.data.owner; - const repoName = parseNames.data.repo; + const owenrname = parseNames.data.owner; + const repoName = parseNames.data.repo; - const targetRepo = await prismaClient.repository.findFirst({ - where: { - name: repoName, - owner: { - username: owenrname - } - }, - select: { - id: true, - ownerId: true, - staredBy: { - where: { id: userId }, - select: { id: true } - } + const targetRepo = await prismaClient.repository.findFirst({ + where: { + name: repoName, + owner: { + username: owenrname } - }) - - if(!targetRepo) { - return res.status(400).json({ - message: "Repo not found" - }) - } - if(targetRepo.staredBy.length === 0) { - res.status(400).json({ - message: "You haven't starred this repo" - }) - } - - await prismaClient.user.update({ - where: { - id: userId - }, - data: { - starRepos: { - disconnect: { - id: targetRepo.id - } - } + }, + select: { + id: true, + ownerId: true, + staredBy: { + where: { id: userId }, + select: { id: true } } - }) - + } + }) - res.status(200).json({ - message: "successfully removed from starred list" + if(!targetRepo) { + return res.status(400).json({ + message: "Repo not found" }) - - } catch (error) { - console.log("Error in unstarRepository: ", error); - res.status(500).json({ - message: "Can't unstar the repo, Error in server" + } + if(targetRepo.staredBy.length === 0) { + res.status(400).json({ + message: "You haven't starred this repo" }) } -} \ No newline at end of file + await prismaClient.user.update({ + where: { + id: userId + }, + data: { + starRepos: { + disconnect: { + id: targetRepo.id + } + } + } + }) + + + res.status(200).json({ + message: "successfully removed from starred list" + }) +}); diff --git a/backend/src/controller/user.controller.ts b/backend/src/controller/user.controller.ts index 771aba1..d5c3e09 100644 --- a/backend/src/controller/user.controller.ts +++ b/backend/src/controller/user.controller.ts @@ -1,59 +1,52 @@ import type { Request, Response } from "express"; import prismaClient from "../config/db.js"; import bcrypt from "bcrypt"; -import jwt from "jsonwebtoken"; -import blackListToken from "../utils/blacklistToken.js"; -import type { jwtPayload } from "../utils/generateToken.js"; import { userUpdateSchema } from "../validators/userSchema.js"; import { getUserParamsSchema } from "../validators/userSchema.js"; import { getTargetQuerySchema } from "../validators/userSchema.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import { userSummarySelect } from "../utils/prismaSelects.js"; +import invalidateTokens from "../utils/invalidateTokens.js"; + //User Profile Management (User Controller) -export const getCurrentUser = async (req: Request, res: Response) => { - const userId = req.user?.id; //from cookies +export const getCurrentUser = asyncHandler(async (req: Request, res: Response) => { + const userId = req.user?.id; if (!userId) { return res.status(401).json({ message: "Unauthorised" }) } - try { - const user = await prismaClient.user.findUnique({ - where: { - id: userId - }, - select: { - id: true, - username: true, - email: true, - name: true, - createdAt: true, - _count: { //return counts instead of full data - select: { - followedBy: true, - following: true, - starRepos: true, - issues: true, - forks: true - } + const user = await prismaClient.user.findUnique({ + where: { + id: userId + }, + select: { + id: true, + username: true, + email: true, + name: true, + createdAt: true, + _count: { + select: { + followedBy: true, + following: true, + starRepos: true, + issues: true, + forks: true } } - }) - - res.status(200).json({ - message: "user fetched succefully", - user - }) + } + }) - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server is failed" - }) - } -} + res.status(200).json({ + message: "user fetched succefully", + user + }) +}); -export const updateUserProfile = async (req: Request, res: Response) => { +export const updateUserProfile = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id; if (!userId) return; @@ -64,193 +57,128 @@ export const updateUserProfile = async (req: Request, res: Response) => { }) } - try { - //can change username, email, password - const updateData: any = {}; - const { username, email, name, password } = parsedData.data; - - if (username) { - //check username availability - const user = await prismaClient.user.findUnique({ - where: { - username: username - } - }) - if (user) { - return res.status(404).json({ - message: `Username ${username} is not available. Please choose another` - }) - } + const updateData: Record = {}; + const { username, email, name, password } = parsedData.data; - updateData.username = username; - } - if(email) { - //cheak avilability - const user = await prismaClient.user.findUnique({ - where: {email: email} + if (username) { + const user = await prismaClient.user.findUnique({ + where: { username: username } + }) + if (user) { + return res.status(404).json({ + message: `Username ${username} is not available. Please choose another` }) - if(user) { - return res.status(404).json({ - message: `Email ${email} is already in use. Please choose another` - }) - } - - updateData.email = email; - } - if (password) { - const pass = password; - const salt = await bcrypt.genSalt(5); - const hash = await bcrypt.hash(pass, salt); - - updateData.password = hash; - } - if(name) { - updateData.name = name; } - - if(Object.keys(updateData).length === 0) { - return res.status(400).json({ - message: "Nothing to update" + updateData.username = username; + } + if(email) { + const user = await prismaClient.user.findUnique({ + where: {email: email} + }) + if(user) { + return res.status(404).json({ + message: `Email ${email} is already in use. Please choose another` }) } + updateData.email = email; + } + if (password) { + const salt = await bcrypt.genSalt(5); + updateData.password = await bcrypt.hash(password, salt); + } + if(name) { + updateData.name = name; + } - const updateUser = await prismaClient.user.update({ - where: { - id: userId, - }, - data: updateData, - select: { - id: true, - username: true, - email: true, - name: true, - createdAt: true, - updatedAt: true - } - }) - - res.status(200).json({ - message: "your profile is updated successfully", - updateUser + if(Object.keys(updateData).length === 0) { + return res.status(400).json({ + message: "Nothing to update" }) + } - } catch (err) { - console.log("Can't update the profile: ", err) + const updateUser = await prismaClient.user.update({ + where: { + id: userId, + }, + data: updateData, + select: { + id: true, + username: true, + email: true, + name: true, + createdAt: true, + updatedAt: true + } + }) - res.status(500).json({ - message: "Error in server", - }) - } -} + res.status(200).json({ + message: "your profile is updated successfully", + updateUser + }) +}); -export const deleteUserProfile = async (req: Request, res: Response) => { +export const deleteUserProfile = asyncHandler(async (req: Request, res: Response) => { const refreshToken = req.cookies.refreshToken; const accessToken = req.cookies.accessToken; - const userId = req.user?.id; //from cookies + const userId = req.user?.id; if (!userId) return; - try { - const user = await prismaClient.user.delete({ - where: { - id: userId - } - }) - - const decodeRefreshToken = jwt.verify(refreshToken, process.env.JWT_SECRET!) as jwtPayload; - const decodeAccessToken = jwt.verify(accessToken, process.env.JWT_SECRET!) as jwtPayload; - - //blacklist both tokens - blackListToken(decodeRefreshToken.jti, 604800); - blackListToken(decodeAccessToken.jti, 900); - - res.clearCookie("refreshToken"); - res.clearCookie("accessToken"); + await prismaClient.user.delete({ + where: { + id: userId + } + }) + invalidateTokens(accessToken, refreshToken, res); - res.status(200).json({ - message: "User deleted successfully" - }) + res.status(200).json({ + message: "User deleted successfully" + }) +}); - } catch (err) { - console.log("Error in delete controller: ", err); - res.status(500).json({ - message: "Error in server" - }) - } -} - -export const getMyFollowers = async (req: Request, res: Response) => { +export const getMyFollowers = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id; if (!userId) return; - try { - //following me - const user = await prismaClient.user.findUnique({ - where: { - id: userId - }, - select: { - followedBy: { - select: { - id: true, - username: true, - email: true, - name: true, - createdAt: true - } - } + const user = await prismaClient.user.findUnique({ + where: { + id: userId + }, + select: { + followedBy: { + select: userSummarySelect } - }) + } + }) - res.status(200).json({ - user - }) + res.status(200).json({ + user + }) +}); - } catch (error) { - console.log("Error in getMyFollowers: ", error); - res.status(500).json({ - message: "Error in server" - }) - } -} - -export const getMyFollowing = async (req: Request, res: Response) => { +export const getMyFollowing = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id; if (!userId) return; - try { - const user = await prismaClient.user.findUnique({ - where: { - id: userId - }, - select: { - following: { - select: { - id: true, - username: true, - email: true, - name: true, - createdAt: true - } - } + const user = await prismaClient.user.findUnique({ + where: { + id: userId + }, + select: { + following: { + select: userSummarySelect } - }) + } + }) - res.status(200).json({ - user - }) - - } catch (error) { - console.log("Error in getMyFollowing: ", error); - res.status(500).json({ - message: "Error in server" - }) - } -} + res.status(200).json({ + user + }) +}); //Public profile controller -export const getUserByUsername = async (req: Request, res: Response) => { +export const getUserByUsername = asyncHandler(async (req: Request, res: Response) => { const result = getUserParamsSchema.safeParse(req.params); if(!result.success) { return res.status(400).json({ @@ -260,74 +188,66 @@ export const getUserByUsername = async (req: Request, res: Response) => { const { username } = result.data; - try { - const user = await prismaClient.user.findUnique({ - where: { - username: username + const user = await prismaClient.user.findUnique({ + where: { + username: username + }, + select: { + username: true, + email: true, + name: true, + createdAt: true, + following: { + select: { + id: true, + username: true, + email: true + } }, - select: { - username: true, - email: true, - name: true, - createdAt: true, - following: { - select: { - id: true, - username: true, - email: true - } - }, - followedBy: { - select: { - id: true, - username: true, - email: true - } - }, - starRepos: { - select: { - id: true, - name: true, - } - }, - issues: { - select: { - id: true, - title: true, - status: true - } - }, - repositories: { - select: { - id: true, - name: true, - description: true, - visibility: true, - } - }, - forks: true, - } - }) - if(!user) { - return res.status(404).json({ - message: "User not found" - }) + followedBy: { + select: { + id: true, + username: true, + email: true + } + }, + starRepos: { + select: { + id: true, + name: true, + } + }, + issues: { + select: { + id: true, + title: true, + status: true + } + }, + repositories: { + select: { + id: true, + name: true, + description: true, + visibility: true, + } + }, + forks: true, } - - res.status(200).json({ - message: "User found succeesfully", - user - }) - - } catch (error) { - console.log("Error in finding uesr: ", error); - res.status(500).json({ - message: "Can't searrch the profile" + }) + if(!user) { + return res.status(404).json({ + message: "User not found" }) } -} -export const getUserFollowers = async (req: Request, res: Response) => { + res.status(200).json({ + message: "User found succeesfully", + user + }) +}); + +export const getUserFollowers = asyncHandler(async (req: Request, res: Response) => { const parsedParams = getUserParamsSchema.safeParse(req.params); if(!parsedParams.success) { return res.status(400).json({ @@ -335,44 +255,30 @@ export const getUserFollowers = async (req: Request, res: Response) => { }) } - try { - const username = parsedParams.data.username; + const username = parsedParams.data.username; - const userFollowers = await prismaClient.user.findUnique({ - where: { - username: username as string - }, - select: { - followedBy: { - select: { - id: true, - username: true, - name: true, - email: true, - createdAt: true - } - } + const userFollowers = await prismaClient.user.findUnique({ + where: { + username: username as string + }, + select: { + followedBy: { + select: userSummarySelect } - }) - if(!userFollowers) { - return res.status(404).json({ - message: "User not found" - }) } - - res.status(200).json({ - userFollowers - }) - - } catch (error) { - console.log("Error in getUserFollowers: ", error); - res.status(500).json({ - message: "Error in server" + }) + if(!userFollowers) { + return res.status(404).json({ + message: "User not found" }) } -} -export const getUserFollowing = async (req: Request, res: Response) => { + res.status(200).json({ + userFollowers + }) +}); + +export const getUserFollowing = asyncHandler(async (req: Request, res: Response) => { const params = getUserParamsSchema.safeParse(req.params); if(!params.success) { return res.status(400).json({ @@ -382,44 +288,30 @@ export const getUserFollowing = async (req: Request, res: Response) => { const username = params.data.username; - try { - const userFollowing = await prismaClient.user.findUnique({ - where: { - username: username as string - }, - select: { - following: { - select: { - id: true, - username: true, - email: true, - name: true, - createdAt: true - } - } + const userFollowing = await prismaClient.user.findUnique({ + where: { + username: username as string + }, + select: { + following: { + select: userSummarySelect } - }) - if(!userFollowing) { - return res.status(404).json({ - message: "User not found" - }) } - - res.status(200).json({ - userFollowing - }) - - } catch (error) { - console.log("Error in getUserFollowing: ", error); - res.status(500).json({ - message: "Error in server" + }) + if(!userFollowing) { + return res.status(404).json({ + message: "User not found" }) } -} + + res.status(200).json({ + userFollowing + }) +}); //Follow and Unfollow user's -export const followUser = async (req: Request, res: Response) => { +export const followUser = asyncHandler(async (req: Request, res: Response) => { const query = getTargetQuerySchema.safeParse(req.query); if(!query.success) { return res.status(400).json({ @@ -436,65 +328,56 @@ export const followUser = async (req: Request, res: Response) => { }) } - try { - //search and verify targetUsername - const targetUser = await prismaClient.user.findUnique({ - where: { username: targetUsername }, - select: { id: true } + const targetUser = await prismaClient.user.findUnique({ + where: { username: targetUsername }, + select: { id: true } + }) + if (!targetUser) { + return res.status(404).json({ + message: "Invalid user" }) - if (!targetUser) { - return res.status(404).json({ - message: "Invalid user" - }) - } - - const targetUserId = targetUser.id; - if(targetUserId === userId) { - return res.status(400).json({ - message: "You can't follow yourself" - }) - } + } - const alreadyFollowing = await prismaClient.user.findUnique({ - where: { id: userId }, - select: { - following: { - where: { id: targetUserId }, - select: { id: true } - } - } + const targetUserId = targetUser.id; + if(targetUserId === userId) { + return res.status(400).json({ + message: "You can't follow yourself" }) + } - if((alreadyFollowing?.following?.length ?? 0) > 0) { - return res.status(400).json({ - message: "Already following this user" - }) + const alreadyFollowing = await prismaClient.user.findUnique({ + where: { id: userId }, + select: { + following: { + where: { id: targetUserId }, + select: { id: true } + } } + }) - await prismaClient.user.update({ - where: { - id: userId, - }, - data: { - following: { - connect: { //connect: Adds a user to the list. - id: targetUserId - } - } - } + if((alreadyFollowing?.following?.length ?? 0) > 0) { + return res.status(400).json({ + message: "Already following this user" }) + } - return res.json({ message: "User followed successfully" }); + await prismaClient.user.update({ + where: { + id: userId, + }, + data: { + following: { + connect: { + id: targetUserId + } + } + } + }) - } catch (error) { - console.log("Error in followUser: ", error); - res.status(500).json({ - message: "Internal server error" - }) - } -} + return res.json({ message: "User followed successfully" }); +}); -export const unfollowUser = async (req: Request, res: Response) => { +export const unfollowUser = asyncHandler(async (req: Request, res: Response) => { const query = getTargetQuerySchema.safeParse(req.query); if(!query.success) { return res.status(400).json({ @@ -511,56 +394,47 @@ export const unfollowUser = async (req: Request, res: Response) => { }) } - try { - const targetUser = await prismaClient.user.findUnique({ - where: { username: targetUsername }, - select: { id: true }, + const targetUser = await prismaClient.user.findUnique({ + where: { username: targetUsername }, + select: { id: true }, + }) + if (!targetUser) { + return res.status(404).json({ + message: "Invalid user" }) - if (!targetUser) { - return res.status(404).json({ - message: "Invalid user" - }) - } + } - const targetUserId = targetUser.id; + const targetUserId = targetUser.id; - const isFollowing = await prismaClient.user.findFirst({ - where: { - id: userId, - following: { - some: { - id: targetUserId - } + const isFollowing = await prismaClient.user.findFirst({ + where: { + id: userId, + following: { + some: { + id: targetUserId } } - }) - - if(!isFollowing) { - return res.status(400).json({ - message: "You are not following this user" - }) } + }) + if(!isFollowing) { + return res.status(400).json({ + message: "You are not following this user" + }) + } - await prismaClient.user.update({ - where: { - id: userId, - }, - data: { - following: { - disconnect: { - id: targetUserId - } + await prismaClient.user.update({ + where: { + id: userId, + }, + data: { + following: { + disconnect: { + id: targetUserId } } - }) - - return res.json({ message: "User unfollowed successfully" }); + } + }) - } catch (error) { - console.log("Error in unfollowUser: ", error); - res.status(500).json({ - message: "Internal server error" - }) - } -} \ No newline at end of file + return res.json({ message: "User unfollowed successfully" }); +}); diff --git a/backend/src/middlewares/validate.ts b/backend/src/middlewares/validate.ts new file mode 100644 index 0000000..b95caf8 --- /dev/null +++ b/backend/src/middlewares/validate.ts @@ -0,0 +1,38 @@ +import type { NextFunction, Request, Response } from "express"; +import type { ZodType } from "zod"; + +interface ValidateOptions { + body?: ZodType; + params?: ZodType; + query?: ZodType; +} + +export default function validate(schemas: ValidateOptions) { + return (req: Request, res: Response, next: NextFunction) => { + if (schemas.params) { + const result = schemas.params.safeParse(req.params); + if (!result.success) { + return res.status(400).json({ message: "Invalid parameters" }); + } + Object.assign(req.params, result.data); + } + + if (schemas.query) { + const result = schemas.query.safeParse(req.query); + if (!result.success) { + return res.status(400).json({ message: "Invalid query parameters" }); + } + Object.assign(req.query, result.data); + } + + if (schemas.body) { + const result = schemas.body.safeParse(req.body); + if (!result.success) { + return res.status(400).json({ message: "Invalid input data" }); + } + req.body = result.data; + } + + next(); + }; +} diff --git a/backend/src/utils/asyncHandler.ts b/backend/src/utils/asyncHandler.ts new file mode 100644 index 0000000..9d299e8 --- /dev/null +++ b/backend/src/utils/asyncHandler.ts @@ -0,0 +1,12 @@ +import type { Request, Response } from "express"; + +type AsyncController = (req: Request, res: Response) => Promise; + +export default function asyncHandler(fn: AsyncController) { + return (req: Request, res: Response) => { + fn(req, res).catch((error) => { + console.error(`Error in ${fn.name || "handler"}: `, error); + res.status(500).json({ message: "Error in server" }); + }); + }; +} diff --git a/backend/src/utils/buildUpdateData.ts b/backend/src/utils/buildUpdateData.ts new file mode 100644 index 0000000..0246528 --- /dev/null +++ b/backend/src/utils/buildUpdateData.ts @@ -0,0 +1,20 @@ +import type { Response } from "express"; + +export default function buildUpdateData( + fields: Record, + res: Response, +): Record | null { + const updateData: Record = {}; + for (const [key, value] of Object.entries(fields)) { + if (value !== undefined && value !== null) { + updateData[key] = value; + } + } + + if (Object.keys(updateData).length === 0) { + res.status(400).json({ message: "Nothing to update" }); + return null; + } + + return updateData; +} diff --git a/backend/src/utils/invalidateTokens.ts b/backend/src/utils/invalidateTokens.ts new file mode 100644 index 0000000..bcf6d5a --- /dev/null +++ b/backend/src/utils/invalidateTokens.ts @@ -0,0 +1,19 @@ +import type { Response } from "express"; +import jwt from "jsonwebtoken"; +import type { jwtPayload } from "./generateToken.js"; +import blackListToken from "./blacklistToken.js"; + +export default function invalidateTokens( + accessToken: string, + refreshToken: string, + res: Response, +) { + const decodeRefreshToken = jwt.verify(refreshToken, process.env.JWT_SECRET!) as jwtPayload; + const decodeAccessToken = jwt.verify(accessToken, process.env.JWT_SECRET!) as jwtPayload; + + blackListToken(decodeRefreshToken.jti, 604800); + blackListToken(decodeAccessToken.jti, 900); + + res.clearCookie("refreshToken"); + res.clearCookie("accessToken"); +} diff --git a/backend/src/utils/prismaSelects.ts b/backend/src/utils/prismaSelects.ts new file mode 100644 index 0000000..e05a10f --- /dev/null +++ b/backend/src/utils/prismaSelects.ts @@ -0,0 +1,57 @@ +import type { Prisma } from "../generated/prisma/client.js"; + +export const repoSummarySelect = { + name: true, + description: true, + _count: { + select: { + staredBy: true, + fork: true, + issues: true, + }, + }, + createdAt: true, + updatedAt: true, +} as const satisfies Prisma.RepositorySelect; + +export const repoListItemSelect = { + name: true, + description: true, + visibility: true, + _count: { + select: { + staredBy: true, + fork: true, + issues: true, + }, + }, + createdAt: true, + updatedAt: true, +} as const satisfies Prisma.RepositorySelect; + +export const issueDetailSelect = { + id: true, + title: true, + description: true, + status: true, + repository: { select: { name: true } }, + author: { select: { username: true } }, + createdAt: true, +} as const satisfies Prisma.IssueSelect; + +export const userSummarySelect = { + id: true, + username: true, + email: true, + name: true, + createdAt: true, +} as const satisfies Prisma.UserSelect; + +export const commentDetailSelect = { + id: true, + author: { + select: { username: true }, + }, + comment: true, + createdAt: true, +} as const satisfies Prisma.CommentsSelect; diff --git a/backend/src/utils/repoHelpers.ts b/backend/src/utils/repoHelpers.ts new file mode 100644 index 0000000..2a1e31b --- /dev/null +++ b/backend/src/utils/repoHelpers.ts @@ -0,0 +1,41 @@ +import type { Request, Response } from "express"; +import prismaClient from "../config/db.js"; +import { VISIBILITY } from "../generated/prisma/enums.js"; + +export async function findRepoWithAccess( + ownerName: string, + repoName: string, + userId?: string, +) { + return prismaClient.repository.findFirst({ + where: { + name: repoName, + owner: { username: ownerName }, + OR: [ + { visibility: VISIBILITY.public }, + ...(userId ? [{ ownerId: userId }] : []), + ], + }, + }); +} + +export async function verifyRepoOwner( + ownerUsername: string, + req: Request, + res: Response, +): Promise<{ id: string } | null> { + const owner = await prismaClient.user.findUnique({ + where: { username: ownerUsername }, + }); + + if (!owner) { + res.status(404).json({ message: "Owner not found" }); + return null; + } + if (owner.id !== req.user?.id) { + res.status(403).json({ message: "You are not the owner of this repo" }); + return null; + } + + return owner; +} diff --git a/frontend/src/api/auth.api.ts b/frontend/src/api/auth.api.ts index 50c6751..3444065 100644 --- a/frontend/src/api/auth.api.ts +++ b/frontend/src/api/auth.api.ts @@ -1,4 +1,5 @@ import axiosInstanse from "../lib/axiosInstance"; +import { getCurrentUserApi } from "./user.api"; interface RegisterUser { @@ -29,7 +30,4 @@ export const refreshTokenApi = async () => { return response.data; } -export const getMe = async () => { - const response = await axiosInstanse.get("/user/me"); - return response.data.user; -} \ No newline at end of file +export const getMe = getCurrentUserApi; \ No newline at end of file diff --git a/frontend/src/hooks/useUser.ts b/frontend/src/hooks/useUser.ts index 5ce5685..827fcd3 100644 --- a/frontend/src/hooks/useUser.ts +++ b/frontend/src/hooks/useUser.ts @@ -7,40 +7,7 @@ import { getUserRepositoriesApi } from "../api/user.api"; import { useAuth } from "./useAuth"; - - -export interface UserProfile { - id?: string; - username: string; - email: string; - name: string; - createdAt: string; - followers?: Array<{ - id: string; - username: string; - name: string; - email: string; - createdAt: string; - }>; - following?: Array<{ - id: string; - username: string; - email: string; - name: string; - createdAt: string; - }>; - starRepos?: Array<{ id: string; name: string }>; - issues?: Array<{ id: string; title: string; status: string }>; - repositories?: Array<{ id: string; name: string; description: string; visibility: string }>; - forks?: number; - _count?: { - followedBy: number; - following: number; - starRepos: number; - issues: number; - forks: number; - }; -} +import type { UserProfile } from "../types/user"; export default function useUser(username?: string, isCurrentUser: boolean = false) { const [user, setUser] = useState(null); diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index a5c4314..3ca10bc 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -15,32 +15,7 @@ import useUser from "../hooks/useUser"; import { useAuth } from "../hooks/useAuth"; import { followUserApi, unfollowUserApi } from "../api/user.api"; import ProfilePageNavbar from "../components/profilePage/navbar"; - -export interface UserProfile { - name: string; - username: string; - avatarUrl?: string; - bio?: string; - followers: number; - following: number; - location?: string; - website?: string; - publicRepos: number; -} - -export interface Repository { - id: string; - name: string; - description?: string; - visibility: string; - - _count?: { - staredBy: number; - fork: number; - issues: number; - }; - updatedAt?: string; -} +import type { Repository } from "../types/user"; //const LANGUAGE_COLORS: Record = { // TypeScript: "#3178c6", @@ -355,7 +330,7 @@ export default function ProfilePage({ username }: { username?: string }) {

No public repositories

) : ( - repos?.map((repo) => { + repos?.map((repo: Repository) => { const isStarred = starredRepos.has(repo.name); const displayStars = (repo._count?.staredBy || 0) + (isStarred ? 1 : 0); const displayForks = repo._count?.fork || 0; diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts new file mode 100644 index 0000000..87162dd --- /dev/null +++ b/frontend/src/types/user.ts @@ -0,0 +1,49 @@ +export interface UserProfile { + id?: string; + username: string; + email: string; + name: string; + createdAt: string; + avatarUrl?: string; + bio?: string; + location?: string; + website?: string; + followers?: Array<{ + id: string; + username: string; + name: string; + email: string; + createdAt: string; + }>; + following?: Array<{ + id: string; + username: string; + email: string; + name: string; + createdAt: string; + }>; + starRepos?: Array<{ id: string; name: string }>; + issues?: Array<{ id: string; title: string; status: string }>; + repositories?: Array<{ id: string; name: string; description: string; visibility: string }>; + forks?: number; + _count?: { + followedBy: number; + following: number; + starRepos: number; + issues: number; + forks: number; + }; +} + +export interface Repository { + id: string; + name: string; + description?: string; + visibility: string; + _count?: { + staredBy: number; + fork: number; + issues: number; + }; + updatedAt?: string; +}