diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index ec85f6f1a..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,21 +0,0 @@ - -## 요구사항 - -### 기본 -- [x] 기본 항목 1 -- [ ] 기본 항목 2 - -### 심화 -- [ ] 심화 항목 1 -- [ ] 심화 항목 2 - -## 주요 변경사항 -- -- - -## 스크린샷 -![image](이미지url) - -## 멘토에게 -- 셀프 코드 리뷰를 통해 질문 이어가겠습니다. -- diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..980180f3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +# Keep environment variables out of version control +.env +node_modules +uploads +/generated/prisma diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..3ee282f81 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2 +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..8c2262710 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +스프린트 미션5 diff --git a/dist/ArticleService.js b/dist/ArticleService.js new file mode 100644 index 000000000..86d90a8cc --- /dev/null +++ b/dist/ArticleService.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class ArticleService { + constructor(articleRepository) { + this.articleRepository = articleRepository; + } + async createArticle(data) { + const { userId, ...rest } = data; + return this.articleRepository.createArticle({ + ...rest, + user: { connect: { id: userId } }, + }); + } + async getArticleById(id) { + return this.articleRepository.findArticleById(id); + } + async getArticles(options) { + return this.articleRepository.findArticles(options); + } + async updateArticle(id, data) { + return this.articleRepository.updateArticle(id, data); + } + async deleteArticle(id) { + return this.articleRepository.deleteArticle(id); + } +} +exports.default = ArticleService; diff --git a/dist/CommentService.js b/dist/CommentService.js new file mode 100644 index 000000000..4aa7edb7e --- /dev/null +++ b/dist/CommentService.js @@ -0,0 +1,29 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class CommentService { + constructor(commentRepository) { + this.commentRepository = commentRepository; + } + async createComment(data) { + const { userId, productId, articleId, ...rest } = data; + return this.commentRepository.createComment({ + ...rest, + user: { connect: { id: userId } }, + ...(productId && { product: { connect: { id: productId } } }), + ...(articleId && { article: { connect: { id: articleId } } }), + }); + } + async getCommentById(id) { + return this.commentRepository.findCommentById(id); + } + async getComments(options) { + return this.commentRepository.findComments(options); + } + async updateComment(id, data) { + return this.commentRepository.updateComment(id, data); + } + async deleteComment(id) { + return this.commentRepository.deleteComment(id); + } +} +exports.default = CommentService; diff --git a/dist/LikeService.js b/dist/LikeService.js new file mode 100644 index 000000000..8453acd5f --- /dev/null +++ b/dist/LikeService.js @@ -0,0 +1,29 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class LikeService { + constructor(likeRepository) { + this.likeRepository = likeRepository; + } + async createLike(data) { + const { userId, productId, articleId, ...rest } = data; + return this.likeRepository.createLike({ + ...rest, + user: { connect: { id: userId } }, + ...(productId && { product: { connect: { id: productId } } }), + ...(articleId && { article: { connect: { id: articleId } } }), + }); + } + async deleteLike(id) { + return this.likeRepository.deleteLike(id); + } + async findLikeByUserIdAndProductId(userId, productId) { + return this.likeRepository.findLikeByUserIdAndProductId(userId, productId); + } + async findLikeByUserIdAndArticleId(userId, articleId) { + return this.likeRepository.findLikeByUserIdAndArticleId(userId, articleId); + } + async findLikes(options) { + return this.likeRepository.findLikes(options); + } +} +exports.default = LikeService; diff --git a/dist/ProductService.js b/dist/ProductService.js new file mode 100644 index 000000000..9a9d5961f --- /dev/null +++ b/dist/ProductService.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class ProductService { + constructor(productRepository) { + this.productRepository = productRepository; + } + async createProduct(data) { + const { userId, ...rest } = data; + return this.productRepository.createProduct({ + ...rest, + user: { connect: { id: userId } }, + }); + } + async getProductById(id) { + return this.productRepository.findProductById(id); + } + async getProducts(options) { + return this.productRepository.findProducts(options); + } + async updateProduct(id, data) { + return this.productRepository.updateProduct(id, data); + } + async deleteProduct(id) { + return this.productRepository.deleteProduct(id); + } +} +exports.default = ProductService; diff --git a/dist/UserService.js b/dist/UserService.js new file mode 100644 index 000000000..6f367803d --- /dev/null +++ b/dist/UserService.js @@ -0,0 +1,66 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const bcrypt_1 = __importDefault(require("bcrypt")); +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +class UserService { + constructor(userRepository) { + // 회원가입 로직 + this.signUp = async (userData) => { + const { email, nickname, password } = userData; + // 이메일 중복 확인 + const existingUser = await this.userRepository.findUserByEmail(email); + if (existingUser) { + throw new Error('이미 사용중인 이메일입니다.'); + } + // 비밀번호 해싱 + const hashedPassword = await bcrypt_1.default.hash(password, 10); + // 유저 생성 + const user = await this.userRepository.createUser({ + email, + nickname, + password: hashedPassword, + }); + // 사용자 정보 반환 + const { password: _, refreshToken: __, ...userWithoutPassword } = user; + return userWithoutPassword; + }; + this.signIn = async (email, password) => { + // 이메일로 사용자 조회 + const user = await this.userRepository.findUserByEmail(email); + if (!user) { + throw new Error('존재하지 않는 이메일입니다.'); + } + // 비밀번호 확인 + const isPasswordMatched = await bcrypt_1.default.compare(password, user.password); + if (!isPasswordMatched) { + throw new Error('비밀번호가 일치하지 않습니다.'); + } + // Access Token 생성 (12시간) + const accessToken = jsonwebtoken_1.default.sign({ userId: user.id }, process.env.JWT_SECRET_KEY, { expiresIn: '12h' }); + // Refresh Token 생성 (7일) + const refreshToken = jsonwebtoken_1.default.sign({ userId: user.id }, process.env.REFRESH_TOKEN_SECRET_KEY, { expiresIn: '7d' }); + // Refresh Token을 해싱해서 DB에 저장 + await this.userRepository.updateUser(user.id, { + refreshToken: await bcrypt_1.default.hash(refreshToken, 10), + }); + return { accessToken, refreshToken }; + }; + this.getUserById = async (id) => { + return this.userRepository.findUserById(id); + }; + this.updateUser = async (id, data) => { + return this.userRepository.updateUser(id, data); + }; + this.updatePassword = async (id, newPasswordHash) => { + return this.userRepository.updateUser(id, { password: newPasswordHash }); + }; + this.getProductsByUserId = async (userId) => { + return this.userRepository.findProductsByUserId(userId); + }; + this.userRepository = userRepository; + } +} +exports.default = UserService; diff --git a/dist/controllers/ArticlesController.js b/dist/controllers/ArticlesController.js new file mode 100644 index 000000000..a5026ccd6 --- /dev/null +++ b/dist/controllers/ArticlesController.js @@ -0,0 +1,254 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const CommentService_1 = __importDefault(require("../CommentService")); +const LikeService_1 = __importDefault(require("../LikeService")); +const CommentRepository_1 = __importDefault(require("../repositories/CommentRepository")); +const LikeRepository_1 = __importDefault(require("../repositories/LikeRepository")); +class ArticlesController { + constructor(articleService) { + this.createArticle = async (req, res, next) => { + try { + const { title, content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await this.articleService.createArticle({ + title, + content, + userId: user.id, + }); + res.status(201).json(article); + } + catch (error) { + next(error); + } + }; + this.getArticles = async (req, res, next) => { + try { + const { sort, search } = req.query; + let page = parseInt(req.query.page) || 1; + let limit = parseInt(req.query.limit) || 10; + let offset = (page - 1) * limit; + const where = search + ? { + OR: [ + { title: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ], + } + : {}; + const user = req.user; + const articles = await this.articleService.getArticles({ + where, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + let responseArticles = articles; + if (user) { + const articleIds = articles.map(article => article.id); + const likes = await this.likeService.findLikes({ + where: { + userId: user.id, + articleId: { in: articleIds }, + }, + }); + const likedArticleIds = new Set(likes.map((like) => like.articleId)); + responseArticles = articles.map(article => ({ + ...article, + isLiked: likedArticleIds.has(article.id), + })); + } + else { + responseArticles = articles.map(article => ({ + ...article, + isLiked: false, + })); + } + res.status(200).json(responseArticles); + } + catch (error) { + next(error); + } + }; + this.getArticleById = async (req, res, next) => { + try { + const { articleId } = req.params; + const user = req.user; + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); + } + let isLiked = false; + if (user) { + const like = await this.likeService.findLikeByUserIdAndArticleId(user.id, parseInt(articleId)); + if (like) { + isLiked = true; + } + } + const responseArticle = { ...article, isLiked }; + res.status(200).json(responseArticle); + } + catch (error) { + next(error); + } + }; + this.updateArticle = async (req, res, next) => { + try { + const { articleId } = req.params; + const { title, content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 수정 권한이 없습니다.' }); + } + const updatedArticle = await this.articleService.updateArticle(parseInt(articleId), { + title, + content, + }); + res.status(200).json(updatedArticle); + } + catch (error) { + next(error); + } + }; + this.deleteArticle = async (req, res, next) => { + try { + const { articleId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); + } + await this.articleService.deleteArticle(parseInt(articleId)); + res.status(204).send(); + } + catch (error) { + next(error); + } + }; + this.createComment = async (req, res, next) => { + try { + const { articleId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '댓글을 입력해주세요.' }); + const newComment = await this.commentService.createComment({ + content, + articleId: parseInt(articleId), + userId: user.id, + }); + res.status(201).json(newComment); + } + catch (error) { + next(error); + } + }; + this.getComments = async (req, res, next) => { + try { + const { articleId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor) : undefined; + let limit = parseInt(req.query.limit) || 10; + const comments = await this.commentService.getComments({ + where: { articleId: parseInt(articleId) }, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } + catch (error) { + next(error); + } + }; + this.updateComment = async (req, res, next) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); + const comment = await this.commentService.getCommentById(parseInt(commentId)); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + const updatedComment = await this.commentService.updateComment(parseInt(commentId), { + content, + }); + res.status(200).json(updatedComment); + } + catch (error) { + next(error); + } + }; + this.deleteComment = async (req, res, next) => { + try { + const { commentId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const comment = await this.commentService.getCommentById(parseInt(commentId)); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); + } + await this.commentService.deleteComment(parseInt(commentId)); + res.status(204).send(); + } + catch (error) { + next(error); + } + }; + this.toggleLike = async (req, res, next) => { + try { + const { articleId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); + } + const existingLike = await this.likeService.findLikeByUserIdAndArticleId(user.id, parseInt(articleId)); + if (existingLike) { + await this.likeService.deleteLike(existingLike.id); + res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); + } + else { + await this.likeService.createLike({ + userId: user.id, + articleId: parseInt(articleId), + }); + res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); + } + } + catch (error) { + next(error); + } + }; + this.articleService = articleService; + const commentRepository = new CommentRepository_1.default(); + this.commentService = new CommentService_1.default(commentRepository); + const likeRepository = new LikeRepository_1.default(); + this.likeService = new LikeService_1.default(likeRepository); + } +} +exports.default = ArticlesController; diff --git a/dist/controllers/ProductsController.js b/dist/controllers/ProductsController.js new file mode 100644 index 000000000..bf4ef522f --- /dev/null +++ b/dist/controllers/ProductsController.js @@ -0,0 +1,256 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const CommentService_1 = __importDefault(require("../CommentService")); +const LikeService_1 = __importDefault(require("../LikeService")); +const CommentRepository_1 = __importDefault(require("../repositories/CommentRepository")); +const LikeRepository_1 = __importDefault(require("../repositories/LikeRepository")); +class ProductsController { + constructor(productService) { + this.createProduct = async (req, res, next) => { + try { + const { name, description, price } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await this.productService.createProduct({ + name, + content: description, // Map description to content + price, + userId: user.id, + }); + res.status(201).json(product); + } + catch (error) { + next(error); + } + }; + this.getProducts = async (req, res, next) => { + try { + const { sort, search } = req.query; + let page = parseInt(req.query.page) || 1; + let limit = parseInt(req.query.limit) || 10; + let offset = (page - 1) * limit; + const where = search + ? { + OR: [ + { name: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' + } }, + ], + } + : {}; + const user = req.user; + const products = await this.productService.getProducts({ + where, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + let responseProducts = products; + if (user) { + const productIds = products.map(product => product.id); + const likes = await this.likeService.findLikes({ + where: { + userId: user.id, + productId: { in: productIds }, + }, + }); + const likedProductIds = new Set(likes.map((like) => like.productId)); + responseProducts = products.map(product => ({ + ...product, + isLiked: likedProductIds.has(product.id), + })); + } + else { + responseProducts = products.map(product => ({ + ...product, + isLiked: false, + })); + } + res.status(200).json(responseProducts); + } + catch (error) { + next(error); + } + }; + this.getProductById = async (req, res, next) => { + try { + const { productId } = req.params; + const user = req.user; + const product = await this.productService.getProductById(parseInt(productId)); + if (!product) + return res.status(404).json({ message: '상품을 찾을수 없습니다.' }); + let isLiked = false; + if (user) { + const like = await this.likeService.findLikeByUserIdAndProductId(user.id, parseInt(productId)); + if (like) { + isLiked = true; + } + } + const responseProduct = { ...product, isLiked }; + res.status(200).json(responseProduct); + } + catch (error) { + next(error); + } + }; + this.updateProduct = async (req, res, next) => { + try { + const { productId } = req.params; + const { name, description, price } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await this.productService.getProductById(parseInt(productId)); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 수정 권한이 없습니다.' }); + } + const updatedProduct = await this.productService.updateProduct(parseInt(productId), { + name, + description, + price, + }); + res.status(200).json(updatedProduct); + } + catch (error) { + next(error); + } + }; + this.deleteProduct = async (req, res, next) => { + try { + const { productId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await this.productService.getProductById(parseInt(productId)); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); + } + await this.productService.deleteProduct(parseInt(productId)); + res.status(204).send(); + } + catch (error) { + next(error); + } + }; + this.createComment = async (req, res, next) => { + try { + const { productId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '댓글을 입력해주세요.' }); + const newComment = await this.commentService.createComment({ + content, + productId: parseInt(productId), + userId: user.id, + }); + res.status(201).json(newComment); + } + catch (error) { + next(error); + } + }; + this.getComments = async (req, res, next) => { + try { + const { productId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor) : undefined; + let limit = parseInt(req.query.limit) || 10; + const comments = await this.commentService.getComments({ + where: { productId: parseInt(productId) }, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } + catch (error) { + next(error); + } + }; + this.updateComment = async (req, res, next) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + const existingComment = await this.commentService.getCommentById(parseInt(commentId)); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + const updatedComment = await this.commentService.updateComment(parseInt(commentId), { + content, + }); + res.status(200).json(updatedComment); + } + catch (error) { + next(error); + } + }; + this.deleteComment = async (req, res, next) => { + try { + const { commentId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const existingComment = await this.commentService.getCommentById(parseInt(commentId)); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); + } + await this.commentService.deleteComment(parseInt(commentId)); + res.status(204).send(); + } + catch (error) { + next(error); + } + }; + this.toggleLike = async (req, res, next) => { + try { + const { productId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await this.productService.getProductById(parseInt(productId)); + if (!product) { + return res.status(404).json({ message: '상품을 찾을 수 없습니다.' }); + } + const existingLike = await this.likeService.findLikeByUserIdAndProductId(user.id, parseInt(productId)); + if (existingLike) { + await this.likeService.deleteLike(existingLike.id); + res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); + } + else { + await this.likeService.createLike({ + userId: user.id, + productId: parseInt(productId), + }); + res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); + } + } + catch (error) { + next(error); + } + }; + this.productService = productService; + const commentRepository = new CommentRepository_1.default(); + this.commentService = new CommentService_1.default(commentRepository); + const likeRepository = new LikeRepository_1.default(); + this.likeService = new LikeService_1.default(likeRepository); + } +} +exports.default = ProductsController; diff --git a/dist/controllers/UploadController.js b/dist/controllers/UploadController.js new file mode 100644 index 000000000..101e61eaf --- /dev/null +++ b/dist/controllers/UploadController.js @@ -0,0 +1,38 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const multer_1 = __importDefault(require("multer")); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const sharp_1 = __importDefault(require("sharp")); +const uploadDir = 'uploads/'; +if (!fs_1.default.existsSync(uploadDir)) { + fs_1.default.mkdirSync(uploadDir); +} +const storage = multer_1.default.memoryStorage(); +const upload = (0, multer_1.default)({ storage: storage }); +class UploadController { + constructor() { + this.uploadImage = async (req, res, next) => { + if (!req.file) { + return res.status(400).json({ message: '이미지 파일이 필요합니다.' }); + } + try { + const ext = path_1.default.extname(req.file.originalname); + const filename = Date.now() + ext; + const imagePath = path_1.default.join(uploadDir, filename); + await (0, sharp_1.default)(req.file.buffer) + .resize({ width: 500 }) + .toFile(imagePath); + const imageUrl = `/uploads/${filename}`; + res.status(201).json({ imageUrl: imageUrl }); + } + catch (error) { + next(error); + } + }; + } +} +exports.default = UploadController; diff --git a/dist/controllers/UsersController.js b/dist/controllers/UsersController.js new file mode 100644 index 000000000..e8a161d36 --- /dev/null +++ b/dist/controllers/UsersController.js @@ -0,0 +1,187 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const bcrypt_1 = __importDefault(require("bcrypt")); +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +class UsersController { + constructor(userService) { + this.signUp = async (req, res, next) => { + try { + const { email, nickname, password } = req.body; + if (!email || !nickname || !password) { + return res.status(400).json({ message: '모든 정보를 입력해주세요' }); + } + const newUser = await this.userService.signUp({ email, nickname, password }); + return res.status(201).json({ + message: '회원가입이 완료되었습니다.', + data: newUser, + }); + } + catch (error) { + if (error instanceof Error) { + return res.status(409).json({ message: error.message }); + } + next(error); + } + }; + this.signIn = async (req, res, next) => { + try { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.' }); + } + const { accessToken, refreshToken } = await this.userService.signIn(email, password); + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: false, + maxAge: 1000 * 60 * 60 * 24 * 7, + }); + return res.status(200).json({ + message: '로그인에 성공했습니다.', + data: { accessToken }, + }); + } + catch (error) { + if (error instanceof Error) { + return res.status(401).json({ message: error.message }); + } + next(error); + } + }; + this.getMe = async (req, res, next) => { + try { + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + res.status(200).json({ + message: '내 정보 조회 성공', + data: { + id: user.id, + email: user.email, + nickname: user.nickname, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }); + } + catch (error) { + next(error); + } + }; + this.updateMe = async (req, res, next) => { + try { + const { nickname, image } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!nickname && !image) { + return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + } + const updatedData = { + ...(nickname && { nickname }), + ...(image && { image }), + }; + const updatedUser = await this.userService.updateUser(user.id, updatedData); + res.status(200).json({ + message: '내 정보 수정에 성공했습니다.', + data: { + id: updatedUser.id, + email: updatedUser.email, + nickname: updatedUser.nickname, + image: updatedUser.image, + createdAt: updatedUser.createdAt, + updatedAt: updatedUser.updatedAt, + }, + }); + } + catch (error) { + next(error); + } + }; + this.changePassword = async (req, res, next) => { + try { + const { currentPassword, newPassword, confirmNewPassword } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!currentPassword || !newPassword || !confirmNewPassword) { + return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); + } + if (newPassword !== confirmNewPassword) { + return res.status(400).json({ message: '새 비밀번호와 확인 비밀번호가 일치하지 않습니다.' }); + } + const existingUser = await this.userService.getUserById(user.id); + if (!existingUser) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const isPasswordMatched = await bcrypt_1.default.compare(currentPassword, existingUser.password); + if (!isPasswordMatched) { + return res.status(401).json({ message: '현재 비밀번호가 일치하지 않습니다.' }); + } + const hashedNewPassword = await bcrypt_1.default.hash(newPassword, 10); + await this.userService.updatePassword(user.id, hashedNewPassword); + res.status(200).json({ message: '비밀번호 변경이 완료되었습니다.' }); + } + catch (error) { + next(error); + } + }; + this.getMyProducts = async (req, res, next) => { + try { + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const products = await this.userService.getProductsByUserId(user.id); + res.status(200).json({ + message: '내가 작성한 상품 목록 조회에 성공했습니다.', + data: products, + }); + } + catch (error) { + next(error); + } + }; + this.refreshToken = async (req, res, next) => { + try { + const { refreshToken } = req.cookies; + if (!refreshToken) { + return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); + } + const decodedToken = jsonwebtoken_1.default.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET_KEY); + const userId = decodedToken.userId; + const user = await this.userService.getUserById(userId); + if (!user) { + return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); + } + const isRefreshTokenMatched = await bcrypt_1.default.compare(refreshToken, user.refreshToken); + if (!isRefreshTokenMatched) { + return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); + } + const newAccessToken = jsonwebtoken_1.default.sign({ userId: user.id }, process.env.JWT_SECRET_KEY, { + expiresIn: '12h', + }); + return res.status(200).json({ + message: 'Access Token이 재발급되었습니다.', + data: { accessToken: newAccessToken }, + }); + } + catch (error) { + if (error instanceof Error) { + if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { + return res.status(401).json({ message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 ' }); + } + } + next(error); + } + }; + this.userService = userService; + } +} +exports.default = UsersController; diff --git a/dist/dtos/ArticleDto.js b/dist/dtos/ArticleDto.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/dist/dtos/ArticleDto.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/dtos/ProductDto.js b/dist/dtos/ProductDto.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/dist/dtos/ProductDto.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/dtos/UserDto.js b/dist/dtos/UserDto.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/dist/dtos/UserDto.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 000000000..e2b107d76 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,37 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const dotenv_1 = __importDefault(require("dotenv")); +dotenv_1.default.config(); +const express_1 = __importDefault(require("express")); +const cors_1 = __importDefault(require("cors")); +const path_1 = __importDefault(require("path")); +const client_1 = require("@prisma/client"); +const app = (0, express_1.default)(); +const PORT = process.env.PORT || 3000; +const prisma = new client_1.PrismaClient(); +// import router +const products_router_1 = __importDefault(require("./routes/products.router")); +const articles_router_1 = __importDefault(require("./routes/articles.router")); +const upload_router_1 = __importDefault(require("./routes/upload.router")); +const users_router_1 = __importDefault(require("./routes/users.router")); +// Middleware +app.use((0, cors_1.default)()); +app.use(express_1.default.json()); +app.use(express_1.default.urlencoded({ extended: true })); +app.use('/uploads', express_1.default.static(path_1.default.join(__dirname, 'uploads'))); +// route settitng +app.use('/api', [products_router_1.default, articles_router_1.default, upload_router_1.default, users_router_1.default]); +// Error Handler Middleware +app.use((err, req, res, next) => { + console.error(err.stack); + const statusCode = err.statusCode || 500; + const message = err.message || '오류가 발생했습니다.'; + res.status(statusCode).json({ message }); +}); +app.listen(PORT, () => { + console.log(`서버가 ${PORT}번에서 실행중입니다.`); +}); +exports.default = prisma; diff --git a/dist/main.js b/dist/main.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/dist/main.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/middlewares/auth.middleware.js b/dist/middlewares/auth.middleware.js new file mode 100644 index 000000000..e8bae92ad --- /dev/null +++ b/dist/middlewares/auth.middleware.js @@ -0,0 +1,53 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +const index_1 = __importDefault(require("../index")); // Import prisma from index.ts +const authMiddleware = async (req, res, next) => { + try { + // 헤더에서 authorization 값 가져오기 + const { authorization } = req.headers; + if (!authorization) { + return res.status(401).json({ message: '인증 정보가 없습니다.' }); + } + // authorization 값에서 토큰 추출하기 + const [tokenType, token] = authorization.split(' '); + if (tokenType !== 'Bearer') { + return res.status(401).json({ message: '지원하지 않는 인증 방식입니다.' }); + } + // 토큰이 없을 경우 + if (!token) { + return res.status(401).json({ message: '인증 정보가 없습니다.' }); + } + // 토큰 검증 (확인) + const decodedToken = jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET_KEY); + const userId = decodedToken.userId; + // 토큰 있는 userId로 조회 + const user = await index_1.default.user.findUnique({ + where: { id: userId }, + }); + if (!user) { + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } + // 사용자 정보 저장 + req.user = user; + next(); + } + catch (error) { + console.error('인증 미들웨어 에러:', error); + if (error instanceof Error) { + switch (error.name) { + case 'TokenExpiredError': + return res.status(401).json({ message: '인증 토큰이 만료되었습니다.' }); + case 'JsonWebTokenError': + return res.status(401).json({ message: '유효하지 않은 인증 토큰입니다.' }); + default: + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } + } + next(error); + } +}; +exports.default = authMiddleware; diff --git a/dist/middlewares/optionalAuth.middleware.js b/dist/middlewares/optionalAuth.middleware.js new file mode 100644 index 000000000..0eeba9b2b --- /dev/null +++ b/dist/middlewares/optionalAuth.middleware.js @@ -0,0 +1,34 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +const index_1 = __importDefault(require("../index")); // Import prisma from index.ts +// 선택적 인증 미들웨어 +// 인증에 성공하면 사용자 정보를 추가하고 실패해도 에러없이 다음 미들웨어 진행 +const optionalAuthMiddleware = async (req, res, next) => { + try { + const { authorization } = req.headers; + if (!authorization) { + return next(); + } + const [tokenType, token] = authorization.split(' '); + if (tokenType !== 'Bearer' || !token) { + return next(); + } + const decodedToken = jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET_KEY); + const user = await index_1.default.user.findUnique({ + where: { id: decodedToken.userId }, + }); + if (user) { + req.user = user; + } + } + catch (error) { + // 에러 발생 시에도 다음 미들웨어로 진행 (선택적 인증이므로) + console.error('선택적 인증 미들웨어 에러:', error); + } + return next(); +}; +exports.default = optionalAuthMiddleware; diff --git a/dist/middlewares/validation.middleware.js b/dist/middlewares/validation.middleware.js new file mode 100644 index 000000000..cfbb8d93b --- /dev/null +++ b/dist/middlewares/validation.middleware.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateArticle = exports.validateProduct = void 0; +//validation +const validateProduct = (req, res, next) => { + const { name, description, price } = req.body; + if (!name || !description || price == null) { + return res.status(400).json({ message: '이름, 설명, 가격을 입력해야 합니다.' }); + } + if (typeof price !== 'number' || price <= 0) { + return res.status(400).json({ message: '가격은 0보다 커야합니다.' }); + } + next(); +}; +exports.validateProduct = validateProduct; +//article validation +const validateArticle = (req, res, next) => { + const { title, content } = req.body; + if (!title || !content) { + return res.status(400).json({ message: '제목, 내용을 입력해야 합니다.' }); + } + next(); +}; +exports.validateArticle = validateArticle; diff --git a/dist/repositories/ArticleRepository.js b/dist/repositories/ArticleRepository.js new file mode 100644 index 000000000..0addad0e0 --- /dev/null +++ b/dist/repositories/ArticleRepository.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("../index")); +class ArticleRepository { + async findArticleById(id) { + return index_1.default.article.findUnique({ where: { id } }); + } + async findArticles(options) { + return index_1.default.article.findMany(options); + } + async createArticle(data) { + return index_1.default.article.create({ data }); + } + async updateArticle(id, data) { + return index_1.default.article.update({ where: { id }, data }); + } + async deleteArticle(id) { + return index_1.default.article.delete({ where: { id } }); + } +} +exports.default = ArticleRepository; diff --git a/dist/repositories/CommentRepository.js b/dist/repositories/CommentRepository.js new file mode 100644 index 000000000..912326e01 --- /dev/null +++ b/dist/repositories/CommentRepository.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("../index")); +class CommentRepository { + async findCommentById(id) { + return index_1.default.comment.findUnique({ where: { id } }); + } + async findComments(options) { + return index_1.default.comment.findMany(options); + } + async createComment(data) { + return index_1.default.comment.create({ data }); + } + async updateComment(id, data) { + return index_1.default.comment.update({ where: { id }, data }); + } + async deleteComment(id) { + return index_1.default.comment.delete({ where: { id } }); + } +} +exports.default = CommentRepository; diff --git a/dist/repositories/LikeRepository.js b/dist/repositories/LikeRepository.js new file mode 100644 index 000000000..013636883 --- /dev/null +++ b/dist/repositories/LikeRepository.js @@ -0,0 +1,31 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("../index")); +class LikeRepository { + async findLikeById(id) { + return index_1.default.like.findUnique({ where: { id } }); + } + async findLikes(options) { + return index_1.default.like.findMany(options); + } + async createLike(data) { + return index_1.default.like.create({ data }); + } + async deleteLike(id) { + return index_1.default.like.delete({ where: { id } }); + } + async findLikeByUserIdAndProductId(userId, productId) { + return index_1.default.like.findFirst({ + where: { userId, productId }, + }); + } + async findLikeByUserIdAndArticleId(userId, articleId) { + return index_1.default.like.findFirst({ + where: { userId, articleId }, + }); + } +} +exports.default = LikeRepository; diff --git a/dist/repositories/ProductRepository.js b/dist/repositories/ProductRepository.js new file mode 100644 index 000000000..06d9bb10b --- /dev/null +++ b/dist/repositories/ProductRepository.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("../index")); +class ProductRepository { + async findProductById(id) { + return index_1.default.product.findUnique({ where: { id } }); + } + async findProducts(options) { + return index_1.default.product.findMany(options); + } + async createProduct(data) { + return index_1.default.product.create({ data }); + } + async updateProduct(id, data) { + return index_1.default.product.update({ where: { id }, data }); + } + async deleteProduct(id) { + return index_1.default.product.delete({ where: { id } }); + } +} +exports.default = ProductRepository; diff --git a/dist/repositories/UserRepository.js b/dist/repositories/UserRepository.js new file mode 100644 index 000000000..aa2a2ecd2 --- /dev/null +++ b/dist/repositories/UserRepository.js @@ -0,0 +1,31 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("../index")); +class UserRepository { + async findUserById(id) { + return index_1.default.user.findUnique({ where: { id } }); + } + async findUserByEmail(email) { + return index_1.default.user.findUnique({ where: { email } }); + } + async createUser(data) { + return index_1.default.user.create({ data }); + } + async updateUser(id, data) { + return index_1.default.user.update({ where: { id }, data }); + } + async deleteUser(id) { + return index_1.default.user.delete({ where: { id } }); + } + async findProductsByUserId(userId) { + const userWithProducts = await index_1.default.user.findUnique({ + where: { id: userId }, + include: { products: true }, + }); + return userWithProducts ? userWithProducts.products : null; + } +} +exports.default = UserRepository; diff --git a/dist/routes/articles.router.js b/dist/routes/articles.router.js new file mode 100644 index 000000000..23aae2d50 --- /dev/null +++ b/dist/routes/articles.router.js @@ -0,0 +1,40 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const ArticlesController_1 = __importDefault(require("../controllers/ArticlesController")); +const ArticleService_1 = __importDefault(require("../ArticleService")); +const ArticleRepository_1 = __importDefault(require("../repositories/ArticleRepository")); +const auth_middleware_1 = __importDefault(require("../middlewares/auth.middleware")); +const optionalAuth_middleware_1 = __importDefault(require("../middlewares/optionalAuth.middleware")); +const validation_middleware_1 = require("../middlewares/validation.middleware"); +const router = (0, express_1.Router)(); +// Initialize repositories and services +const articleRepository = new ArticleRepository_1.default(); +const articleService = new ArticleService_1.default(articleRepository); +const articlesController = new ArticlesController_1.default(articleService); +//article registration +router + .route('/articles') + .post(auth_middleware_1.default, validation_middleware_1.validateArticle, articlesController.createArticle) + // 게시글 목록 조회 + .get(optionalAuth_middleware_1.default, articlesController.getArticles); +// article detail, modify, delete +router + .route('/articles/:articleId') + .get(optionalAuth_middleware_1.default, articlesController.getArticleById) + .patch(auth_middleware_1.default, validation_middleware_1.validateArticle, articlesController.updateArticle) + .delete(auth_middleware_1.default, articlesController.deleteArticle); +// article comment creation +router.post('/articles/:articleId/comments', auth_middleware_1.default, articlesController.createComment); +// article comments check +router.get('/articles/:articleId/comments', articlesController.getComments); +//article comment modify +router.patch('/articles/comments/:commentId', auth_middleware_1.default, articlesController.updateComment); +//article comment delete +router.delete('/articles/comments/:commentId', auth_middleware_1.default, articlesController.deleteComment); +// 게시글 좋아요 API +router.post('/:articleId/like', auth_middleware_1.default, articlesController.toggleLike); +exports.default = router; diff --git a/dist/routes/products.router.js b/dist/routes/products.router.js new file mode 100644 index 000000000..44b03a72f --- /dev/null +++ b/dist/routes/products.router.js @@ -0,0 +1,38 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const ProductsController_1 = __importDefault(require("../controllers/ProductsController")); +const ProductService_1 = __importDefault(require("../ProductService")); +const ProductRepository_1 = __importDefault(require("../repositories/ProductRepository")); +const auth_middleware_1 = __importDefault(require("../middlewares/auth.middleware")); +const optionalAuth_middleware_1 = __importDefault(require("../middlewares/optionalAuth.middleware")); +const validation_middleware_1 = require("../middlewares/validation.middleware"); +const router = (0, express_1.Router)(); +// Initialize repositories and services +const productRepository = new ProductRepository_1.default(); +const productService = new ProductService_1.default(productRepository); +const productsController = new ProductsController_1.default(productService); +// registration router +router.post('/products', auth_middleware_1.default, validation_middleware_1.validateProduct, productsController.createProduct); +// cherck router +router.get('/products', optionalAuth_middleware_1.default, productsController.getProducts); +// datail, modify, delete +router + .route('/products/:productId') + .get(optionalAuth_middleware_1.default, productsController.getProductById) + .patch(validation_middleware_1.validateProduct, auth_middleware_1.default, productsController.updateProduct) + .delete(auth_middleware_1.default, productsController.deleteProduct); +// comment +router.post('/products/:productId/comments', auth_middleware_1.default, productsController.createComment); +//comment check +router.get('/products/:productId/comments', productsController.getComments); +// comment modify +router.patch('/products/comments/:commentId', auth_middleware_1.default, productsController.updateComment); +// comment delete +router.delete('/products/comments/:commentId', auth_middleware_1.default, productsController.deleteComment); +// 상품 좋아요 API +router.post('/:productId/like', auth_middleware_1.default, productsController.toggleLike); +exports.default = router; diff --git a/dist/routes/upload.router.js b/dist/routes/upload.router.js new file mode 100644 index 000000000..5f9c3ef2c --- /dev/null +++ b/dist/routes/upload.router.js @@ -0,0 +1,21 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const UploadController_1 = __importDefault(require("../controllers/UploadController")); +const multer_1 = __importDefault(require("multer")); +const fs_1 = __importDefault(require("fs")); +const router = (0, express_1.Router)(); +//uploads 디렉토리가 없을 때 생성 +const uploadDir = 'uploads/'; +if (!fs_1.default.existsSync(uploadDir)) { + fs_1.default.mkdirSync(uploadDir); +} +const storage = multer_1.default.memoryStorage(); +const upload = (0, multer_1.default)({ storage: storage }); +const uploadController = new UploadController_1.default(); +//image api +router.post('/upload', upload.single('image'), uploadController.uploadImage); +exports.default = router; diff --git a/dist/routes/users.router.js b/dist/routes/users.router.js new file mode 100644 index 000000000..c3d2d9196 --- /dev/null +++ b/dist/routes/users.router.js @@ -0,0 +1,30 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const UsersController_1 = __importDefault(require("../controllers/UsersController")); +const UserService_1 = __importDefault(require("../UserService")); +const UserRepository_1 = __importDefault(require("../repositories/UserRepository")); +const auth_middleware_1 = __importDefault(require("../middlewares/auth.middleware")); +const router = (0, express_1.Router)(); +// Initialize repositories and services +const userRepository = new UserRepository_1.default(); +const userService = new UserService_1.default(userRepository); +const usersController = new UsersController_1.default(userService); +// 회원가입 API +router.post('/sign-up', usersController.signUp); +// 로그인 API +router.post('/sign-in', usersController.signIn); +// 내 정보 조회 API +router.get('/me', auth_middleware_1.default, usersController.getMe); +// 내 정보 수정 API +router.patch('/me', auth_middleware_1.default, usersController.updateMe); +// 비밀번호 변경 API +router.patch('/me/password', auth_middleware_1.default, usersController.changePassword); +// 내가 작성한 상품 목록 조회 API +router.get('/me/products', auth_middleware_1.default, usersController.getMyProducts); +// Token 재발급 API +router.post('/token/refresh', usersController.refreshToken); +exports.default = router; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..fba4ef16b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3274 @@ +{ + "name": "sprintmission3", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sprintmission3", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^6.16.1", + "bcrypt": "^6.0.0", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "pg": "^8.16.3", + "sharp": "^0.34.3" + }, + "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.5.2", + "@types/sharp": "^0.31.1", + "nodemon": "^3.1.10", + "prettier": "^3.6.2", + "prisma": "^6.16.1", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.9.2" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@prisma/client": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.1.tgz", + "integrity": "sha512-QaBCOY29lLAxEFFJgBPyW3WInCW52fJeQTmWx/h6YsP5u0bwuqP51aP0uhqFvhK9DaZPwvai/M4tSDYLVE9vRg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.1.tgz", + "integrity": "sha512-sz3uxRPNL62QrJ0EYiujCFkIGZ3hg+9hgC1Ae1HjoYuj0BxCqHua4JNijYvYCrh9LlofZDZcRBX3tHBfLvAngA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.1.tgz", + "integrity": "sha512-RWv/VisW5vJE4cDRTuAHeVedtGoItXTnhuLHsSlJ9202QKz60uiXWywBlVcqXVq8bFeIZoCoWH+R1duZJPwqLw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.1.tgz", + "integrity": "sha512-EOnEM5HlosPudBqbI+jipmaW/vQEaF0bKBo4gVkGabasINHR6RpC6h44fKZEqx4GD8CvH+einD2+b49DQrwrAg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.1", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/fetch-engine": "6.16.1", + "@prisma/get-platform": "6.16.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz", + "integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.1.tgz", + "integrity": "sha512-fl/PKQ8da5YTayw86WD3O9OmKJEM43gD3vANy2hS5S1CnfW2oPXk+Q03+gUWqcKK306QqhjjIHRFuTZ31WaosQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.1", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.1.tgz", + "integrity": "sha512-kUfg4vagBG7dnaGRcGd1c0ytQFcDj2SUABiuveIpL3bthFdTLI6PJeLEia6Q8Dgh+WhPdo0N2q0Fzjk63XTyaA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sharp": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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, + "license": "ISC", + "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==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "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/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "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==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "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==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.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==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "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", + "dev": true, + "license": "ISC", + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "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, + "license": "ISC" + }, + "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.", + "dev": true, + "license": "ISC", + "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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "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.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.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==", + "dev": true, + "license": "ISC", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "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/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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nypm": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz", + "integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.2.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.1.tgz", + "integrity": "sha512-MFkMU0eaDDKAT4R/By2IA9oQmwLTxokqv2wegAErr9Rf+oIe7W2sYpE/Uxq0H2DliIR7vnV63PkC1bEwUtl98w==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.16.1", + "@prisma/engines": "6.16.1" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "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.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.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, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "devOptional": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "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==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..0fdfb1991 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "sprintmission3", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "tsc", + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "start": "node dist/src/index.js", + "format": "prettier --write \"src/**/*.ts\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Jerang2/4-sprint-mission_fork.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "bugs": { + "url": "https://github.com/Jerang2/4-sprint-mission_fork/issues" + }, + "homepage": "https://github.com/Jerang2/4-sprint-mission_fork#readme", + "dependencies": { + "@prisma/client": "^6.16.1", + "bcrypt": "^6.0.0", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "pg": "^8.16.3", + "sharp": "^0.34.3" + }, + "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.5.2", + "@types/sharp": "^0.31.1", + "nodemon": "^3.1.10", + "prettier": "^3.6.2", + "prisma": "^6.16.1", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.9.2" + } +} diff --git a/prisma/index.js b/prisma/index.js new file mode 100644 index 000000000..22157413e --- /dev/null +++ b/prisma/index.js @@ -0,0 +1,37 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); +const path = require('path'); +const cookieParser = require('cookie-parser'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// router import +const productRouter = require('./routes/products.router.js'); +const articleRouter = require('./routes/articles.router.js'); +const uploadRouter = require('./routes/upload.router.js'); +const usersRouter = require('./routes/users.router.js'); + +// middleware +app.use(cors()); +app.use(express.json()); +app.use(cookieParser()); +app.use(express.urlencoded({ extended: true })); +app.use('/uploads', express.static(path.join(__dirname, 'uploads' +))); + +// 라우터 설정 +app.use('/api', [productRouter, articleRouter, uploadRouter, usersRouter]); + +// Error middleware +app.use((err, req, res, next) => { + console.error(err.stack); + const statusCode = err.statusCode || 500; + const message = err.message || '오류가 발생했습니다.'; + res.status(statusCode).json({ message }); +}); + +app.listen(PORT, () => { + console.log(`서버가 ${PORT}번에서 실행중입니다.`); +}); \ No newline at end of file diff --git a/prisma/migrations/20250817033539_init/migration.sql b/prisma/migrations/20250817033539_init/migration.sql new file mode 100644 index 000000000..6defc84c2 --- /dev/null +++ b/prisma/migrations/20250817033539_init/migration.sql @@ -0,0 +1,51 @@ +-- CreateTable +CREATE TABLE "public"."Product" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "price" INTEGER NOT NULL, + "tags" TEXT[], + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Article" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."ProductComment" ( + "id" SERIAL NOT NULL, + "comment" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "productId" INTEGER NOT NULL, + + CONSTRAINT "ProductComment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."ArticleComment" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "articleId" INTEGER NOT NULL, + + CONSTRAINT "ArticleComment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "public"."ProductComment" ADD CONSTRAINT "ProductComment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ArticleComment" ADD CONSTRAINT "ArticleComment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250914051507_modify/migration.sql b/prisma/migrations/20250914051507_modify/migration.sql new file mode 100644 index 000000000..aa2f5ad71 --- /dev/null +++ b/prisma/migrations/20250914051507_modify/migration.sql @@ -0,0 +1,48 @@ +/* + Warnings: + + - Added the required column `userId` to the `Article` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `ArticleComment` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `Product` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `ProductComment` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."Article" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "public"."ArticleComment" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "public"."Product" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "public"."ProductComment" ADD COLUMN "userId" INTEGER NOT NULL; + +-- CreateTable +CREATE TABLE "public"."User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "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") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email"); + +-- AddForeignKey +ALTER TABLE "public"."Product" ADD CONSTRAINT "Product_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Article" ADD CONSTRAINT "Article_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ProductComment" ADD CONSTRAINT "ProductComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ArticleComment" ADD CONSTRAINT "ArticleComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250914141855_prisma_schema/migration.sql b/prisma/migrations/20250914141855_prisma_schema/migration.sql new file mode 100644 index 000000000..7e65c4927 --- /dev/null +++ b/prisma/migrations/20250914141855_prisma_schema/migration.sql @@ -0,0 +1,131 @@ +/* + Warnings: + + - You are about to drop the `Article` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `ArticleComment` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Product` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `ProductComment` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "public"."Article" DROP CONSTRAINT "Article_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ArticleComment" DROP CONSTRAINT "ArticleComment_articleId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ArticleComment" DROP CONSTRAINT "ArticleComment_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."Product" DROP CONSTRAINT "Product_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ProductComment" DROP CONSTRAINT "ProductComment_productId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ProductComment" DROP CONSTRAINT "ProductComment_userId_fkey"; + +-- DropTable +DROP TABLE "public"."Article"; + +-- DropTable +DROP TABLE "public"."ArticleComment"; + +-- DropTable +DROP TABLE "public"."Product"; + +-- DropTable +DROP TABLE "public"."ProductComment"; + +-- DropTable +DROP TABLE "public"."User"; + +-- CreateTable +CREATE TABLE "public"."products" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "content" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'FOR_SALE', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "products_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."articles" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "articles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."comments" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + "productId" INTEGER, + "articleId" INTEGER, + + CONSTRAINT "comments_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."likes" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" INTEGER NOT NULL, + "productId" INTEGER, + "articleId" INTEGER, + + CONSTRAINT "likes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."users" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "nickname" TEXT NOT NULL, + "image" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "public"."users"("email"); + +-- AddForeignKey +ALTER TABLE "public"."products" ADD CONSTRAINT "products_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."articles" ADD CONSTRAINT "articles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."comments" ADD CONSTRAINT "comments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."comments" ADD CONSTRAINT "comments_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."products"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."comments" ADD CONSTRAINT "comments_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."articles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."likes" ADD CONSTRAINT "likes_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."likes" ADD CONSTRAINT "likes_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."products"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."likes" ADD CONSTRAINT "likes_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."articles"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250914162141_add_refresh_token/migration.sql b/prisma/migrations/20250914162141_add_refresh_token/migration.sql new file mode 100644 index 000000000..091164c26 --- /dev/null +++ b/prisma/migrations/20250914162141_add_refresh_token/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."users" ADD COLUMN "refreshToken" TEXT; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..044d57cdb --- /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 (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 000000000..3d8a36c70 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,82 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Product { + id Int @id @default(autoincrement()) + name String + content String + status String @default("FOR_SALE") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + comments Comment[] + likes Like[] + + @@map("products") +} + +model Article { + id Int @id @default(autoincrement()) + title String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + comments Comment[] + likes Like[] + + @@map("articles") +} + +model Comment { + id Int @id @default(autoincrement()) + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + productId Int? + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + articleId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + + @@map("comments") +} + +model Like { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + productId Int? + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + articleId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + + @@map("likes") +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + password String + nickname String + image String? + refreshToken String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + products Product[] + articles Article[] + comments Comment[] + likes Like[] + + @@map("users") +} diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 000000000..db6e1274f --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,65 @@ +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +async function main() { + // Create Products + const product1 = await prisma.product.create({ + data: { + name: '노트북', + description: '고성능 노트북입니다.', + price: 1500000, + tags: ['electronics', 'computer'], + }, + }); + + const product2 = await prisma.product.create({ + data: { + name: '키보드', + description: '기계식 키보드입니다.', + price: 100000, + tags: ['electronics', 'accessory'], + }, + }); + + // Create Articles + const article1 = await prisma.article.create({ + data: { + title: '첫 번째 게시글', + content: '이것은 첫 번째 게시글의 내용입니다.', + }, + }); + + const article2 = await prisma.article.create({ + data: { + title: '두 번째 게시글', + content: '이것은 두 번째 게시글의 내용입니다.', + }, + }); + + // Create Product Comments + await prisma.productComment.create({ + data: { + comment: '이 노트북 정말 좋네요!', + productId: product1.id, + }, + }); + + // Create Article Comments + await prisma.articleComment.create({ + data: { + content: '좋은 글 감사합니다.', + articleId: article1.id, + }, + }); + + console.log('Seeding finished.'); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/ArticleService.ts b/src/ArticleService.ts new file mode 100644 index 000000000..303e3c101 --- /dev/null +++ b/src/ArticleService.ts @@ -0,0 +1,48 @@ +import { Article as PrismaArticle, Prisma } from '@prisma/client'; +import ArticleRepository from './repositories/ArticleRepository'; +import { ArticleCreateDto, ArticleUpdateDto } from './dtos/ArticleDto'; + +interface ArticleCreateServiceInput { + title: string; + content: string; + userId: number; +} + +class ArticleService { + private articleRepository: ArticleRepository; + + constructor(articleRepository: ArticleRepository) { + this.articleRepository = articleRepository; + } + + async createArticle(data: ArticleCreateServiceInput): Promise { + const { userId, ...rest } = data; + return this.articleRepository.createArticle({ + ...rest, + user: { connect: { id: userId } }, + }); + } + + async getArticleById(id: number): Promise { + return this.articleRepository.findArticleById(id); + } + + async getArticles(options?: { + skip?: number; + take?: number; + where?: Prisma.ArticleWhereInput; + orderBy?: Prisma.ArticleOrderByWithRelationInput; + }): Promise { + return this.articleRepository.findArticles(options); + } + + async updateArticle(id: number, data: ArticleUpdateDto): Promise { + return this.articleRepository.updateArticle(id, data); + } + + async deleteArticle(id: number): Promise { + return this.articleRepository.deleteArticle(id); + } +} + +export default ArticleService; diff --git a/src/CommentService.ts b/src/CommentService.ts new file mode 100644 index 000000000..7873da3d8 --- /dev/null +++ b/src/CommentService.ts @@ -0,0 +1,54 @@ +import { Comment as PrismaComment, Prisma } from '@prisma/client'; +import CommentRepository from './repositories/CommentRepository'; + +interface CommentCreateInput { + content: string; + userId: number; + productId?: number; + articleId?: number; +} + +interface CommentUpdateInput { + content?: string; +} + +class CommentService { + private commentRepository: CommentRepository; + + constructor(commentRepository: CommentRepository) { + this.commentRepository = commentRepository; + } + + async createComment(data: CommentCreateInput): Promise { + const { userId, productId, articleId, ...rest } = data; + return this.commentRepository.createComment({ + ...rest, + user: { connect: { id: userId } }, + ...(productId && { product: { connect: { id: productId } } }), + ...(articleId && { article: { connect: { id: articleId } } }), + }); + } + + async getCommentById(id: number): Promise { + return this.commentRepository.findCommentById(id); + } + + async getComments(options?: { + skip?: number; + take?: number; + where?: Prisma.CommentWhereInput; + orderBy?: Prisma.CommentOrderByWithRelationInput; + }): Promise { + return this.commentRepository.findComments(options); + } + + async updateComment(id: number, data: CommentUpdateInput): Promise { + return this.commentRepository.updateComment(id, data); + } + + async deleteComment(id: number): Promise { + return this.commentRepository.deleteComment(id); + } +} + +export default CommentService; diff --git a/src/LikeService.ts b/src/LikeService.ts new file mode 100644 index 000000000..b44845c8e --- /dev/null +++ b/src/LikeService.ts @@ -0,0 +1,55 @@ +import { Like as PrismaLike, Prisma } from '@prisma/client'; +import LikeRepository from './repositories/LikeRepository'; + +interface LikeCreateInput { + userId: number; + productId?: number; + articleId?: number; +} + +class LikeService { + private likeRepository: LikeRepository; + + constructor(likeRepository: LikeRepository) { + this.likeRepository = likeRepository; + } + + async createLike(data: LikeCreateInput): Promise { + const { userId, productId, articleId, ...rest } = data; + return this.likeRepository.createLike({ + ...rest, + user: { connect: { id: userId } }, + ...(productId && { product: { connect: { id: productId } } }), + ...(articleId && { article: { connect: { id: articleId } } }), + }); + } + + async deleteLike(id: number): Promise { + return this.likeRepository.deleteLike(id); + } + + async findLikeByUserIdAndProductId( + userId: number, + productId: number, + ): Promise { + return this.likeRepository.findLikeByUserIdAndProductId(userId, productId); + } + + async findLikeByUserIdAndArticleId( + userId: number, + articleId: number, + ): Promise { + return this.likeRepository.findLikeByUserIdAndArticleId(userId, articleId); + } + + async findLikes(options?: { + skip?: number; + take?: number; + where?: Prisma.LikeWhereInput; + orderBy?: Prisma.LikeOrderByWithRelationInput; + }): Promise { + return this.likeRepository.findLikes(options); + } +} + +export default LikeService; diff --git a/src/ProductService.ts b/src/ProductService.ts new file mode 100644 index 000000000..25665f407 --- /dev/null +++ b/src/ProductService.ts @@ -0,0 +1,50 @@ +import { Product as PrismaProduct, Prisma } from '@prisma/client'; +import ProductRepository from './repositories/ProductRepository'; +import { ProductCreateDto, ProductUpdateDto } from './dtos/ProductDto'; + +interface ProductCreateServiceInput { + name: string; + content: string; + price: number; + userId: number; + status?: string; +} + +class ProductService { + private productRepository: ProductRepository; + + constructor(productRepository: ProductRepository) { + this.productRepository = productRepository; + } + + async createProduct(data: ProductCreateServiceInput): Promise { + const { userId, ...rest } = data; + return this.productRepository.createProduct({ + ...rest, + user: { connect: { id: userId } }, + }); + } + + async getProductById(id: number): Promise { + return this.productRepository.findProductById(id); + } + + async getProducts(options?: { + skip?: number; + take?: number; + where?: Prisma.ProductWhereInput; + orderBy?: Prisma.ProductOrderByWithRelationInput; + }): Promise { + return this.productRepository.findProducts(options); + } + + async updateProduct(id: number, data: ProductUpdateDto): Promise { + return this.productRepository.updateProduct(id, data); + } + + async deleteProduct(id: number): Promise { + return this.productRepository.deleteProduct(id); + } +} + +export default ProductService; diff --git a/src/UserService.ts b/src/UserService.ts new file mode 100644 index 000000000..522265ad8 --- /dev/null +++ b/src/UserService.ts @@ -0,0 +1,94 @@ +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import { User as PrismaUser, Product, Prisma } from '@prisma/client'; +import UserRepository from './repositories/UserRepository'; +import { UserCreateDto, UserUpdateDto } from './dtos/UserDto'; + +interface UserWithoutPassword extends Omit {} + +class UserService { + private userRepository: UserRepository; + + constructor(userRepository: UserRepository) { + this.userRepository = userRepository; + } + + // 회원가입 로직 + public signUp = async (userData: UserCreateDto): Promise => { + const { email, nickname, password } = userData; + + // 이메일 중복 확인 + const existingUser = await this.userRepository.findUserByEmail(email); + if (existingUser) { + throw new Error('이미 사용중인 이메일입니다.'); + } + + // 비밀번호 해싱 + const hashedPassword = await bcrypt.hash(password, 10); + + // 유저 생성 + const user = await this.userRepository.createUser({ + email, + nickname, + password: hashedPassword, + }); + + // 사용자 정보 반환 + const { password: _, refreshToken: __, ...userWithoutPassword } = user; + return userWithoutPassword; + }; + + public signIn = async ( + email: string, + password: string, + ): Promise<{ accessToken: string; refreshToken: string }> => { + // 이메일로 사용자 조회 + const user = await this.userRepository.findUserByEmail(email); + if (!user) { + throw new Error('존재하지 않는 이메일입니다.'); + } + + // 비밀번호 확인 + const isPasswordMatched = await bcrypt.compare(password, user.password); + if (!isPasswordMatched) { + throw new Error('비밀번호가 일치하지 않습니다.'); + } + + // Access Token 생성 (12시간) + const accessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET_KEY as string, { + expiresIn: '12h', + }); + + // Refresh Token 생성 (7일) + const refreshToken = jwt.sign( + { userId: user.id }, + process.env.REFRESH_TOKEN_SECRET_KEY as string, + { expiresIn: '7d' }, + ); + + // Refresh Token을 해싱해서 DB에 저장 + await this.userRepository.updateUser(user.id, { + refreshToken: await bcrypt.hash(refreshToken, 10), + }); + + return { accessToken, refreshToken }; + }; + + public getUserById = async (id: number): Promise => { + return this.userRepository.findUserById(id); + }; + + public updateUser = async (id: number, data: UserUpdateDto): Promise => { + return this.userRepository.updateUser(id, data); + }; + + public updatePassword = async (id: number, newPasswordHash: string): Promise => { + return this.userRepository.updateUser(id, { password: newPasswordHash }); + }; + + public getProductsByUserId = async (userId: number): Promise => { + return this.userRepository.findProductsByUserId(userId); + }; +} + +export default UserService; diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts new file mode 100644 index 000000000..14d13e421 --- /dev/null +++ b/src/controllers/ArticlesController.ts @@ -0,0 +1,296 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import ArticleService from '../ArticleService'; +import { Article as PrismaArticle, Prisma, Like } from '@prisma/client'; +import prisma from '../index'; +import { ArticleCreateDto, ArticleUpdateDto } from '../dtos/ArticleDto'; +import CommentService from '../CommentService'; +import LikeService from '../LikeService'; +import CommentRepository from '../repositories/CommentRepository'; +import LikeRepository from '../repositories/LikeRepository'; + +class ArticlesController { + private articleService: ArticleService; + private commentService: CommentService; + private likeService: LikeService; + + constructor(articleService: ArticleService) { + this.articleService = articleService; + const commentRepository = new CommentRepository(); + this.commentService = new CommentService(commentRepository); + const likeRepository = new LikeRepository(); + this.likeService = new LikeService(likeRepository); + } + + createArticle = async (req: Request, res: Response, next: NextFunction) => { + try { + const { title, content }: ArticleCreateDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.createArticle({ + title, + content, + userId: user.id, + }); + res.status(201).json(article); + } catch (error) { + next(error); + } + }; + + getArticles = async (req: Request, res: Response, next: NextFunction) => { + try { + const { sort, search } = req.query as { sort?: string; search?: string }; + let page = parseInt(req.query.page as string) || 1; + let limit = parseInt(req.query.limit as string) || 10; + let offset = (page - 1) * limit; + + const where: Prisma.ArticleWhereInput = search + ? { + OR: [ + { title: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ], + } + : {}; + + const user = req.user; + const articles = await this.articleService.getArticles({ + where, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + + let responseArticles: (PrismaArticle & { isLiked?: boolean })[] = articles; + + if (user) { + const articleIds = articles.map((article) => article.id); + const likes = await this.likeService.findLikes({ + where: { + userId: user.id, + articleId: { in: articleIds }, + }, + }); + + const likedArticleIds = new Set(likes.map((like: Like) => like.articleId)); + + responseArticles = articles.map((article) => ({ + ...article, + isLiked: likedArticleIds.has(article.id), + })); + } else { + responseArticles = articles.map((article) => ({ + ...article, + isLiked: false, + })); + } + + res.status(200).json(responseArticles); + } catch (error) { + next(error); + } + }; + + getArticleById = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const user = req.user; + + const article = await this.articleService.getArticleById(parseInt(articleId)); + + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); + } + + let isLiked = false; + if (user) { + const like = await this.likeService.findLikeByUserIdAndArticleId( + user.id, + parseInt(articleId), + ); + if (like) { + isLiked = true; + } + } + + const responseArticle = { ...article, isLiked }; + res.status(200).json(responseArticle); + } catch (error) { + next(error); + } + }; + + updateArticle = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { title, content }: ArticleUpdateDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 수정 권한이 없습니다.' }); + } + + const updatedArticle = await this.articleService.updateArticle(parseInt(articleId), { + title, + content, + }); + res.status(200).json(updatedArticle); + } catch (error) { + next(error); + } + }; + + deleteArticle = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); + } + + await this.articleService.deleteArticle(parseInt(articleId)); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + createComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.' }); + + const newComment = await this.commentService.createComment({ + content, + articleId: parseInt(articleId), + userId: user.id, + }); + res.status(201).json(newComment); + } catch (error) { + next(error); + } + }; + + getComments = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor as string) : undefined; + let limit = parseInt(req.query.limit as string) || 10; + + const comments = await this.commentService.getComments({ + where: { articleId: parseInt(articleId) }, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } catch (error) { + next(error); + } + }; + + updateComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); + + const comment = await this.commentService.getCommentById(parseInt(commentId)); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + + const updatedComment = await this.commentService.updateComment(parseInt(commentId), { + content, + }); + res.status(200).json(updatedComment); + } catch (error) { + next(error); + } + }; + + deleteComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const comment = await this.commentService.getCommentById(parseInt(commentId)); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); + } + + await this.commentService.deleteComment(parseInt(commentId)); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + toggleLike = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); + } + + const existingLike = await this.likeService.findLikeByUserIdAndArticleId( + user.id, + parseInt(articleId), + ); + + if (existingLike) { + await this.likeService.deleteLike(existingLike.id); + res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); + } else { + await this.likeService.createLike({ + userId: user.id, + articleId: parseInt(articleId), + }); + res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); + } + } catch (error) { + next(error); + } + }; +} + +export default ArticlesController; diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts new file mode 100644 index 000000000..8ce374d18 --- /dev/null +++ b/src/controllers/ProductsController.ts @@ -0,0 +1,296 @@ +import { Request, Response, NextFunction } from 'express'; +import ProductService from '../ProductService'; +import { Product as PrismaProduct, Prisma, Like } from '@prisma/client'; +import prisma from '../index'; +import { ProductCreateDto, ProductUpdateDto } from '../dtos/ProductDto'; +import CommentService from '../CommentService'; +import LikeService from '../LikeService'; +import CommentRepository from '../repositories/CommentRepository'; +import LikeRepository from '../repositories/LikeRepository'; + +class ProductsController { + private productService: ProductService; + private commentService: CommentService; + private likeService: LikeService; + + constructor(productService: ProductService) { + this.productService = productService; + const commentRepository = new CommentRepository(); + this.commentService = new CommentService(commentRepository); + const likeRepository = new LikeRepository(); + this.likeService = new LikeService(likeRepository); + } + + createProduct = async (req: Request, res: Response, next: NextFunction) => { + try { + const { name, description, price }: ProductCreateDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.createProduct({ + name, + content: description, // Map description to content + price, + userId: user.id, + }); + res.status(201).json(product); + } catch (error) { + next(error); + } + }; + + getProducts = async (req: Request, res: Response, next: NextFunction) => { + try { + const { sort, search } = req.query as { sort?: string; search?: string }; + let page = parseInt(req.query.page as string) || 1; + let limit = parseInt(req.query.limit as string) || 10; + let offset = (page - 1) * limit; + + const where: Prisma.ProductWhereInput = search + ? { + OR: [ + { name: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ], + } + : {}; + + const user = req.user; + const products = await this.productService.getProducts({ + where, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + + let responseProducts: (PrismaProduct & { isLiked?: boolean })[] = products; + + if (user) { + const productIds = products.map((product) => product.id); + const likes = await this.likeService.findLikes({ + where: { + userId: user.id, + productId: { in: productIds }, + }, + }); + + const likedProductIds = new Set(likes.map((like: Like) => like.productId)); + + responseProducts = products.map((product) => ({ + ...product, + isLiked: likedProductIds.has(product.id), + })); + } else { + responseProducts = products.map((product) => ({ + ...product, + isLiked: false, + })); + } + + res.status(200).json(responseProducts); + } catch (error) { + next(error); + } + }; + + getProductById = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const user = req.user; + + const product = await this.productService.getProductById(parseInt(productId)); + + if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.' }); + + let isLiked = false; + if (user) { + const like = await this.likeService.findLikeByUserIdAndProductId( + user.id, + parseInt(productId), + ); + if (like) { + isLiked = true; + } + } + + const responseProduct = { ...product, isLiked }; + res.status(200).json(responseProduct); + } catch (error) { + next(error); + } + }; + + updateProduct = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { name, description, price }: ProductUpdateDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.getProductById(parseInt(productId)); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 수정 권한이 없습니다.' }); + } + + const updatedProduct = await this.productService.updateProduct(parseInt(productId), { + name, + description, + price, + }); + res.status(200).json(updatedProduct); + } catch (error) { + next(error); + } + }; + + deleteProduct = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.getProductById(parseInt(productId)); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); + } + + await this.productService.deleteProduct(parseInt(productId)); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + createComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.' }); + + const newComment = await this.commentService.createComment({ + content, + productId: parseInt(productId), + userId: user.id, + }); + res.status(201).json(newComment); + } catch (error) { + next(error); + } + }; + + getComments = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor as string) : undefined; + let limit = parseInt(req.query.limit as string) || 10; + + const comments = await this.commentService.getComments({ + where: { productId: parseInt(productId) }, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } catch (error) { + next(error); + } + }; + + updateComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + + const existingComment = await this.commentService.getCommentById(parseInt(commentId)); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + + const updatedComment = await this.commentService.updateComment(parseInt(commentId), { + content, + }); + res.status(200).json(updatedComment); + } catch (error) { + next(error); + } + }; + + deleteComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const existingComment = await this.commentService.getCommentById(parseInt(commentId)); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); + } + + await this.commentService.deleteComment(parseInt(commentId)); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + toggleLike = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.getProductById(parseInt(productId)); + if (!product) { + return res.status(404).json({ message: '상품을 찾을 수 없습니다.' }); + } + + const existingLike = await this.likeService.findLikeByUserIdAndProductId( + user.id, + parseInt(productId), + ); + + if (existingLike) { + await this.likeService.deleteLike(existingLike.id); + res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); + } else { + await this.likeService.createLike({ + userId: user.id, + productId: parseInt(productId), + }); + res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); + } + } catch (error) { + next(error); + } + }; +} + +export default ProductsController; diff --git a/src/controllers/UploadController.ts b/src/controllers/UploadController.ts new file mode 100644 index 000000000..739f82872 --- /dev/null +++ b/src/controllers/UploadController.ts @@ -0,0 +1,36 @@ +import { Request, Response, NextFunction } from 'express'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import sharp from 'sharp'; + +const uploadDir = 'uploads/'; +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir); +} + +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); + +class UploadController { + uploadImage = async (req: Request, res: Response, next: NextFunction) => { + if (!req.file) { + return res.status(400).json({ message: '이미지 파일이 필요합니다.' }); + } + + try { + const ext = path.extname(req.file.originalname); + const filename = Date.now() + ext; + const imagePath = path.join(uploadDir, filename); + + await sharp(req.file.buffer).resize({ width: 500 }).toFile(imagePath); + + const imageUrl = `/uploads/${filename}`; + res.status(201).json({ imageUrl: imageUrl }); + } catch (error) { + next(error); + } + }; +} + +export default UploadController; diff --git a/src/controllers/UsersController.ts b/src/controllers/UsersController.ts new file mode 100644 index 000000000..79aa6c772 --- /dev/null +++ b/src/controllers/UsersController.ts @@ -0,0 +1,232 @@ +import { Request, Response, NextFunction } from 'express'; +import UserService from '../UserService'; +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import prisma from '../index'; +import { + UserCreateDto, + UserSignInDto, + UserUpdateDto, + UserChangePasswordDto, +} from '../dtos/UserDto'; + +class UsersController { + private userService: UserService; + + constructor(userService: UserService) { + this.userService = userService; + } + + signUp = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, nickname, password }: UserCreateDto = req.body; + + if (!email || !nickname || !password) { + return res.status(400).json({ message: '모든 정보를 입력해주세요' }); + } + + const newUser = await this.userService.signUp({ email, nickname, password }); + + return res.status(201).json({ + message: '회원가입이 완료되었습니다.', + data: newUser, + }); + } catch (error) { + if (error instanceof Error) { + return res.status(409).json({ message: error.message }); + } + next(error); + } + }; + + signIn = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, password }: UserSignInDto = req.body; + + if (!email || !password) { + return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.' }); + } + const { accessToken, refreshToken } = await this.userService.signIn(email, password); + + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: false, + maxAge: 1000 * 60 * 60 * 24 * 7, + }); + + return res.status(200).json({ + message: '로그인에 성공했습니다.', + data: { accessToken }, + }); + } catch (error) { + if (error instanceof Error) { + return res.status(401).json({ message: error.message }); + } + next(error); + } + }; + + getMe = async (req: Request, res: Response, next: NextFunction) => { + try { + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + res.status(200).json({ + message: '내 정보 조회 성공', + data: { + id: user.id, + email: user.email, + nickname: user.nickname, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }); + } catch (error) { + next(error); + } + }; + + updateMe = async (req: Request, res: Response, next: NextFunction) => { + try { + const { nickname, image }: UserUpdateDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!nickname && !image) { + return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + } + + const updatedData: { nickname?: string; image?: string } = { + ...(nickname && { nickname }), + ...(image && { image }), + }; + + const updatedUser = await this.userService.updateUser(user.id, updatedData); + + res.status(200).json({ + message: '내 정보 수정에 성공했습니다.', + data: { + id: updatedUser.id, + email: updatedUser.email, + nickname: updatedUser.nickname, + image: updatedUser.image, + createdAt: updatedUser.createdAt, + updatedAt: updatedUser.updatedAt, + }, + }); + } catch (error) { + next(error); + } + }; + + changePassword = async (req: Request, res: Response, next: NextFunction) => { + try { + const { currentPassword, newPassword, confirmNewPassword }: UserChangePasswordDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!currentPassword || !newPassword || !confirmNewPassword) { + return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); + } + if (newPassword !== confirmNewPassword) { + return res + .status(400) + .json({ message: '새 비밀번호와 확인 비밀번호가 일치하지 않습니다.' }); + } + + const existingUser = await this.userService.getUserById(user.id); + if (!existingUser) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const isPasswordMatched = await bcrypt.compare(currentPassword, existingUser.password); + if (!isPasswordMatched) { + return res.status(401).json({ message: '현재 비밀번호가 일치하지 않습니다.' }); + } + + const hashedNewPassword = await bcrypt.hash(newPassword, 10); + + await this.userService.updatePassword(user.id, hashedNewPassword); + + res.status(200).json({ message: '비밀번호 변경이 완료되었습니다.' }); + } catch (error) { + next(error); + } + }; + + getMyProducts = async (req: Request, res: Response, next: NextFunction) => { + try { + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const products = await this.userService.getProductsByUserId(user.id); + + res.status(200).json({ + message: '내가 작성한 상품 목록 조회에 성공했습니다.', + data: products, + }); + } catch (error) { + next(error); + } + }; + + refreshToken = async (req: Request, res: Response, next: NextFunction) => { + try { + const { refreshToken } = req.cookies; + if (!refreshToken) { + return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); + } + + const decodedToken = jwt.verify( + refreshToken, + process.env.REFRESH_TOKEN_SECRET_KEY as string, + ) as { userId: number }; + const userId = decodedToken.userId; + + const user = await this.userService.getUserById(userId); + if (!user) { + return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); + } + + const isRefreshTokenMatched = await bcrypt.compare(refreshToken, user.refreshToken as string); + if (!isRefreshTokenMatched) { + return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); + } + + const newAccessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET_KEY as string, { + expiresIn: '12h', + }); + + return res.status(200).json({ + message: 'Access Token이 재발급되었습니다.', + data: { accessToken: newAccessToken }, + }); + } catch (error) { + if (error instanceof Error) { + if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { + return res + .status(401) + .json({ + message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 ', + }); + } + } + next(error); + } + }; +} + +export default UsersController; diff --git a/src/dtos/ArticleDto.ts b/src/dtos/ArticleDto.ts new file mode 100644 index 000000000..5cbaeb41c --- /dev/null +++ b/src/dtos/ArticleDto.ts @@ -0,0 +1,9 @@ +export interface ArticleCreateDto { + title: string; + content: string; +} + +export interface ArticleUpdateDto { + title?: string; + content?: string; +} diff --git a/src/dtos/ProductDto.ts b/src/dtos/ProductDto.ts new file mode 100644 index 000000000..576e1e38a --- /dev/null +++ b/src/dtos/ProductDto.ts @@ -0,0 +1,11 @@ +export interface ProductCreateDto { + name: string; + description: string; + price: number; +} + +export interface ProductUpdateDto { + name?: string; + description?: string; + price?: number; +} diff --git a/src/dtos/UserDto.ts b/src/dtos/UserDto.ts new file mode 100644 index 000000000..9ff995aef --- /dev/null +++ b/src/dtos/UserDto.ts @@ -0,0 +1,21 @@ +export interface UserCreateDto { + email: string; + nickname: string; + password: string; +} + +export interface UserSignInDto { + email: string; + password: string; +} + +export interface UserUpdateDto { + nickname?: string; + image?: string; +} + +export interface UserChangePasswordDto { + currentPassword: string; + newPassword: string; + confirmNewPassword: string; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..e93558336 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,39 @@ +import dotenv from 'dotenv'; +dotenv.config(); +import express, { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import path from 'path'; +import { PrismaClient } from '@prisma/client'; + +const app = express(); +const PORT = process.env.PORT || 3000; +const prisma = new PrismaClient(); + +// import router +import productRouter from './routes/products.router'; +import articleRouter from './routes/articles.router'; +import uploadRouter from './routes/upload.router'; +import usersRouter from './routes/users.router'; + +// Middleware +app.use(cors()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + +// route settitng +app.use('/api', [productRouter, articleRouter, uploadRouter, usersRouter]); + +// Error Handler Middleware +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + const statusCode = (err as any).statusCode || 500; + const message = err.message || '오류가 발생했습니다.'; + res.status(statusCode).json({ message }); +}); + +app.listen(PORT, () => { + console.log(`서버가 ${PORT}번에서 실행중입니다.`); +}); + +export default prisma; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 000000000..f657aadf8 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,2 @@ +import ProductService from './ProductService'; +import ArticleService from './ArticleService'; diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts new file mode 100644 index 000000000..6fa66b7d6 --- /dev/null +++ b/src/middlewares/auth.middleware.ts @@ -0,0 +1,60 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import prisma from '../index'; // Import prisma from index.ts +import { User as PrismaUser } from '@prisma/client'; + +interface DecodedToken { + userId: number; +} + +const authMiddleware = async (req: Request, res: Response, next: NextFunction) => { + try { + // 헤더에서 authorization 값 가져오기 + const { authorization } = req.headers; + if (!authorization) { + return res.status(401).json({ message: '인증 정보가 없습니다.' }); + } + + // authorization 값에서 토큰 추출하기 + const [tokenType, token] = authorization.split(' '); + if (tokenType !== 'Bearer') { + return res.status(401).json({ message: '지원하지 않는 인증 방식입니다.' }); + } + + // 토큰이 없을 경우 + if (!token) { + return res.status(401).json({ message: '인증 정보가 없습니다.' }); + } + + // 토큰 검증 (확인) + const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY as string) as DecodedToken; + const userId = decodedToken.userId; + + // 토큰 있는 userId로 조회 + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + if (!user) { + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } + + // 사용자 정보 저장 + req.user = user; + next(); + } catch (error) { + console.error('인증 미들웨어 에러:', error); + if (error instanceof Error) { + switch (error.name) { + case 'TokenExpiredError': + return res.status(401).json({ message: '인증 토큰이 만료되었습니다.' }); + case 'JsonWebTokenError': + return res.status(401).json({ message: '유효하지 않은 인증 토큰입니다.' }); + default: + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } + } + next(error); + } +}; + +export default authMiddleware; diff --git a/src/middlewares/optionalAuth.middleware.ts b/src/middlewares/optionalAuth.middleware.ts new file mode 100644 index 000000000..33cd7856a --- /dev/null +++ b/src/middlewares/optionalAuth.middleware.ts @@ -0,0 +1,39 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import prisma from '../index'; // Import prisma from index.ts + +interface DecodedToken { + userId: number; +} + +// 선택적 인증 미들웨어 +// 인증에 성공하면 사용자 정보를 추가하고 실패해도 에러없이 다음 미들웨어 진행 +const optionalAuthMiddleware = async (req: Request, res: Response, next: NextFunction) => { + try { + const { authorization } = req.headers; + if (!authorization) { + return next(); + } + + const [tokenType, token] = authorization.split(' '); + if (tokenType !== 'Bearer' || !token) { + return next(); + } + + const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY as string) as DecodedToken; + const user = await prisma.user.findUnique({ + where: { id: decodedToken.userId }, + }); + + if (user) { + req.user = user; + } + } catch (error) { + // 에러 발생 시에도 다음 미들웨어로 진행 (선택적 인증이므로) + console.error('선택적 인증 미들웨어 에러:', error); + } + + return next(); +}; + +export default optionalAuthMiddleware; diff --git a/src/middlewares/validation.middleware.ts b/src/middlewares/validation.middleware.ts new file mode 100644 index 000000000..5712e97a5 --- /dev/null +++ b/src/middlewares/validation.middleware.ts @@ -0,0 +1,22 @@ +import { Request, Response, NextFunction } from 'express'; + +//validation +export const validateProduct = (req: Request, res: Response, next: NextFunction) => { + const { name, description, price } = req.body; + if (!name || !description || price == null) { + return res.status(400).json({ message: '이름, 설명, 가격을 입력해야 합니다.' }); + } + if (typeof price !== 'number' || price <= 0) { + return res.status(400).json({ message: '가격은 0보다 커야합니다.' }); + } + next(); +}; + +//article validation +export const validateArticle = (req: Request, res: Response, next: NextFunction) => { + const { title, content } = req.body; + if (!title || !content) { + return res.status(400).json({ message: '제목, 내용을 입력해야 합니다.' }); + } + next(); +}; diff --git a/src/repositories/ArticleRepository.ts b/src/repositories/ArticleRepository.ts new file mode 100644 index 000000000..7055513ab --- /dev/null +++ b/src/repositories/ArticleRepository.ts @@ -0,0 +1,31 @@ +import prisma from '../index'; +import { Article as PrismaArticle, Prisma } from '@prisma/client'; + +class ArticleRepository { + async findArticleById(id: number): Promise { + return prisma.article.findUnique({ where: { id } }); + } + + async findArticles(options?: { + skip?: number; + take?: number; + where?: Prisma.ArticleWhereInput; + orderBy?: Prisma.ArticleOrderByWithRelationInput; + }): Promise { + return prisma.article.findMany(options); + } + + async createArticle(data: Prisma.ArticleCreateInput): Promise { + return prisma.article.create({ data }); + } + + async updateArticle(id: number, data: Prisma.ArticleUpdateInput): Promise { + return prisma.article.update({ where: { id }, data }); + } + + async deleteArticle(id: number): Promise { + return prisma.article.delete({ where: { id } }); + } +} + +export default ArticleRepository; diff --git a/src/repositories/CommentRepository.ts b/src/repositories/CommentRepository.ts new file mode 100644 index 000000000..4940b5949 --- /dev/null +++ b/src/repositories/CommentRepository.ts @@ -0,0 +1,31 @@ +import prisma from '../index'; +import { Comment as PrismaComment, Prisma } from '@prisma/client'; + +class CommentRepository { + async findCommentById(id: number): Promise { + return prisma.comment.findUnique({ where: { id } }); + } + + async findComments(options?: { + skip?: number; + take?: number; + where?: Prisma.CommentWhereInput; + orderBy?: Prisma.CommentOrderByWithRelationInput; + }): Promise { + return prisma.comment.findMany(options); + } + + async createComment(data: Prisma.CommentCreateInput): Promise { + return prisma.comment.create({ data }); + } + + async updateComment(id: number, data: Prisma.CommentUpdateInput): Promise { + return prisma.comment.update({ where: { id }, data }); + } + + async deleteComment(id: number): Promise { + return prisma.comment.delete({ where: { id } }); + } +} + +export default CommentRepository; diff --git a/src/repositories/LikeRepository.ts b/src/repositories/LikeRepository.ts new file mode 100644 index 000000000..2eca01fef --- /dev/null +++ b/src/repositories/LikeRepository.ts @@ -0,0 +1,45 @@ +import prisma from '../index'; +import { Like as PrismaLike, Prisma } from '@prisma/client'; + +class LikeRepository { + async findLikeById(id: number): Promise { + return prisma.like.findUnique({ where: { id } }); + } + + async findLikes(options?: { + skip?: number; + take?: number; + where?: Prisma.LikeWhereInput; + orderBy?: Prisma.LikeOrderByWithRelationInput; + }): Promise { + return prisma.like.findMany(options); + } + + async createLike(data: Prisma.LikeCreateInput): Promise { + return prisma.like.create({ data }); + } + + async deleteLike(id: number): Promise { + return prisma.like.delete({ where: { id } }); + } + + async findLikeByUserIdAndProductId( + userId: number, + productId: number, + ): Promise { + return prisma.like.findFirst({ + where: { userId, productId }, + }); + } + + async findLikeByUserIdAndArticleId( + userId: number, + articleId: number, + ): Promise { + return prisma.like.findFirst({ + where: { userId, articleId }, + }); + } +} + +export default LikeRepository; diff --git a/src/repositories/ProductRepository.ts b/src/repositories/ProductRepository.ts new file mode 100644 index 000000000..f65911eb8 --- /dev/null +++ b/src/repositories/ProductRepository.ts @@ -0,0 +1,31 @@ +import prisma from '../index'; +import { Product as PrismaProduct, Prisma } from '@prisma/client'; + +class ProductRepository { + async findProductById(id: number): Promise { + return prisma.product.findUnique({ where: { id } }); + } + + async findProducts(options?: { + skip?: number; + take?: number; + where?: Prisma.ProductWhereInput; + orderBy?: Prisma.ProductOrderByWithRelationInput; + }): Promise { + return prisma.product.findMany(options); + } + + async createProduct(data: Prisma.ProductCreateInput): Promise { + return prisma.product.create({ data }); + } + + async updateProduct(id: number, data: Prisma.ProductUpdateInput): Promise { + return prisma.product.update({ where: { id }, data }); + } + + async deleteProduct(id: number): Promise { + return prisma.product.delete({ where: { id } }); + } +} + +export default ProductRepository; diff --git a/src/repositories/UserRepository.ts b/src/repositories/UserRepository.ts new file mode 100644 index 000000000..173c2a58b --- /dev/null +++ b/src/repositories/UserRepository.ts @@ -0,0 +1,34 @@ +import prisma from '../index'; +import { User as PrismaUser, Prisma, Product } from '@prisma/client'; + +class UserRepository { + async findUserById(id: number): Promise { + return prisma.user.findUnique({ where: { id } }); + } + + async findUserByEmail(email: string): Promise { + return prisma.user.findUnique({ where: { email } }); + } + + async createUser(data: Prisma.UserCreateInput): Promise { + return prisma.user.create({ data }); + } + + async updateUser(id: number, data: Prisma.UserUpdateInput): Promise { + return prisma.user.update({ where: { id }, data }); + } + + async deleteUser(id: number): Promise { + return prisma.user.delete({ where: { id } }); + } + + async findProductsByUserId(userId: number): Promise { + const userWithProducts = await prisma.user.findUnique({ + where: { id: userId }, + include: { products: true }, + }); + return userWithProducts ? userWithProducts.products : null; + } +} + +export default UserRepository; diff --git a/src/routes/articles.router.ts b/src/routes/articles.router.ts new file mode 100644 index 000000000..003bf62ef --- /dev/null +++ b/src/routes/articles.router.ts @@ -0,0 +1,45 @@ +import { Router } from 'express'; +import ArticlesController from '../controllers/ArticlesController'; +import ArticleService from '../ArticleService'; +import ArticleRepository from '../repositories/ArticleRepository'; +import authMiddleware from '../middlewares/auth.middleware'; +import optionalAuthMiddleware from '../middlewares/optionalAuth.middleware'; +import { validateArticle } from '../middlewares/validation.middleware'; + +const router = Router(); + +// Initialize repositories and services +const articleRepository = new ArticleRepository(); +const articleService = new ArticleService(articleRepository); +const articlesController = new ArticlesController(articleService); + +//article registration +router + .route('/articles') + .post(authMiddleware, validateArticle, articlesController.createArticle) + // 게시글 목록 조회 + .get(optionalAuthMiddleware, articlesController.getArticles); + +// article detail, modify, delete +router + .route('/articles/:articleId') + .get(optionalAuthMiddleware, articlesController.getArticleById) + .patch(authMiddleware, validateArticle, articlesController.updateArticle) + .delete(authMiddleware, articlesController.deleteArticle); + +// article comment creation +router.post('/articles/:articleId/comments', authMiddleware, articlesController.createComment); + +// article comments check +router.get('/articles/:articleId/comments', articlesController.getComments); + +//article comment modify +router.patch('/articles/comments/:commentId', authMiddleware, articlesController.updateComment); + +//article comment delete +router.delete('/articles/comments/:commentId', authMiddleware, articlesController.deleteComment); + +// 게시글 좋아요 API +router.post('/:articleId/like', authMiddleware, articlesController.toggleLike); + +export default router; diff --git a/src/routes/products.router.ts b/src/routes/products.router.ts new file mode 100644 index 000000000..6401e47d8 --- /dev/null +++ b/src/routes/products.router.ts @@ -0,0 +1,44 @@ +import { Router } from 'express'; +import ProductsController from '../controllers/ProductsController'; +import ProductService from '../ProductService'; +import ProductRepository from '../repositories/ProductRepository'; +import authMiddleware from '../middlewares/auth.middleware'; +import optionalAuthMiddleware from '../middlewares/optionalAuth.middleware'; +import { validateProduct } from '../middlewares/validation.middleware'; + +const router = Router(); + +// Initialize repositories and services +const productRepository = new ProductRepository(); +const productService = new ProductService(productRepository); +const productsController = new ProductsController(productService); + +// registration router +router.post('/products', authMiddleware, validateProduct, productsController.createProduct); + +// cherck router +router.get('/products', optionalAuthMiddleware, productsController.getProducts); + +// datail, modify, delete +router + .route('/products/:productId') + .get(optionalAuthMiddleware, productsController.getProductById) + .patch(validateProduct, authMiddleware, productsController.updateProduct) + .delete(authMiddleware, productsController.deleteProduct); + +// comment +router.post('/products/:productId/comments', authMiddleware, productsController.createComment); + +//comment check +router.get('/products/:productId/comments', productsController.getComments); + +// comment modify +router.patch('/products/comments/:commentId', authMiddleware, productsController.updateComment); + +// comment delete +router.delete('/products/comments/:commentId', authMiddleware, productsController.deleteComment); + +// 상품 좋아요 API +router.post('/:productId/like', authMiddleware, productsController.toggleLike); + +export default router; diff --git a/src/routes/upload.router.ts b/src/routes/upload.router.ts new file mode 100644 index 000000000..f095013c8 --- /dev/null +++ b/src/routes/upload.router.ts @@ -0,0 +1,24 @@ +import { Router } from 'express'; +import UploadController from '../controllers/UploadController'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import sharp from 'sharp'; + +const router = Router(); + +//uploads 디렉토리가 없을 때 생성 +const uploadDir = 'uploads/'; +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir); +} + +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); + +const uploadController = new UploadController(); + +//image api +router.post('/upload', upload.single('image'), uploadController.uploadImage); + +export default router; diff --git a/src/routes/users.router.ts b/src/routes/users.router.ts new file mode 100644 index 000000000..cf2f54f96 --- /dev/null +++ b/src/routes/users.router.ts @@ -0,0 +1,35 @@ +import { Router } from 'express'; +import UsersController from '../controllers/UsersController'; +import UserService from '../UserService'; +import UserRepository from '../repositories/UserRepository'; +import authMiddleware from '../middlewares/auth.middleware'; + +const router = Router(); + +// Initialize repositories and services +const userRepository = new UserRepository(); +const userService = new UserService(userRepository); +const usersController = new UsersController(userService); + +// 회원가입 API +router.post('/sign-up', usersController.signUp); + +// 로그인 API +router.post('/sign-in', usersController.signIn); + +// 내 정보 조회 API +router.get('/me', authMiddleware, usersController.getMe); + +// 내 정보 수정 API +router.patch('/me', authMiddleware, usersController.updateMe); + +// 비밀번호 변경 API +router.patch('/me/password', authMiddleware, usersController.changePassword); + +// 내가 작성한 상품 목록 조회 API +router.get('/me/products', authMiddleware, usersController.getMyProducts); + +// Token 재발급 API +router.post('/token/refresh', usersController.refreshToken); + +export default router; diff --git a/src/types/express.d.ts b/src/types/express.d.ts new file mode 100644 index 000000000..90f7ec13b --- /dev/null +++ b/src/types/express.d.ts @@ -0,0 +1,9 @@ +import { User as PrismaUser } from '@prisma/client'; + +declare global { + namespace Express { + interface Request { + user?: PrismaUser; // Or a more specific subset of PrismaUser if only certain fields are always present + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..3ba669922 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file