diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..989cf15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# env +.env +env.js \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..44513bb --- /dev/null +++ b/app.js @@ -0,0 +1,57 @@ +import cors from "cors"; +import dotenv from "dotenv"; +import express from "express"; +import session from "express-session"; +import fs from "fs"; +import moment from "moment-timezone"; +import morgan from "morgan"; +import path, { dirname } from "path"; +import swaggerJsdoc from "swagger-jsdoc"; +import swaggerUi from "swagger-ui-express"; +import { fileURLToPath } from "url"; +import passport from "./config/passport.js"; +import errorHandler from "./middlewares/errorHandler.js"; +import articlesRouter from "./routes/articleRoutes.js"; +import authRouter from "./routes/authRoutes.js"; +import imagesRouter from "./routes/imageRoutes.js"; +import productsRouter from "./routes/productRoutes.js"; +import swaggerOptions from "./swagger/swaggerOptions.js"; +dotenv.config(); +const { JWT_SECRET } = process.env; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const app = express(); + +const specs = swaggerJsdoc(swaggerOptions); + +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); + +morgan.token("date", (req, res, tz) => { + return moment().tz(tz).format("YYYY-MM-DD HH:mm:ss"); +}); + +const customFormat = ":method :url :status :res[content-length] - :response-time ms - :date[Asia/Seoul]"; + +const accessLogStream = fs.createWriteStream(path.join(__dirname, "access.log"), { flags: "a" }); + +app.use(morgan(customFormat, { stream: accessLogStream })); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(cors()); +app.use(session({ secret: JWT_SECRET, resave: false, saveUninitialized: true })); +app.use(passport.initialize()); +app.use(passport.session()); + +app.use("/products", productsRouter); +app.use("/articles", articlesRouter); +app.use("/images", imagesRouter); +app.use("/auth", authRouter); + +app.use(errorHandler); + +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + console.log("Server is running on port 3000"); +}); diff --git a/config/passport.js b/config/passport.js new file mode 100644 index 0000000..6a14c18 --- /dev/null +++ b/config/passport.js @@ -0,0 +1,59 @@ +import { PrismaClient } from "@prisma/client"; +import dotenv from "dotenv"; +import passport from "passport"; +import { Strategy as GoogleStrategy } from "passport-google-oauth20"; +import { generateAccessToken, generateRefreshToken } from "../utils/tokens.js"; + +dotenv.config(); +const prisma = new PrismaClient(); + +const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID; +const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET; + +passport.use( + new GoogleStrategy( + { + clientID: GOOGLE_CLIENT_ID, + clientSecret: GOOGLE_CLIENT_SECRET, + callbackURL: "http://localhost:3000/auth/google/callback", + }, + async (accessToken, refreshToken, profile, done) => { + const { id, displayName, emails } = profile; + + try { + let user = await prisma.user.findUnique({ + where: { googleId: id }, + }); + + if (!user) { + user = await prisma.user.create({ + data: { + googleId: id, + email: emails[0].value, + name: displayName, + nickname: displayName, + password: null, + }, + }); + } + + const accessTokenJwt = generateAccessToken(user); + const refreshTokenJwt = generateRefreshToken(user); + + done(null, { user, accessToken: accessTokenJwt, refreshToken: refreshTokenJwt }); + } catch (error) { + done(error, null); + } + } + ) +); + +passport.serializeUser((userWithTokens, done) => { + done(null, userWithTokens); +}); + +passport.deserializeUser((userWithTokens, done) => { + done(null, userWithTokens); +}); + +export default passport; diff --git a/controllers/articleController.js b/controllers/articleController.js new file mode 100644 index 0000000..ea6b2bd --- /dev/null +++ b/controllers/articleController.js @@ -0,0 +1,86 @@ +import { assert } from "superstruct"; +import * as articleService from "../services/articleService.js"; +import { CreateArticle, PatchArticle } from "../structs.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import AppError from "../utils/errors.js"; + +export const getArticles = asyncHandler(async (req, res) => { + const { offset = 0, limit = 10, orderBy = "recent", keyword = "" } = req.query; + const articles = await articleService.getArticles({ offset, limit, orderBy, keyword }); + const bestArticles = await articleService.getBestArticles(); + res.send({ articles, bestArticles }); +}); + +export const createArticle = asyncHandler(async (req, res) => { + assert(req.body, CreateArticle); + const { userId } = req; + const article = await articleService.createArticle({ ...req.body, userId }); + res.status(201).send(article); +}); + +export const getArticleById = asyncHandler(async (req, res) => { + const { id } = req.params; + const article = await articleService.getArticleById(id); + res.send(article); +}); + +export const updateArticle = asyncHandler(async (req, res, next) => { + assert(req.body, PatchArticle); + + const { id: articleId } = req.params; + const { userId } = req; + + try { + const updatedArticle = await articleService.updateArticle(articleId, userId, req.body); + res.send(updatedArticle); + } catch (error) { + if (error instanceof AppError) { + return res.status(error.statusCode).json({ message: error.message }); + } + } +}); + +export const deleteArticle = asyncHandler(async (req, res, next) => { + const { id: articleId } = req.params; + const { userId } = req; + + try { + await articleService.deleteArticle(articleId, userId); + res.sendStatus(204); + } catch (error) { + if (error instanceof AppError) { + return res.status(error.statusCode).json({ message: error.message }); + } + next(error); + } +}); + +export const likeArticle = asyncHandler(async (req, res, next) => { + const { id: articleId } = req.params; + const { userId } = req; + + try { + const updatedArticle = await articleService.likeArticle(articleId, userId); + res.send(updatedArticle); + } catch (error) { + if (error instanceof AppError) { + return res.status(error.statusCode).json({ message: error.message }); + } + next(error); + } +}); + +export const unlikeArticle = asyncHandler(async (req, res, next) => { + const { id: articleId } = req.params; + const { userId } = req; + + try { + const updatedArticle = await articleService.unlikeArticle(articleId, userId); + res.send(updatedArticle); + } catch (error) { + if (error instanceof AppError) { + return res.status(error.statusCode).json({ message: error.message }); + } + next(error); + } +}); diff --git a/controllers/authController.js b/controllers/authController.js new file mode 100644 index 0000000..accb542 --- /dev/null +++ b/controllers/authController.js @@ -0,0 +1,74 @@ +import dotenv from "dotenv"; +import jwt from "jsonwebtoken"; +import { assert } from "superstruct"; +import { createUser, findUserByEmail, findUserById, validatePassword } from "../services/authService.js"; +import { CreateUser } from "../structs.js"; +import { generateAccessToken, generateRefreshToken, regenerateRefreshToken } from "../utils/tokens.js"; + +dotenv.config(); +const JWT_SECRET = process.env.JWT_SECRET; + +export const signUp = async (req, res) => { + const { email, password, name, nickname } = req.body; + assert(req.body, CreateUser); + + const existingUser = await findUserByEmail(email); + + if (existingUser) { + return res.status(400).json({ message: "이미 가입된 이메일입니다." }); + } + + await createUser(email, password, name, nickname); + + res.status(201).json({ message: "회원가입이 완료되었습니다." }); +}; + +export const signIn = async (req, res) => { + const { email, password } = req.body; + + const user = await findUserByEmail(email); + + if (!user) { + return res.status(401).json({ message: "이메일과 비밀번호를 확인해주세요." }); + } + + const isPasswordValid = await validatePassword(password, user.password); + + if (!isPasswordValid) { + return res.status(401).json({ message: "이메일과 비밀번호를 확인해주세요." }); + } + + const accessToken = generateAccessToken(user); + const refreshToken = generateRefreshToken(user); + + res.json({ accessToken, refreshToken }); +}; + +export const refreshToken = async (req, res) => { + const { refreshToken } = req.body; + + if (!refreshToken) { + return res.status(401).json({ message: "토큰은 필수값입니다." }); + } + + try { + const newRefreshToken = regenerateRefreshToken(refreshToken); + const decoded = jwt.verify(newRefreshToken, JWT_SECRET); + const user = await findUserById(decoded.userId); + + if (!user) { + return res.status(401).json({ message: "유효하지 않은 토큰입니다." }); + } + + const accessToken = generateAccessToken(user); + + res.json({ accessToken, refreshToken: newRefreshToken }); + } catch (error) { + return res.status(401).json({ message: "유효하지 않은 토큰입니다." }); + } +}; + +export const googleCallback = (req, res) => { + const { accessToken, refreshToken } = req.user; + res.json({ accessToken, refreshToken }); +}; diff --git a/controllers/commentController.js b/controllers/commentController.js new file mode 100644 index 0000000..f73fc09 --- /dev/null +++ b/controllers/commentController.js @@ -0,0 +1,60 @@ +import { assert } from "superstruct"; +import * as commentService from "../services/commentService.js"; +import { CreateComment, PatchComment } from "../structs.js"; +import asyncHandler from "../utils/asyncHandler.js"; + +export const getCommentsByProductId = asyncHandler(async (req, res) => { + const { id: productId } = req.params; + const { cursor } = req.query; + const comments = await commentService.getCommentsByProductId(productId, cursor); + res.send(comments); +}); + +export const getCommentsByArticleId = asyncHandler(async (req, res) => { + const { id: articleId } = req.params; + const { cursor } = req.query; + const comments = await commentService.getCommentsByArticleId(articleId, cursor); + res.send(comments); +}); + +export const createComment = asyncHandler(async (req, res) => { + assert(req.body, CreateComment); + + const { userId } = req; + + const commentData = { + ...req.body, + ...req.params, + userId, + }; + + const comment = await commentService.createComment(commentData); + res.status(201).send(comment); +}); + +export const updateComment = asyncHandler(async (req, res) => { + assert(req.body, PatchComment); + + const { userId } = req; + const { content } = req.body; + const { commentId } = req.params; + + try { + const updatedComment = await commentService.updateComment(commentId, userId, content); + res.send(updatedComment); + } catch (error) { + res.status(403).json({ message: error.message }); + } +}); + +export const deleteComment = asyncHandler(async (req, res) => { + const { commentId } = req.params; + const { userId } = req; + + try { + await commentService.deleteComment(commentId, userId); + res.sendStatus(204); + } catch (error) { + res.status(403).json({ message: error.message }); + } +}); diff --git a/controllers/imageController.js b/controllers/imageController.js new file mode 100644 index 0000000..76a3bd5 --- /dev/null +++ b/controllers/imageController.js @@ -0,0 +1,13 @@ +import * as imageService from "../services/imageService.js"; +import asyncHandler from "../utils/asyncHandler.js"; + +export const uploadImage = asyncHandler(async (req, res) => { + const file = req.file; + + try { + const imageUrl = await imageService.uploadImage(file); + res.status(200).json({ url: imageUrl }); + } catch (error) { + res.status(400).send(error.message); + } +}); diff --git a/controllers/productController.js b/controllers/productController.js new file mode 100644 index 0000000..9467511 --- /dev/null +++ b/controllers/productController.js @@ -0,0 +1,73 @@ +import { assert } from "superstruct"; +import * as productService from "../services/productService.js"; +import { CreateProduct, PatchProduct } from "../structs.js"; +import asyncHandler from "../utils/asyncHandler.js"; + +export const getProducts = asyncHandler(async (req, res) => { + const { offset = 0, limit = 10, orderBy = "recent", keyword = "" } = req.query; + const products = await productService.getProducts({ offset, limit, orderBy, keyword }); + res.send(products); +}); + +export const createProduct = asyncHandler(async (req, res) => { + assert(req.body, CreateProduct); + const { userId } = req; + const product = await productService.createProduct({ ...req.body, userId }); + res.status(201).send(product); +}); + +export const getProductById = asyncHandler(async (req, res) => { + const { id } = req.params; + const product = await productService.getProductById(id); + res.send(product); +}); + +export const updateProduct = asyncHandler(async (req, res) => { + assert(req.body, PatchProduct); + + const { id: productId } = req.params; + const { userId } = req; + + try { + const updatedProduct = await productService.updateProduct(productId, userId, req.body); + res.send(updatedProduct); + } catch (error) { + res.status(403).json({ message: error.message }); + } +}); + +export const deleteProduct = asyncHandler(async (req, res) => { + const { id: productId } = req.params; + const { userId } = req; + + try { + await productService.deleteProduct(productId, userId); + res.sendStatus(204); + } catch (error) { + res.status(403).json({ message: error.message }); + } +}); + +export const likeProduct = asyncHandler(async (req, res) => { + const { id: productId } = req.params; + const { userId } = req; + + try { + const updatedProduct = await productService.likeProduct(productId, userId); + res.send(updatedProduct); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}); + +export const unlikeProduct = asyncHandler(async (req, res) => { + const { id: productId } = req.params; + const { userId } = req; + + try { + const updatedProduct = await productService.unlikeProduct(productId, userId); + res.send(updatedProduct); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}); diff --git a/http/articles.http b/http/articles.http new file mode 100644 index 0000000..510a021 --- /dev/null +++ b/http/articles.http @@ -0,0 +1,81 @@ +# 게시글 목록 조회 +GET http://localhost:3000/articles?&limit=10&&orderBy=like + +### +# 게시글 상세 조회 + +GET http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da + +### +# 게시글 등록 +POST http://localhost:3000/articles +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg4MjIxMCwiZXhwIjoxNzE2ODgzMTEwfQ.HKPt_52tCohwFBk5yESkDiChtWpqd7pf531uQsI47kY +Content-Type: application/json + +{ + "title": "제가 아끼는 티모 인형입니다", + "content": "버섯 농사 짓는 모습이 너무 깜찍하지않나요?", + "imageUrl": "https://cdn.011st.com/11dims/resize/600x600/quality/75/11src/product/5575072075/B.jpg?51000000" +} + +### +# 게시글 수정 +PATCH http://localhost:3000/articles/c058e3ae-00d2-4b5a-a0dc-acd284f03e7b +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 +Content-Type: application/json + +{ + "title":"판다 대박이네요", + "content":"대나무를 잘 먹네요 ㄷㄷ" +} + +### +# 게시글 삭제 +DELETE http://localhost:3000/articles/9c96039f-e56d-470e-847f-aa24b8da981c +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 + +### +# 게시글 좋아요 +PATCH http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/like +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 + +### +# 게시글 좋아요 취소 +PATCH http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/unlike +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 + + + + +### +# 자유게시판 댓글 조회 +GET http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comments + +### +# 자유게시판 댓글 커서 조회 +GET http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comments?cursor=4f9cd26c-09fa-441c-997f-2fdec0fb443e + +### +# 자유게시판 댓글 등록 +POST http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comments +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 + +{ + "content":"판다가 너무 귀여워요2" +} + +### +# 자유게시판 댓글 수정 +PATCH http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comments/31599931-2aac-47fb-85ad-8de4c0a4a56d +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 + +{ + "content":"판다가 너무 귀여워요 수정" +} + +### +# 자유게시판 댓글 삭제 +DELETE http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comments/98b65e20-e12b-43fb-8b04-6525b019eb8b +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 \ No newline at end of file diff --git a/http/auth.http b/http/auth.http new file mode 100644 index 0000000..aeaf407 --- /dev/null +++ b/http/auth.http @@ -0,0 +1,29 @@ +POST http://localhost:3000/auth/signUp +Content-Type: application/json + +{ + "email":"test52@gmail.com", + "password":"pandapower", + "name":"김판다", + "nickname":"판다의 왕" +} + +### +# 로그인 + +POST http://localhost:3000/auth/signIn +Content-Type: application/json + +{ + "email":"test2@gmail.com", + "password":"pandapower" +} + +### +# 토큰 재발급 +POST http://localhost:3000/auth/refresh-token +Content-Type: application/json + +{ + "refreshToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjgsImlhdCI6MTcxNjc5Mzk0NywiZXhwIjoxNzE3Mzk4NzQ3fQ.bBxra1mgW2Pe4s9KcD_8Z5QmuFAIqZhjwKEupNpkXbs" +} \ No newline at end of file diff --git a/http/products.http b/http/products.http new file mode 100644 index 0000000..39b37f6 --- /dev/null +++ b/http/products.http @@ -0,0 +1,92 @@ + +# 상품 조회 쿼리 x +GET http://localhost:3000/products + +### +# 상품 조회 쿼리 o + +GET http://localhost:3000/products?offset=1&limit=2&keyword=판다&orderBy=favorite + +### +# 상품 조회 검색어 테스트 +GET http://localhost:3000/products?keyword=판다 + +### +# 상품 상세 조회 +GET http://localhost:3000/products/d44e8c9a0-5d45-4c9f-9b4b-7626f3c9c9a9 + +### +# 상품 등록 +POST http://localhost:3000/products +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 +Content-Type: application/json + +{ + "name": "판다랑 불곰 교환원해요", + "description": "세종시청에서 교환원합니다.", + "price": 20000, + "tags": ["판다", "불곰"], + "images": ["https://www.wishbucket.io/_next/image?url=https%3A%2F%2Fd2gfz7wkiigkmv.cloudfront.net%2Fpickin%2F2%2F1%2F2%2FHereyhSJRMOmUw7I5uWAxg&w=640&q=75","https://wimg.mk.co.kr/meet/2021/09/image_listtop_2021_854860_1630738087.jpg"] +} + +### +# 상품 수정 +PATCH http://localhost:3000/products/d4e8c9a0-5d45-4c9f-9b4b-7626f3c9c9a9 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 +Content-Type: application/json + +{ + "name":"판다 안팔려서 안판다", + "description":"안판다고 했지만 사실은 판다", + "price":7000, + "tags":["판다","안판다"] +} + +### +# 상품 삭제 +DELETE http://localhost:3000/products/d4e8c9a0-5d45-4c9f-9b4b-7626f3c9c9a9 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 + +### +# 상품 좋아요 +PATCH http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/like +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 + +### +# 상품 좋아요 취소 +PATCH http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/unlike +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 + +### +# 상품 댓글 조회 +GET http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments + +### +# 상품 댓글 커서 조회 +GET http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments?cursor=f81aac65-fb44-4973-8d6d-3789c6ba39d7 + +### +# 상품 댓글 등록 +POST http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 +Content-Type: application/json + +{ + "content":"판다가 너무 귀여워요", + "writer":"판다사랑나라사랑" +} + +### +# 상품 댓글 수정 +PATCH http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments/536a7b1b-83c9-42c1-93b9-c4755e7fc8d6 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 +Content-Type: application/json + +{ + "content":"판다가 너무 귀여워요 수정" +} + +### +# 상품 댓글 삭제 +DELETE http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments/9d7d8a72-693e-44d3-bf72-889c9ebac0d9 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlhdCI6MTcxNjg3NDc2NywiZXhwIjoxNzE2ODc1NjY3fQ.chuRJN6EHR4x2r9-ocN1lKQKwYrSnZKYULaSgj5oU88 \ No newline at end of file diff --git a/middlewares/authenticate.js b/middlewares/authenticate.js new file mode 100644 index 0000000..cb54af3 --- /dev/null +++ b/middlewares/authenticate.js @@ -0,0 +1,25 @@ +import dotenv from "dotenv"; +import jwt from "jsonwebtoken"; + +dotenv.config(); + +const JWT_SECRET = process.env.JWT_SECRET; + +const authenticate = (req, res, next) => { + const authHeader = req.headers.authorization; + if (!authHeader) { + return res.status(401).json({ message: "인증 토큰이 제공되지 않았습니다." }); + } + + const token = authHeader.split(" ")[1]; + + try { + const decoded = jwt.verify(token, JWT_SECRET); + req.userId = decoded.userId; + next(); + } catch (error) { + res.status(401).json({ message: "올바르지 않은 토큰입니다." }); + } +}; + +export default authenticate; diff --git a/middlewares/errorHandler.js b/middlewares/errorHandler.js new file mode 100644 index 0000000..c01422f --- /dev/null +++ b/middlewares/errorHandler.js @@ -0,0 +1,19 @@ +import AppError from "../utils/errors.js"; + +const errorHandler = (err, req, res, next) => { + console.error("Error handler triggered:", err.stack); + console.log("Response object:", res); + if (err instanceof AppError) { + res.status(err.statusCode).json({ + status: err.status, + message: err.message, + }); + } else { + res.status(500).json({ + status: "error", + message: "서버 오류가 발생했습니다.", + }); + } +}; + +export default errorHandler; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4efa6b8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2345 @@ +{ + "name": "QA-sprint-mission", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@prisma/client": "^5.4.2", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-session": "^1.18.0", + "is-email": "^1.0.2", + "is-uuid": "^1.0.2", + "jsonwebtoken": "^9.0.2", + "moment-timezone": "^0.5.45", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "prisma": "^5.4.2", + "superstruct": "^1.0.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@prisma/client": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.14.0.tgz", + "integrity": "sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.14.0.tgz", + "integrity": "sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w==" + }, + "node_modules/@prisma/engines": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.14.0.tgz", + "integrity": "sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==", + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.14.0", + "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "@prisma/fetch-engine": "5.14.0", + "@prisma/get-platform": "5.14.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz", + "integrity": "sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.14.0.tgz", + "integrity": "sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==", + "dependencies": { + "@prisma/debug": "5.14.0", + "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "@prisma/get-platform": "5.14.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.14.0.tgz", + "integrity": "sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==", + "dependencies": { + "@prisma/debug": "5.14.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "dependencies": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-email": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-email/-/is-email-1.0.2.tgz", + "integrity": "sha512-UojUgD2EhDTBQ2SGKwrK9edce5phRzgLsP+V5+Uu2Swi+uvjVXgH3zduM3HhT9iaC/9Kq19/TYUbP0jPoi6ioA==" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-uuid": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-uuid/-/is-uuid-1.0.2.tgz", + "integrity": "sha512-tCByphFcJgf2qmiMo5hMCgNAquNSagOetVetDvBXswGkNfoyEMvGH1yDlF8cbZbKnbVBr4Y5/rlpMz9umxyBkQ==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", + "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/oauth": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", + "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "peer": true + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prisma": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.14.0.tgz", + "integrity": "sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.14.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/superstruct": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", + "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", + "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fad07ce --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "dependencies": { + "@prisma/client": "^5.4.2", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-session": "^1.18.0", + "is-email": "^1.0.2", + "is-uuid": "^1.0.2", + "jsonwebtoken": "^9.0.2", + "moment-timezone": "^0.5.45", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "prisma": "^5.4.2", + "superstruct": "^1.0.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "type": "module", + "scripts": { + "dev": "nodemon app.js", + "start": "node app.js" + }, + "prisma": { + "seed": "node prisma/seed.js" + } +} diff --git a/prisma/migrations/20240522062619_init/migration.sql b/prisma/migrations/20240522062619_init/migration.sql new file mode 100644 index 0000000..6a86190 --- /dev/null +++ b/prisma/migrations/20240522062619_init/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "Product" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "price" DOUBLE PRECISION NOT NULL, + "tags" TEXT[], + "images" TEXT[], + "favoriteCount" INTEGER NOT NULL DEFAULT 0, + "isFavorite" BOOLEAN NOT NULL DEFAULT false, + "ownerId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20240522094218_init_article/migration.sql b/prisma/migrations/20240522094218_init_article/migration.sql new file mode 100644 index 0000000..78aea67 --- /dev/null +++ b/prisma/migrations/20240522094218_init_article/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "Article" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "imageUrl" TEXT, + "likeCount" INTEGER NOT NULL DEFAULT 0, + "isLiked" BOOLEAN NOT NULL DEFAULT false, + "writer" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20240523002157_init_comment/migration.sql b/prisma/migrations/20240523002157_init_comment/migration.sql new file mode 100644 index 0000000..645652d --- /dev/null +++ b/prisma/migrations/20240523002157_init_comment/migration.sql @@ -0,0 +1,24 @@ +-- CreateTable +CREATE TABLE "Comment" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "writer" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "productId" TEXT, + "articleId" TEXT, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Comment_productId_idx" ON "Comment"("productId"); + +-- CreateIndex +CREATE INDEX "Comment_articleId_idx" ON "Comment"("articleId"); + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20240523072342_change_writer_to_optional/migration.sql b/prisma/migrations/20240523072342_change_writer_to_optional/migration.sql new file mode 100644 index 0000000..c2d04be --- /dev/null +++ b/prisma/migrations/20240523072342_change_writer_to_optional/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "Article" ALTER COLUMN "imageUrl" SET DEFAULT '', +ALTER COLUMN "writer" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Comment" ALTER COLUMN "writer" DROP NOT NULL; diff --git a/prisma/migrations/20240527032014_add_product_and_article_image_model/migration.sql b/prisma/migrations/20240527032014_add_product_and_article_image_model/migration.sql new file mode 100644 index 0000000..55bd59b --- /dev/null +++ b/prisma/migrations/20240527032014_add_product_and_article_image_model/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "Image" ( + "id" TEXT NOT NULL, + "imagePath" TEXT NOT NULL, + "productId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "articleId" TEXT, + + CONSTRAINT "Image_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Image_productId_idx" ON "Image"("productId"); + +-- CreateIndex +CREATE INDEX "Image_articleId_idx" ON "Image"("articleId"); + +-- AddForeignKey +ALTER TABLE "Image" ADD CONSTRAINT "Image_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Image" ADD CONSTRAINT "Image_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20240527043147_add_user_and_favorite_model_and_change_product_model/migration.sql b/prisma/migrations/20240527043147_add_user_and_favorite_model_and_change_product_model/migration.sql new file mode 100644 index 0000000..50bbf1d --- /dev/null +++ b/prisma/migrations/20240527043147_add_user_and_favorite_model_and_change_product_model/migration.sql @@ -0,0 +1,60 @@ +/* + Warnings: + + - You are about to drop the column `isLiked` on the `Article` table. All the data in the column will be lost. + - You are about to drop the column `isFavorite` on the `Product` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Article" DROP COLUMN "isLiked", +ADD COLUMN "userId" INTEGER; + +-- AlterTable +ALTER TABLE "Product" DROP COLUMN "isFavorite", +ADD COLUMN "userId" INTEGER; + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + "nickname" TEXT NOT NULL, + "image" TEXT, + "password" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Favorite" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" INTEGER NOT NULL, + "productId" TEXT, + "articleId" TEXT, + + CONSTRAINT "Favorite_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Favorite_userId_productId_articleId_key" ON "Favorite"("userId", "productId", "articleId"); + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Article" ADD CONSTRAINT "Article_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Favorite" ADD CONSTRAINT "Favorite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Favorite" ADD CONSTRAINT "Favorite_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Favorite" ADD CONSTRAINT "Favorite_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240527072552_change_favorite_model/migration.sql b/prisma/migrations/20240527072552_change_favorite_model/migration.sql new file mode 100644 index 0000000..171efba --- /dev/null +++ b/prisma/migrations/20240527072552_change_favorite_model/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - A unique constraint covering the columns `[userId,productId]` on the table `Favorite` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[userId,articleId]` on the table `Favorite` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropIndex +DROP INDEX "Favorite_userId_productId_articleId_key"; + +-- CreateIndex +CREATE UNIQUE INDEX "Favorite_userId_productId_key" ON "Favorite"("userId", "productId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Favorite_userId_articleId_key" ON "Favorite"("userId", "articleId"); diff --git a/prisma/migrations/20240527074901_rename_owner_id_to_user_id/migration.sql b/prisma/migrations/20240527074901_rename_owner_id_to_user_id/migration.sql new file mode 100644 index 0000000..b019051 --- /dev/null +++ b/prisma/migrations/20240527074901_rename_owner_id_to_user_id/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - The primary key for the `Favorite` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `ownerId` on the `Product` table. All the data in the column will be lost. + - You are about to alter the column `price` on the `Product` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Integer`. + +*/ +-- AlterTable +ALTER TABLE "Favorite" DROP CONSTRAINT "Favorite_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "Favorite_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "Favorite_id_seq"; + +-- AlterTable +ALTER TABLE "Product" DROP COLUMN "ownerId", +ALTER COLUMN "price" SET DATA TYPE INTEGER; diff --git a/prisma/migrations/20240528031512_add_user_id_to_comment/migration.sql b/prisma/migrations/20240528031512_add_user_id_to_comment/migration.sql new file mode 100644 index 0000000..b5f13a6 --- /dev/null +++ b/prisma/migrations/20240528031512_add_user_id_to_comment/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `userId` to the `Comment` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Comment" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240528063148_add_google_auth/migration.sql b/prisma/migrations/20240528063148_add_google_auth/migration.sql new file mode 100644 index 0000000..88eb841 --- /dev/null +++ b/prisma/migrations/20240528063148_add_google_auth/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - A unique constraint covering the columns `[googleId]` on the table `User` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "googleId" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "User_googleId_key" ON "User"("googleId"); diff --git a/prisma/migrations/20240528063710_make_password_optional/migration.sql b/prisma/migrations/20240528063710_make_password_optional/migration.sql new file mode 100644 index 0000000..0b600a7 --- /dev/null +++ b/prisma/migrations/20240528063710_make_password_optional/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/mock.js b/prisma/mock.js new file mode 100644 index 0000000..255b921 --- /dev/null +++ b/prisma/mock.js @@ -0,0 +1,198 @@ +export const PRODUCTS = [ + { + id: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + favoriteCount: 7, + images: ["https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg"], + tags: ["판다인형", "인형", "판다"], + price: 700000, + description: "판다인형 판다", + name: "판다인형", + userId: 1, + }, + { + id: "d4e8c9a0-5d45-4c9f-9b4b-7626f3c9c9a9", + favoriteCount: 2, + images: [ + "https://view01.wemep.co.kr/wmp-product/4/879/2515748794/pm_ebifv5nrjsyf.jpg?1683280710&f=webp&w=460&h=460", + ], + tags: ["판다인형", "인형", "판다", "불곰"], + price: 7000, + description: "판다인형 안판다", + name: "불곰사세요", + userId: 2, + }, +]; + +export const ARTICLES = [ + { + id: "2c027764-d7ef-4a94-8399-f15ffbf8f4da", + title: "판다인형 구매 후기", + content: "판다인형 구매 후기입니다.", + imageUrl: "https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg", + likeCount: 7, + userId: 1, + }, + { + id: "7c8b9d2e-5d45-4c9f-9b4b-7626f3c9c9a9", + title: "판다인형 판매 후기", + content: "판다인형 판매 후기입니다.", + imageUrl: "https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg", + likeCount: 2, + userId: 1, + }, + { + id: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", + title: "불곰인형 구하는 곳 아시는분", + content: "불곰인형 구하는 곳 아시는분 계신가요?", + imageUrl: "https://wimg.mk.co.kr/meet/2021/09/image_listtop_2021_854860_1630738087.jpg", + likeCount: 3, + userId: 2, + }, +]; + +export const COMMENTS = [ + { + id: "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p", + content: "판다인형 너무 귀여워요!", + writer: "판다인형 수집가", + articleId: "2c027764-d7ef-4a94-8399-f15ffbf8f4da", + userId: 1, + }, + { + id: "2b3c4d5e-6f7g-8h9i-0j1k-2l3m4n5o6p7q", + content: "판다인형 너무 귀여워요1", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요2", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요3", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요4", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요5", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요6", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요7", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요8", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요9", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요10", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요11", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + content: "판다인형 너무 귀여워요12", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + userId: 1, + }, + { + id: "3c4d5e6f-7g8h-9i0j-1k2l-3m4n5o6p7q8r", + content: "불곰인형 너무 귀여워요!", + writer: "불곰인형 수집가", + articleId: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", + userId: 2, + }, + { + id: "3c4d5e6f-7g8h-9i0j-1k2l-3m4n5o6p7q8r", + content: "불곰인형 너무 귀여워요1", + writer: "불곰인형 수집가", + articleId: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", + userId: 2, + }, + { + content: "불곰인형 너무 귀여워요2", + writer: "불곰인형 수집가", + articleId: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", + userId: 2, + }, + { + content: "불곰인형 너무 귀여워요3", + writer: "불곰인형 수집가", + articleId: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", + userId: 2, + }, +]; + +export const USERS = [ + { + id: 1, + email: "test@gmail.com", + name: "김판다", + nickname: "판다의 왕", + password: "$2b$10$2vHNtEq54uvrSksq8CPadugrnhYwcPjltPp6z66E85HJJqMLCXGN.", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 2, + email: "test2@gmail.com", + name: "박불곰", + nickname: "킹오브불곰", + password: "$2b$10$2vHNtEq54uvrSksq8CPadugrnhYwcPjltPp6z66E85HJJqMLCXGN.", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 1, + email: "test@gmail.com", + name: "김판다", + nickname: "판다의 왕", + password: "$2b$10$2vHNtEq54uvrSksq8CPadugrnhYwcPjltPp6z66E85HJJqMLCXGN.", + createdAt: new Date(), + updatedAt: new Date(), + }, +]; + +export const FAVORITE = [ + { + id: "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p", + userId: 1, + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, +]; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..3915113 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,107 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + googleId String? @unique + email String @unique + name String? + nickname String + image String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + products Product[] + articles Article[] + favorites Favorite[] + Comment Comment[] +} + +model Product { + id String @id @default(uuid()) + name String + description String + price Int + tags String[] + images String[] + favoriteCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + comments Comment[] + Image Image[] + User User? @relation(fields: [userId], references: [id]) + userId Int? + favorites Favorite[] +} + +model Article { + id String @id @default(uuid()) + title String + content String + imageUrl String? @default("") + likeCount Int @default(0) + writer String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + comments Comment[] + Image Image[] + User User? @relation(fields: [userId], references: [id]) + userId Int? + favorites Favorite[] +} + +model Comment { + id String @id @default(uuid()) + content String + writer String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + product Product? @relation(fields: [productId], references: [id], onDelete: SetNull) + productId String? + article Article? @relation(fields: [articleId], references: [id], onDelete: SetNull) + articleId String? + userId Int + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([productId]) + @@index([articleId]) +} + +model Image { + id String @id @default(uuid()) + imagePath String + product Product? @relation(fields: [productId], references: [id], onDelete: SetNull) + productId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + article Article? @relation(fields: [articleId], references: [id], onDelete: SetNull) + articleId String? + + @@index([productId]) + @@index([articleId]) +} + +model Favorite { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + productId String? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId String? + + @@unique([userId, productId]) + @@unique([userId, articleId]) +} diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 0000000..7e067cf --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,49 @@ +import { PrismaClient } from "@prisma/client"; +import { ARTICLES, COMMENTS, FAVORITE, PRODUCTS, USERS } from "./mock.js"; +const prisma = new PrismaClient(); + +async function main() { + await prisma.user.deleteMany(); + + await prisma.user.createMany({ + data: USERS, + skipDuplicates: true, + }); + await prisma.product.deleteMany(); + + await prisma.product.createMany({ + data: PRODUCTS, + skipDuplicates: true, + }); + + await prisma.article.deleteMany(); + + await prisma.article.createMany({ + data: ARTICLES, + skipDuplicates: true, + }); + + await prisma.favorite.deleteMany(); + + await prisma.favorite.createMany({ + data: FAVORITE, + skipDuplicates: true, + }); + + await prisma.comment.deleteMany(); + + await prisma.comment.createMany({ + data: COMMENTS, + skipDuplicates: true, + }); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/routes/articleRoutes.js b/routes/articleRoutes.js new file mode 100644 index 0000000..c2d9e4d --- /dev/null +++ b/routes/articleRoutes.js @@ -0,0 +1,926 @@ +import express from "express"; +import * as articleController from "../controllers/articleController.js"; +import * as commentController from "../controllers/commentController.js"; +import authenticate from "../middlewares/authenticate.js"; +const router = express.Router(); + +/** + * @swagger + * tags: + * name: Articles + * description: 자유게시판 + */ + +/** + * @swagger + * /articles: + * get: + * summary: 게시글 목록 조회 + * tags: [Articles] + * parameters: + * - in: query + * name: offset + * schema: + * type: integer + * example: 0 + * description: 가져올 데이터의 시작 지점 + * - in: query + * name: limit + * schema: + * type: integer + * example: 10 + * description: 한 번에 가져올 데이터의 개수 + * - in: query + * name: orderBy + * schema: + * type: string + * enum: [like, recent] + * example: recent + * description: 정렬 기준 + * - in: query + * name: keyword + * schema: + * type: string + * example: "게시글 제목" + * description: 검색 키워드 + * responses: + * 200: + * description: 게시글 목록 조회 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * articles: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * title: + * type: string + * example: "게시글 제목" + * content: + * type: string + * example: "게시글 내용" + * imageUrl: + * type: string + * example: "http://example.com/image.jpg" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * likeCount: + * type: number + * example: 5 + * writer: + * type: string + * example: "작성자" + * bestArticles: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * title: + * type: string + * example: "인기 게시글 제목" + * content: + * type: string + * example: "인기 게시글 내용" + * imageUrl: + * type: string + * example: "http://example.com/image.jpg" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * likeCount: + * type: number + * example: 10 + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * articles: + * - id: "550e8400-e29b-41d4-a716-446655440000" + * title: "게시글 제목" + * content: "게시글 내용" + * imageUrl: "http://example.com/image.jpg" + * createdAt: "2023-01-01T00:00:00.000Z" + * likeCount: 5 + * writer: "작성자" + * bestArticles: + * - id: "550e8400-e29b-41d4-a716-446655440000" + * title: "인기 게시글 제목" + * content: "인기 게시글 내용" + * imageUrl: "http://example.com/image.jpg" + * createdAt: "2023-01-01T00:00:00.000Z" + * likeCount: 10 + * writer: "작성자" + * post: + * summary: 게시글 생성 + * tags: [Articles] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * example: "게시글 제목" + * content: + * type: string + * example: "게시글 내용" + * imageUrl: + * type: string + * example: "http://example.com/image.jpg" + * responses: + * 201: + * description: 게시글 생성 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * title: + * type: string + * example: "게시글 제목" + * content: + * type: string + * example: "게시글 내용" + * imageUrl: + * type: string + * example: "http://example.com/image.jpg" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * likeCount: + * type: number + * example: 0 + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * id: "550e8400-e29b-41d4-a716-446655440000" + * title: "게시글 제목" + * content: "게시글 내용" + * imageUrl: "http://example.com/image.jpg" + * createdAt: "2023-01-01T00:00:00.000Z" + * likeCount: 0 + * writer: "작성자" + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * + */ +router.route("/").get(articleController.getArticles).post(authenticate, articleController.createArticle); +/** + * @swagger + * /articles/{id}: + * get: + * summary: 특정 게시글 조회 + * tags: [Articles] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: 게시글 조회 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * title: + * type: string + * example: "게시글 제목" + * content: + * type: string + * example: "게시글 내용" + * imageUrl: + * type: string + * example: "http://example.com/image.jpg" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * likeCount: + * type: number + * example: 5 + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * id: "550e8400-e29b-41d4-a716-446655440000" + * title: "게시글 제목" + * content: "게시글 내용" + * imageUrl: "http://example.com/image.jpg" + * createdAt: "2023-01-01T00:00:00.000Z" + * likeCount: 5 + * writer: "작성자" + * 404: + * description: 게시글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 게시글입니다." + * patch: + * summary: 게시글 수정 + * tags: [Articles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * example: "게시글 제목" + * content: + * type: string + * example: "게시글 내용" + * imageUrl: + * type: string + * example: "http://example.com/image.jpg" + * responses: + * 200: + * description: 게시글 수정 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * title: + * type: string + * example: "게시글 제목" + * content: + * type: string + * example: "게시글 내용" + * imageUrl: + * type: string + * example: "http://example.com/image.jpg" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * likeCount: + * type: number + * example: 5 + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * id: "550e8400-e29b-41d4-a716-446655440000" + * title: "게시글 제목" + * content: "게시글 내용" + * imageUrl: "http://example.com/image.jpg" + * createdAt: "2023-01-01T00:00:00.000Z" + * likeCount: 5 + * writer: "작성자" + * 400: + * description: 유효성 검사 오류 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "유효성 검사 오류입니다." + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 403: + * description: 권한 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "게시글을 수정할 권한이 없습니다." + * 404: + * description: 게시글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 게시글입니다." + * delete: + * summary: 게시글 삭제 + * tags: [Articles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 204: + * description: 게시글 삭제 성공 + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 403: + * description: 권한 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "게시글을 삭제할 권한이 없습니다." + * 404: + * description: 게시글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 게시글입니다." + */ +router + .route("/:id") + .get(articleController.getArticleById) + .patch(authenticate, articleController.updateArticle) + .delete(authenticate, articleController.deleteArticle); + +/** + * @swagger + * /articles/{id}/like: + * patch: + * summary: 게시글 좋아요 + * tags: [Articles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: 게시글 좋아요 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * title: + * type: string + * example: "게시글 제목" + * content: + * type: string + * example: "게시글 내용" + * imageUrl: + * type: string + * example: "http://example.com/image.jpg" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * likeCount: + * type: number + * example: 6 + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * id: "550e8400-e29b-41d4-a716-446655440000" + * title: "게시글 제목" + * content: "게시글 내용" + * imageUrl: "http://example.com/image.jpg" + * createdAt: "2023-01-01T00:00:00.000Z" + * likeCount: 6 + * writer: "작성자" + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 409: + * description: 이미 좋아요 처리된 게시글 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "이미 좋아요 처리된 게시글입니다." + */ +router.route("/:id/like").patch(authenticate, articleController.likeArticle); + +/** + * @swagger + * /articles/{id}/unlike: + * patch: + * summary: 게시글 좋아요 취소 + * tags: [Articles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: 게시글 좋아요 취소 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * title: + * type: string + * example: "게시글 제목" + * content: + * type: string + * example: "게시글 내용" + * imageUrl: + * type: string + * example: "http://example.com/image.jpg" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * likeCount: + * type: number + * example: 4 + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * id: "550e8400-e29b-41d4-a716-446655440000" + * title: "게시글 제목" + * content: "게시글 내용" + * imageUrl: "http://example.com/image.jpg" + * createdAt: "2023-01-01T00:00:00.000Z" + * likeCount: 4 + * writer: "작성자" + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 409: + * description: 아직 좋아요 처리되지 않은 게시글 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "아직 좋아요 처리되지 않은 게시글입니다." + */ +router.route("/:id/unlike").patch(authenticate, articleController.unlikeArticle); + +/** + * @swagger + * /articles/{articleId}/comments: + * get: + * summary: 특정 게시글의 댓글 목록 조회 + * tags: [Comments] + * parameters: + * - in: path + * name: articleId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: 댓글 목록 조회 성공 + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * content: + * type: string + * example: "댓글 내용" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * - id: "550e8400-e29b-41d4-a716-446655440000" + * content: "댓글 내용" + * createdAt: "2023-01-01T00:00:00.000Z" + * writer: "작성자" + * 404: + * description: 게시글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 게시글입니다." + * post: + * summary: 댓글 작성 + * tags: [Comments] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: articleId + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * example: "댓글 내용" + * responses: + * 201: + * description: 댓글 작성 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * content: + * type: string + * example: "댓글 내용" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * id: "550e8400-e29b-41d4-a716-446655440000" + * content: "댓글 내용" + * createdAt: "2023-01-01T00:00:00.000Z" + * writer: "작성자" + * 400: + * description: 유효성 검사 오류 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "유효성 검사 오류입니다." + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 404: + * description: 게시글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 게시글입니다." + */ + +/** + * @swagger + * tags: + * name: Comments + * description: 댓글 + */ + +router + .route("/:articleId/comments") + .get(commentController.getCommentsByProductId) + .post(authenticate, commentController.createComment); + +/** + * @swagger + * /articles/{articleId}/comments/{commentId}: + * patch: + * summary: 댓글 수정 + * tags: [Comments] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: articleId + * required: true + * schema: + * type: string + * - in: path + * name: commentId + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * example: "댓글 내용" + * responses: + * 200: + * description: 댓글 수정 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * content: + * type: string + * example: "댓글 내용" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * id: "550e8400-e29b-41d4-a716-446655440000" + * content: "댓글 내용" + * createdAt: "2023-01-01T00:00:00.000Z" + * writer: "작성자" + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 403: + * description: 권한 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "이 댓글을 수정할 권한이 없습니다." + * 404: + * description: 댓글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 댓글입니다." + * delete: + * summary: 댓글 삭제 + * tags: [Comments] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: articleId + * required: true + * schema: + * type: string + * - in: path + * name: commentId + * required: true + * schema: + * type: string + * responses: + * 204: + * description: 댓글 삭제 성공 + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 403: + * description: 권한 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "이 댓글을 삭제할 권한이 없습니다." + * 404: + * description: 댓글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 댓글입니다." + */ +router + .route("/:articleId/comments/:commentId") + .patch(authenticate, commentController.updateComment) + .delete(authenticate, commentController.deleteComment); + +export default router; diff --git a/routes/authRoutes.js b/routes/authRoutes.js new file mode 100644 index 0000000..0a598a6 --- /dev/null +++ b/routes/authRoutes.js @@ -0,0 +1,223 @@ +import express from "express"; +import passport from "../config/passport.js"; +import { googleCallback, refreshToken, signIn, signUp } from "../controllers/authController.js"; +import asyncHandler from "../utils/asyncHandler.js"; + +const router = express.Router(); + +/** + * @swagger + * tags: + * name: Auth + * description: 사용자 인증 + */ + +/** + * @swagger + * /auth/signUp: + * post: + * summary: 사용자 회원가입 + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * - name + * - nickname + * properties: + * email: + * type: string + * example: "pandaking@example.com" + * password: + * type: string + * example: "password123" + * name: + * type: string + * example: "김판다" + * nickname: + * type: string + * example: "판다의 왕" + * responses: + * 201: + * description: 회원가입이 완료되었습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "회원가입이 완료되었습니다." + * 400: + * description: 이미 가입된 이메일입니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "이미 가입된 이메일입니다." + */ +router.post("/signUp", asyncHandler(signUp)); + +/** + * @swagger + * /auth/signIn: + * post: + * summary: 사용자 로그인 + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * example: "user@example.com" + * password: + * type: string + * example: "password123" + * responses: + * 200: + * description: 로그인 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * accessToken: + * type: string + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * refreshToken: + * type: string + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * 401: + * description: 이메일과 비밀번호를 확인해주세요. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "이메일과 비밀번호를 확인해주세요." + */ +router.post("/signIn", asyncHandler(signIn)); + +/** + * @swagger + * /auth/refresh-token: + * post: + * summary: JWT 토큰 갱신 + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - refreshToken + * properties: + * refreshToken: + * type: string + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * responses: + * 200: + * description: 토큰 갱신 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * accessToken: + * type: string + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * refreshToken: + * type: string + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * 401: + * description: 유효하지 않은 토큰입니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "유효하지 않은 토큰입니다." + */ +router.post("/refresh-token", asyncHandler(refreshToken)); + +/** + * @swagger + * /auth/google: + * get: + * summary: 구글 인증 + * tags: [Auth] + * responses: + * 302: + * description: 구글 인증 페이지로 리디렉션 + */ +router.get("/google", passport.authenticate("google", { scope: ["profile", "email"] })); + +/** + * @swagger + * /auth/google/callback: + * get: + * summary: 구글 OAuth 콜백 + * tags: [Auth] + * responses: + * 200: + * description: 구글 인증 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * accessToken: + * type: string + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * refreshToken: + * type: string + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * 401: + * description: 구글 인증 실패 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "구글 인증 실패" + */ +router.get("/google/callback", passport.authenticate("google", { failureRedirect: "/" }), googleCallback); + +/** + * @swagger + * /auth/logout: + * get: + * summary: 로그아웃 + * tags: [Auth] + * responses: + * 302: + * description: 로그아웃 후 리디렉션 + */ +router.get("/logout", (req, res) => { + req.logout(); + res.redirect("/"); +}); + +export default router; diff --git a/routes/imageRoutes.js b/routes/imageRoutes.js new file mode 100644 index 0000000..4966f1a --- /dev/null +++ b/routes/imageRoutes.js @@ -0,0 +1,70 @@ +import express from "express"; +import multer from "multer"; +import path from "path"; +import * as imageController from "../controllers/imageController.js"; +import authenticate from "../middlewares/authenticate.js"; + +const router = express.Router(); + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, "uploads/"); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); + cb(null, file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname)); + }, +}); + +const upload = multer({ storage: storage }); + +/** + * @swagger + * tags: + * name: Images + * description: 이미지 관리 + */ + +/** + * @swagger + * /images/upload: + * post: + * summary: 이미지 업로드 + * tags: [Images] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * image: + * type: string + * format: binary + * responses: + * 200: + * description: 이미지 업로드 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * url: + * type: string + * example: "http://localhost:3000/uploads/image-123456789.jpg" + * 400: + * description: 이미지 파일이 선택되지 않음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "이미지 파일을 선택해주세요." + */ +router.post("/upload", authenticate, upload.single("image"), imageController.uploadImage); + +export default router; diff --git a/routes/productRoutes.js b/routes/productRoutes.js new file mode 100644 index 0000000..0ea88cd --- /dev/null +++ b/routes/productRoutes.js @@ -0,0 +1,857 @@ +import express from "express"; +import * as commentController from "../controllers/commentController.js"; +import * as productController from "../controllers/productController.js"; +import authenticate from "../middlewares/authenticate.js"; +const router = express.Router(); + +/** + * @swagger + * tags: + * name: Products + * description: 중고마켓 + */ + +/** + * @swagger + * /products: + * get: + * summary: 상품 목록 조회 + * tags: [Products] + * parameters: + * - in: query + * name: offset + * schema: + * type: integer + * example: 0 + * description: 가져올 데이터의 시작 지점 + * - in: query + * name: limit + * schema: + * type: integer + * example: 10 + * description: 한 번에 가져올 데이터의 개수 + * - in: query + * name: orderBy + * schema: + * type: string + * enum: [favorite, recent] + * example: recent + * description: 정렬 기준 + * - in: query + * name: keyword + * schema: + * type: string + * example: "상품 이름" + * description: 검색 키워드 + * responses: + * 200: + * description: 상품 목록 조회 성공 + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * example: "1" + * name: + * type: string + * example: "상품 이름" + * description: + * type: string + * example: "상품 설명" + * price: + * type: number + * example: 1000 + * favoriteCount: + * type: number + * example: 5 + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * examples: + * application/json: + * value: + * - id: "1" + * name: "상품 이름" + * description: "상품 설명" + * price: 1000 + * favoriteCount: 5 + * createdAt: "2023-01-01T00:00:00.000Z" + * post: + * summary: 상품 생성 + * tags: [Products] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * example: "상품 이름" + * description: + * type: string + * example: "상품 설명" + * price: + * type: number + * example: 1000 + * responses: + * 201: + * description: 상품 생성 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "1" + * name: + * type: string + * example: "상품 이름" + * description: + * type: string + * example: "상품 설명" + * price: + * type: number + * example: 1000 + * favoriteCount: + * type: number + * example: 5 + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * examples: + * application/json: + * value: + * id: "1" + * name: "상품 이름" + * description: "상품 설명" + * price: 1000 + * favoriteCount: 5 + * createdAt: "2023-01-01T00:00:00.000Z" + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + */ +router.route("/").get(productController.getProducts).post(authenticate, productController.createProduct); + +/** + * @swagger + * /products/{id}: + * get: + * summary: 특정 상품 조회 + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: 상품 조회 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "1" + * name: + * type: string + * example: "상품 이름" + * description: + * type: string + * example: "상품 설명" + * price: + * type: number + * example: 1000 + * favoriteCount: + * type: number + * example: 5 + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * examples: + * application/json: + * value: + * id: "1" + * name: "상품 이름" + * description: "상품 설명" + * price: 1000 + * favoriteCount: 5 + * createdAt: "2023-01-01T00:00:00.000Z" + * 404: + * description: 상품을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 게시글입니다." + * patch: + * summary: 상품 수정 + * tags: [Products] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * example: "상품 이름" + * description: + * type: string + * example: "상품 설명" + * price: + * type: number + * example: 1000 + * responses: + * 200: + * description: 상품 수정 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "1" + * name: + * type: string + * example: "상품 이름" + * description: + * type: string + * example: "상품 설명" + * price: + * type: number + * example: 1000 + * favoriteCount: + * type: number + * example: 5 + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * examples: + * application/json: + * value: + * id: "1" + * name: "상품 이름" + * description: "상품 설명" + * price: 1000 + * favoriteCount: 5 + * createdAt: "2023-01-01T00:00:00.000Z" + * 400: + * description: 유효성 검사 오류 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "유효성 검사 오류입니다." + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 403: + * description: 권한 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "상품을 수정할 권한이 없습니다." + * 404: + * description: 상품을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 게시글입니다." + * delete: + * summary: 상품 삭제 + * tags: [Products] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 204: + * description: 상품 삭제 성공 + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 403: + * description: 권한 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "상품을 삭제할 권한이 없습니다." + * 404: + * description: 상품을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 게시글입니다." + */ +router + .route("/:id") + .get(productController.getProductById) + .patch(authenticate, productController.updateProduct) + .delete(authenticate, productController.deleteProduct); + +/** + * @swagger + * /products/{id}/like: + * patch: + * summary: 상품 좋아요 + * tags: [Products] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: 상품 좋아요 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "1" + * name: + * type: string + * example: "상품 이름" + * description: + * type: string + * example: "상품 설명" + * price: + * type: number + * example: 1000 + * favoriteCount: + * type: number + * example: 5 + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * examples: + * application/json: + * value: + * id: "1" + * name: "상품 이름" + * description: "상품 설명" + * price: 1000 + * favoriteCount: 6 + * createdAt: "2023-01-01T00:00:00.000Z" + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 409: + * description: 이미 좋아요 처리된 상품 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "이미 좋아요 처리된 상품입니다." + */ +router.route("/:id/like").patch(authenticate, productController.likeProduct); + +/** + * @swagger + * /products/{id}/unlike: + * patch: + * summary: 상품 좋아요 취소 + * tags: [Products] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: 상품 좋아요 취소 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "1" + * name: + * type: string + * example: "상품 이름" + * description: + * type: string + * example: "상품 설명" + * price: + * type: number + * example: 1000 + * favoriteCount: + * type: number + * example: 5 + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * examples: + * application/json: + * value: + * id: "1" + * name: "상품 이름" + * description: "상품 설명" + * price: 1000 + * favoriteCount: 4 + * createdAt: "2023-01-01T00:00:00.000Z" + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 409: + * description: 아직 좋아요 처리되지 않은 상품 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "아직 좋아요 처리되지 않은 상품입니다." + */ +router.route("/:id/unlike").patch(authenticate, productController.unlikeProduct); + +/** + * @swagger + * /products/{productId}/comments: + * get: + * summary: 특정 상품의 댓글 목록 조회 + * tags: [Comments] + * parameters: + * - in: path + * name: productId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: 댓글 목록 조회 성공 + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * content: + * type: string + * example: "댓글 내용" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * - id: "550e8400-e29b-41d4-a716-446655440000" + * content: "댓글 내용" + * createdAt: "2023-01-01T00:00:00.000Z" + * writer: "작성자" + * 404: + * description: 게시글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 게시글입니다." + * post: + * summary: 댓글 작성 + * tags: [Comments] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: articleId + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * example: "댓글 내용" + * responses: + * 201: + * description: 댓글 작성 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * content: + * type: string + * example: "댓글 내용" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * id: "550e8400-e29b-41d4-a716-446655440000" + * content: "댓글 내용" + * createdAt: "2023-01-01T00:00:00.000Z" + * writer: "작성자" + * 400: + * description: 유효성 검사 오류 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "유효성 검사 오류입니다." + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 404: + * description: 게시글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 게시글입니다." + */ + +router + .route("/:productId/comments") + .get(commentController.getCommentsByProductId) + .post(authenticate, commentController.createComment); + +/** + * @swagger + * /products/{productId}/comments/{commentId}: + * patch: + * summary: 댓글 수정 + * tags: [Comments] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: productId + * required: true + * schema: + * type: string + * - in: path + * name: commentId + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * example: "댓글 내용" + * responses: + * 200: + * description: 댓글 수정 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * example: "550e8400-e29b-41d4-a716-446655440000" + * content: + * type: string + * example: "댓글 내용" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * writer: + * type: string + * example: "작성자" + * examples: + * application/json: + * value: + * id: "550e8400-e29b-41d4-a716-446655440000" + * content: "댓글 내용" + * createdAt: "2023-01-01T00:00:00.000Z" + * writer: "작성자" + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 403: + * description: 권한 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "이 댓글을 수정할 권한이 없습니다." + * 404: + * description: 댓글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 댓글입니다." + * delete: + * summary: 댓글 삭제 + * tags: [Comments] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: articleId + * required: true + * schema: + * type: string + * - in: path + * name: commentId + * required: true + * schema: + * type: string + * responses: + * 204: + * description: 댓글 삭제 성공 + * 401: + * description: 인증 필요 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * examples: + * noToken: + * summary: 토큰이 제공되지 않은 경우 + * value: + * message: "인증 토큰이 제공되지 않았습니다." + * invalidToken: + * summary: 유효하지 않은 토큰인 경우 + * value: + * message: "유효하지 않은 토큰입니다." + * 403: + * description: 권한 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "이 댓글을 삭제할 권한이 없습니다." + * 404: + * description: 댓글을 찾을 수 없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "존재하지 않는 댓글입니다." + */ + +router + .route("/:productId/comments/:commentId") + .patch(authenticate, commentController.updateComment) + .delete(authenticate, commentController.deleteComment); + +export default router; diff --git a/services/articleService.js b/services/articleService.js new file mode 100644 index 0000000..ce3148c --- /dev/null +++ b/services/articleService.js @@ -0,0 +1,173 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export const getArticles = async ({ offset, limit, orderBy, keyword }) => { + const order = orderBy === "like" ? { likeCount: "desc" } : { createdAt: "desc" }; + const articles = await prisma.article.findMany({ + select: { + id: true, + title: true, + content: true, + imageUrl: true, + createdAt: true, + writer: true, + }, + orderBy: order, + skip: parseInt(offset), + take: parseInt(limit), + where: { + OR: [ + { + title: { + contains: keyword, + mode: "insensitive", + }, + }, + { + content: { + contains: keyword, + mode: "insensitive", + }, + }, + ], + }, + }); + + return articles; +}; + +export const getBestArticles = async () => { + const bestArticles = await prisma.article.findMany({ + orderBy: { + likeCount: "desc", + }, + take: 4, + }); + + return bestArticles; +}; + +export const createArticle = async (articleData) => { + return await prisma.article.create({ + data: articleData, + }); +}; + +export const getArticleById = async (id) => { + return await prisma.article.findUniqueOrThrow({ + where: { id }, + select: { + id: true, + title: true, + content: true, + imageUrl: true, + createdAt: true, + likeCount: true, + writer: true, + }, + }); +}; + +export const updateArticle = async (articleId, userId, articleData) => { + const article = await prisma.article.findUniqueOrThrow({ + where: { id: articleId }, + }); + + if (article.userId !== userId) { + throw new AppError("게시글을 수정할 권한이 없습니다.", 403); + } + + return await prisma.article.update({ + where: { id: articleId }, + data: articleData, + }); +}; + +export const deleteArticle = async (articleId, userId) => { + const article = await prisma.article.findUniqueOrThrow({ + where: { id: articleId }, + }); + + if (article.userId !== userId) { + throw new AppError("게시글을 삭제할 권한이 없습니다.", 403); + } + + await prisma.article.delete({ + where: { id: articleId }, + }); +}; + +export const likeArticle = async (articleId, userId) => { + const favorite = await prisma.favorite.findUnique({ + where: { + userId_articleId: { + userId, + articleId, + }, + }, + }); + + if (favorite) { + throw new AppError("이미 좋아요 처리된 게시글입니다.", 409); + } + + const [createdFavorite, updatedArticle] = await prisma.$transaction([ + prisma.favorite.create({ + data: { + userId, + articleId, + }, + }), + prisma.article.update({ + where: { + id: articleId, + }, + data: { + likeCount: { + increment: 1, + }, + }, + }), + ]); + + return updatedArticle; +}; + +export const unlikeArticle = async (articleId, userId) => { + const favorite = await prisma.favorite.findUnique({ + where: { + userId_articleId: { + userId, + articleId, + }, + }, + }); + + if (!favorite) { + throw new AppError("아직 좋아요 처리되지 않은 게시글입니다.", 409); + } + + const [deletedFavorite, updatedArticle] = await prisma.$transaction([ + prisma.favorite.delete({ + where: { + userId_articleId: { + userId, + articleId, + }, + }, + }), + prisma.article.update({ + where: { + id: articleId, + }, + data: { + likeCount: { + decrement: 1, + }, + }, + }), + ]); + + return updatedArticle; +}; diff --git a/services/authService.js b/services/authService.js new file mode 100644 index 0000000..f736007 --- /dev/null +++ b/services/authService.js @@ -0,0 +1,32 @@ +import { PrismaClient } from "@prisma/client"; +import bcrypt from "bcrypt"; + +const prisma = new PrismaClient(); + +export const createUser = async (email, password, name, nickname) => { + const hashedPassword = await bcrypt.hash(password, 10); + return prisma.user.create({ + data: { + email, + password: hashedPassword, + name, + nickname, + }, + }); +}; + +export const findUserByEmail = async (email) => { + return prisma.user.findUnique({ + where: { email }, + }); +}; + +export const findUserById = async (id) => { + return prisma.user.findUnique({ + where: { id }, + }); +}; + +export const validatePassword = async (inputPassword, storedPassword) => { + return bcrypt.compare(inputPassword, storedPassword); +}; diff --git a/services/commentService.js b/services/commentService.js new file mode 100644 index 0000000..68df378 --- /dev/null +++ b/services/commentService.js @@ -0,0 +1,106 @@ +import { PrismaClient } from "@prisma/client"; +import AppError from "../utils/errors.js"; + +const prisma = new PrismaClient(); + +export const getCommentsByProductId = async (productId, cursor) => { + let queryOptions = { + take: 10, + orderBy: { + createdAt: "desc", + }, + where: { + productId: productId, + }, + select: { + id: true, + content: true, + createdAt: true, + writer: true, + }, + }; + + if (cursor) { + queryOptions = { + ...queryOptions, + cursor: { + id: cursor, + }, + skip: 1, + }; + } + + return await prisma.comment.findMany(queryOptions); +}; + +export const getCommentsByArticleId = async (articleId, cursor) => { + let queryOptions = { + take: 10, + orderBy: { + createdAt: "desc", + }, + where: { + articleId: articleId, + }, + select: { + id: true, + content: true, + createdAt: true, + writer: true, + }, + }; + + if (cursor) { + queryOptions = { + ...queryOptions, + cursor: { + id: cursor, + }, + skip: 1, + }; + } + + return await prisma.comment.findMany(queryOptions); +}; + +export const createComment = async (commentData) => { + return await prisma.comment.create({ + data: commentData, + }); +}; + +export const updateComment = async (commentId, userId, content) => { + const comment = await prisma.comment.findUnique({ + where: { id: commentId }, + }); + + if (!comment) { + throw new AppError("존재하지 않는 댓글입니다.", 404); + } + + if (comment.userId !== userId) { + throw new AppError("이 댓글을 수정할 권한이 없습니다.", 403); + } + + return await prisma.comment.update({ + where: { id: commentId }, + data: { content }, + }); +}; + +export const deleteComment = async (commentId, userId) => { + const comment = await prisma.comment.findUnique({ + where: { id: commentId }, + }); + + if (!comment) { + throw new AppError("존재하지 않는 댓글입니다.", 404); + } + + if (comment.userId !== userId) { + throw new AppError("이 댓글을 삭제할 권한이 없습니다.", 403); + } + await prisma.comment.delete({ + where: { id: commentId }, + }); +}; diff --git a/services/imageService.js b/services/imageService.js new file mode 100644 index 0000000..0950480 --- /dev/null +++ b/services/imageService.js @@ -0,0 +1,21 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); +const SERVER_URL = "http://localhost:3000"; + +export const uploadImage = async (file) => { + if (!file) { + throw new Error("이미지 파일을 선택해주세요."); + } + + const imagePath = file.path; + const imageUrl = `${SERVER_URL}/${imagePath.replace(/\\/g, "/")}`; + + const image = await prisma.image.create({ + data: { + imagePath: imagePath, + }, + }); + + return imageUrl; +}; diff --git a/services/productService.js b/services/productService.js new file mode 100644 index 0000000..e8a3b7a --- /dev/null +++ b/services/productService.js @@ -0,0 +1,156 @@ +import { PrismaClient } from "@prisma/client"; +import AppError from "../utils/errors.js"; +const prisma = new PrismaClient(); + +export const getProducts = async ({ offset, limit, orderBy, keyword }) => { + const order = orderBy === "favorite" ? { favoriteCount: "desc" } : { createdAt: "desc" }; + return await prisma.product.findMany({ + orderBy: order, + skip: parseInt(offset), + take: parseInt(limit), + where: { + OR: [ + { + name: { + contains: keyword, + mode: "insensitive", + }, + }, + { + description: { + contains: keyword, + mode: "insensitive", + }, + }, + ], + }, + }); +}; + +export const createProduct = async (productData) => { + return await prisma.product.create({ + data: productData, + }); +}; + +export const getProductById = async (id) => { + const product = await prisma.product.findUnique({ + where: { id }, + }); + + if (!product) { + throw new AppError("존재하지 않는 상품입니다.", 404); + } + return product; +}; + +export const updateProduct = async (productId, userId, productData) => { + const product = await prisma.product.findUnique({ + where: { id: productId }, + }); + + if (!product) { + throw new AppError("존재하지 않는 상품입니다.", 404); + } + + if (product.userId !== userId) { + throw new AppError("상품을 수정할 권한이 없습니다.", 403); + } + + return await prisma.product.update({ + where: { id: productId }, + data: productData, + }); +}; + +export const deleteProduct = async (productId, userId) => { + const product = await prisma.product.findUnique({ + where: { id: productId }, + }); + + if (!product) { + throw new AppError("존재하지 않는 상품입니다.", 404); + } + + if (product.userId !== userId) { + throw new AppError("상품을 삭제할 권한이 없습니다.", 403); + } + + await prisma.product.delete({ + where: { id: productId }, + }); +}; + +export const likeProduct = async (productId, userId) => { + const favorite = await prisma.favorite.findUnique({ + where: { + userId_productId: { + userId, + productId, + }, + }, + }); + + if (favorite) { + throw AppError("이미 좋아요 처리된 상품입니다.", 409); + } + + const [createdFavorite, updatedProduct] = await prisma.$transaction([ + prisma.favorite.create({ + data: { + userId, + productId, + }, + }), + prisma.product.update({ + where: { + id: productId, + }, + data: { + favoriteCount: { + increment: 1, + }, + }, + }), + ]); + + return updatedProduct; +}; + +export const unlikeProduct = async (productId, userId) => { + const favorite = await prisma.favorite.findUnique({ + where: { + userId_productId: { + userId, + productId, + }, + }, + }); + + if (!favorite) { + throw AppError("아직 좋아요 처리되지 않은 상품입니다.", 409); + } + + const [deletedFavorite, updatedProduct] = await prisma.$transaction([ + prisma.favorite.delete({ + where: { + userId_productId: { + userId, + productId, + }, + }, + }), + prisma.product.update({ + where: { + id: productId, + }, + data: { + favoriteCount: { + decrement: 1, + }, + }, + }), + ]); + + return updatedProduct; +}; diff --git a/structs.js b/structs.js new file mode 100644 index 0000000..a92e0c9 --- /dev/null +++ b/structs.js @@ -0,0 +1,36 @@ +import isEmail from "is-email"; +import * as s from "superstruct"; +const PositivePrice = s.refine(s.number(), "PositivePrice", (value) => value > 0 && value < 1000000000); + +export const CreateProduct = s.object({ + images: s.array(s.string()), + tags: s.array(s.size(s.string(), 1, 32)), + price: PositivePrice, + description: s.string(), + name: s.size(s.string(), 1, 60), +}); + +export const PatchProduct = s.partial(CreateProduct); + +export const CreateArticle = s.object({ + title: s.size(s.string(), 1, 60), + content: s.string(), + imageUrl: s.optional(s.string()), + writer: s.optional(s.string()), +}); + +export const PatchArticle = s.partial(CreateArticle); + +export const CreateComment = s.object({ + content: s.string(), + writer: s.optional(s.string()), +}); + +export const PatchComment = s.partial(CreateComment); + +export const CreateUser = s.object({ + email: s.define("Email", isEmail), + password: s.size(s.string(), 1, 32), + name: s.size(s.string(), 1, 16), + nickname: s.size(s.string(), 1, 16), +}); diff --git a/swagger/swaggerOptions.js b/swagger/swaggerOptions.js new file mode 100644 index 0000000..454df85 --- /dev/null +++ b/swagger/swaggerOptions.js @@ -0,0 +1,42 @@ +import { readFileSync } from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const packageJson = JSON.parse(readFileSync(path.join(__dirname, "../package.json"), "utf8")); +const { version } = packageJson; + +const swaggerOptions = { + swaggerDefinition: { + openapi: "3.0.0", + info: { + title: "Panda Market API", + version, + description: "API documentation for Panda Market", + }, + servers: [ + { + url: "http://localhost:3000", + description: "Development server", + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + apis: ["./routes/*.js"], +}; + +export default swaggerOptions; diff --git a/test.html b/test.html new file mode 100644 index 0000000..4e32f08 --- /dev/null +++ b/test.html @@ -0,0 +1,46 @@ + + + +
+ + +