diff --git a/exportSwagger.ts b/exportSwagger.ts new file mode 100644 index 0000000..1eb2837 --- /dev/null +++ b/exportSwagger.ts @@ -0,0 +1,62 @@ +import { registry } from '@/lib/swagger'; +import { OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi'; +import fs from 'fs'; +import path from 'path'; + +function recursiveFileSearch(dir: string, fileList: string[] = []): string[] { + const files = fs.readdirSync(dir); + files.forEach((file) => { + const filePath = path.join(dir, file); + if (fs.statSync(filePath).isDirectory()) { + recursiveFileSearch(filePath, fileList); + } else if (file.match(/swagger\.ts$/)) { + fileList.push(filePath); + } + }); + return fileList; +} + +async function exportSwagger() { + const filesToImport = recursiveFileSearch( + path.join(process.cwd(), 'src', 'app', 'api'), + ); + + await Promise.all( + filesToImport.map((file) => { + return import(file) + .then((module) => { + if (module.default) { + module.default(registry); + } + }) + .catch((error) => { + console.error(`Error importing ${file}:`, error); + }); + }), + ); + + await import('./src/app/api/validation'); + + const generator = new OpenApiGeneratorV3(registry.definitions); + const spec = generator.generateDocument({ + openapi: '3.0.0', + info: { + version: '1.0.0', + title: 'MeetUP', + description: 'API documentation for MeetUP application', + }, + }); + + const outputPath = path.join( + process.cwd(), + 'src', + 'generated', + 'swagger.json', + ); + fs.writeFileSync(outputPath, JSON.stringify(spec, null, 2), 'utf8'); + console.log(`Swagger JSON generated at ${outputPath}`); +} + +exportSwagger().catch((error) => { + console.error('Error exporting Swagger:', error); +}); diff --git a/next-swagger-doc.json b/next-swagger-doc.json deleted file mode 100644 index eec01cb..0000000 --- a/next-swagger-doc.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "apiFolder": "src/app/api", - "definition": { - "openapi": "3.0.0", - "info": { - "title": "MeetUP API", - "version": "1.0" - }, - "components": { - "schemas": { - "ErrorResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "default": false - }, - "message": { - "type": "string", - "description": "Error message" - } - } - }, - "ZodErrorResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "default": false - }, - "message": { - "type": "string", - "description": "Error message" - }, - "errors": { - "type": "array", - "items": { - "type": "object", - "properties": { - "path": { "type": "string" }, - "message": { "type": "string" } - } - } - } - } - }, - "User": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "first_name": { "type": "string" }, - "last_name": { "type": "string" }, - "email": { "type": "string", "format": "email" }, - "image": { "type": "string", "format": "uri" }, - "timezone": { "type": "string", "description": "User timezone" }, - "created_at": { "type": "string", "format": "date-time" }, - "updated_at": { "type": "string", "format": "date-time" } - } - }, - "PublicUser": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "first_name": { "type": "string" }, - "last_name": { "type": "string" }, - "image": { "type": "string", "format": "uri" }, - "timezone": { "type": "string", "description": "User timezone" } - } - }, - "SimpleUser": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" } - } - }, - "Participant": { - "type": "object", - "properties": { - "user": { - "$ref": "#/components/schemas/SimpleUser" - }, - "status": { "type": "string" } - } - }, - "Event": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "title": { "type": "string" }, - "description": { "type": "string" }, - "start_time": { "type": "string", "format": "date-time" }, - "end_time": { "type": "string", "format": "date-time" }, - "status": { "type": "string" }, - "location": { "type": "string" }, - "organizer": { - "$ref": "#/components/schemas/SimpleUser" - }, - "participants": { - "type": "array", - "items": { - "type": "object", - "properties": { - "user": { - "$ref": "#/components/schemas/SimpleUser" - }, - "status": { "type": "string" } - } - } - }, - "created_at": { "type": "string", "format": "date-time" }, - "updated_at": { "type": "string", "format": "date-time" } - } - } - } - }, - "security": [] - } -} diff --git a/package.json b/package.json index 79b1bf0..1f5bf19 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,11 @@ "prisma:db:push": "dotenv -e .env.local -- prisma db push", "prisma:migrate:reset": "dotenv -e .env.local -- prisma migrate reset", "dev_container": "docker compose -f docker-compose.dev.yml up --watch --build", - "swagger:generate": "next-swagger-doc-cli next-swagger-doc.json --output src/generated/swagger.json", + "swagger:generate": "ts-node -r tsconfig-paths/register exportSwagger.ts", "orval:generate": "orval" }, "dependencies": { + "@asteasolutions/zod-to-openapi": "^8.0.0-beta.4", "@auth/prisma-adapter": "^2.9.1", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", @@ -40,9 +41,8 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.511.0", - "next": "15.3.3", + "next": "15.4.0-canary.85", "next-auth": "^5.0.0-beta.25", - "next-swagger-doc": "^0.4.1", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -58,6 +58,7 @@ "@types/react": "19.1.8", "@types/react-dom": "19.1.6", "@types/swagger-ui-react": "^5", + "@types/webpack-env": "^1.18.8", "dotenv-cli": "8.0.0", "eslint": "9.29.0", "eslint-config-next": "15.3.3", @@ -67,8 +68,10 @@ "prettier": "3.5.3", "prisma": "6.9.0", "tailwindcss": "4.1.10", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", "tw-animate-css": "1.3.4", - "typescript": "5.8.3" + "typescript": "^5.8.3" }, "packageManager": "yarn@4.9.2" } diff --git a/src/app/api/event/[eventID]/participant/[user]/route.ts b/src/app/api/event/[eventID]/participant/[user]/route.ts index 7ca75f9..890308c 100644 --- a/src/app/api/event/[eventID]/participant/[user]/route.ts +++ b/src/app/api/event/[eventID]/participant/[user]/route.ts @@ -1,97 +1,40 @@ import { prisma } from '@/prisma'; import { auth } from '@/auth'; -import { NextResponse } from 'next/server'; -import { z } from 'zod/v4'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { + ErrorResponseSchema, + SuccessResponseSchema, + ZodErrorResponseSchema, +} from '@/app/api/validation'; +import { + ParticipantResponseSchema, + updateParticipantSchema, +} from '../validation'; -const patchParticipantSchema = z.object({ - status: z.enum(['ACCEPTED', 'DECLINED', 'TENTATIVE', 'PENDING']), -}); - -/** - * @swagger - * /api/event/{eventID}/participant/{user}: - * get: - * summary: Get a specific participant's details in an event - * description: Returns the details of a specific participant in an event. - * tags: - * - Event_Participant - * parameters: - * - in: path - * name: eventID - * required: true - * schema: - * type: string - * description: The ID of the event. - * - in: path - * name: user - * required: true - * schema: - * type: string - * description: The ID or name of the user. - * responses: - * 200: - * description: Details of the participant. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * participant: - * $ref: "#/components/schemas/Participant" - * 401: - * description: Not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Not authenticated - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User not found - * 403: - * description: User is not a participant or organizer of this event. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User is not a participant or organizer of this event - */ export const GET = auth(async (req, { params }) => { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dbUser = await prisma.user.findUnique({ where: { - id: req.auth.user.id, + id: authCheck.user.id, }, }); - if (!dbUser) { - return NextResponse.json( + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User not found' }, { status: 404 }, ); - } const eventID = (await params).eventID; const user = (await params).user; @@ -110,15 +53,15 @@ export const GET = auth(async (req, { params }) => { }, }); - if (!isParticipant && !isOrganizer) { - return NextResponse.json( + if (!isParticipant && !isOrganizer) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User is not a participant or organizer of this event', }, { status: 403 }, ); - } const participant = await prisma.meetingParticipant.findUnique({ where: { @@ -132,110 +75,50 @@ export const GET = auth(async (req, { params }) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, status: true, }, }); - if (!participant) { - return NextResponse.json( + if (!participant) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'Participant not found' }, { status: 404 }, ); - } - return NextResponse.json({ + return returnZodTypeCheckedResponse(ParticipantResponseSchema, { success: true, participant, }); }); -/** - * @swagger - * /api/event/{eventID}/participant/{user}: - * delete: - * summary: Remove a participant from an event - * description: Removes a participant from an event. Only the organizer can remove participants. - * tags: - * - Event_Participant - * parameters: - * - in: path - * name: eventID - * required: true - * schema: - * type: string - * description: The ID of the event. - * - in: path - * name: user - * required: true - * schema: - * type: string - * description: The ID or name of the user to be removed. - * responses: - * 200: - * description: Participant removed successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * 401: - * description: Not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Not authenticated - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User not found - * 403: - * description: Only organizer can remove participants. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Only organizer can remove participants - */ export const DELETE = auth(async (req, { params }) => { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dbUser = await prisma.user.findUnique({ where: { - id: req.auth.user.id, + id: authCheck.user.id, }, }); - if (!dbUser) { - return NextResponse.json( + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User not found' }, { status: 404 }, ); - } const eventID = (await params).eventID; const user = (await params).user; @@ -247,12 +130,12 @@ export const DELETE = auth(async (req, { params }) => { }, }); - if (!isOrganizer) { - return NextResponse.json( + if (!isOrganizer) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'Only organizer can remove participants' }, { status: 403 }, ); - } const participant = await prisma.meetingParticipant.findUnique({ where: { @@ -263,12 +146,12 @@ export const DELETE = auth(async (req, { params }) => { }, }); - if (!participant) { - return NextResponse.json( + if (!participant) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'Participant not found' }, { status: 404 }, ); - } await prisma.meetingParticipant.delete({ where: { @@ -279,124 +162,44 @@ export const DELETE = auth(async (req, { params }) => { }, }); - return NextResponse.json({ - success: true, - message: 'Participant removed successfully', - }); + return returnZodTypeCheckedResponse( + SuccessResponseSchema, + { success: true, message: 'Participant removed successfully' }, + { status: 200 }, + ); }); -/** - * @swagger - * /api/event/{eventID}/participant/{user}: - * patch: - * summary: Update a participant's status in an event - * description: Updates the status of a participant in an event. Only the participant can update their own status. - * tags: - * - Event_Participant - * parameters: - * - in: path - * name: eventID - * required: true - * schema: - * type: string - * description: The ID of the event. - * - in: path - * name: user - * required: true - * schema: - * type: string - * description: The ID or name of the user whose status is being updated. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * enum: [accepted, declined, tentative] - * description: The new status of the participant. - * responses: - * 200: - * description: Participant status updated successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * participant: - * $ref: "#/components/schemas/Participant" - * 400: - * description: Invalid request data. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ZodErrorResponse" - * 401: - * description: Not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Not authenticated - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User not found - * 403: - * description: Only participant can update their status. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Only participant can update their status - */ export const PATCH = auth(async (req, { params }) => { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dbUser = await prisma.user.findUnique({ where: { - id: req.auth.user.id, + id: authCheck.user.id, }, }); - if (!dbUser) { - return NextResponse.json( + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User not found' }, { status: 404 }, ); - } const eventID = (await params).eventID; const user = (await params).user; - if (dbUser.id !== user && dbUser.name !== user) { - return NextResponse.json( + if (dbUser.id !== user && dbUser.name !== user) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'You can only update your own participation' }, { status: 403 }, ); - } const participant = await prisma.meetingParticipant.findUnique({ where: { @@ -410,23 +213,28 @@ export const PATCH = auth(async (req, { params }) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, status: true, }, }); - if (!participant) { - return NextResponse.json( + if (!participant) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'Participant not found' }, { status: 404 }, ); - } const body = await req.json(); - const parsedBody = patchParticipantSchema.safeParse(body); - if (!parsedBody.success) { - return NextResponse.json( + const parsedBody = await updateParticipantSchema.safeParseAsync(body); + if (!parsedBody.success) + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, { success: false, message: 'Invalid request body', @@ -434,10 +242,9 @@ export const PATCH = auth(async (req, { params }) => { }, { status: 400 }, ); - } const { status } = parsedBody.data; - await prisma.meetingParticipant.update({ + const updatedParticipant = await prisma.meetingParticipant.update({ where: { meeting_id_user_id: { meeting_id: eventID, @@ -447,10 +254,23 @@ export const PATCH = auth(async (req, { params }) => { data: { status, }, + select: { + user: { + select: { + id: true, + name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, + }, + }, + status: true, + }, }); - return NextResponse.json({ + return returnZodTypeCheckedResponse(ParticipantResponseSchema, { success: true, - participant, + participant: updatedParticipant, }); }); diff --git a/src/app/api/event/[eventID]/participant/[user]/swagger.ts b/src/app/api/event/[eventID]/participant/[user]/swagger.ts new file mode 100644 index 0000000..aaf0f5a --- /dev/null +++ b/src/app/api/event/[eventID]/participant/[user]/swagger.ts @@ -0,0 +1,102 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; +import { + ParticipantResponseSchema, + updateParticipantSchema, +} from '../validation'; +import { + invalidRequestDataResponse, + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; +import { + EventIdParamSchema, + UserIdParamSchema, + SuccessResponseSchema, +} from '@/app/api/validation'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/event/{eventID}/participant/{user}', + request: { + params: zod.object({ + eventID: EventIdParamSchema, + user: UserIdParamSchema, + }), + }, + responses: { + 200: { + description: 'Get a participant for the event', + content: { + 'application/json': { + schema: ParticipantResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event Participant'], + }); + + registry.registerPath({ + method: 'delete', + path: '/event/{eventID}/participant/{user}', + request: { + params: zod.object({ + eventID: EventIdParamSchema, + user: UserIdParamSchema, + }), + }, + responses: { + 200: { + description: 'Participant removed successfully', + content: { + 'application/json': { + schema: SuccessResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event Participant'], + }); + + registry.registerPath({ + method: 'patch', + path: '/event/{eventID}/participant/{user}', + request: { + params: zod.object({ + eventID: EventIdParamSchema, + user: UserIdParamSchema, + }), + body: { + content: { + 'application/json': { + schema: updateParticipantSchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'Participant updated successfully', + content: { + 'application/json': { + schema: ParticipantResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + ...invalidRequestDataResponse, + }, + tags: ['Event Participant'], + }); +} diff --git a/src/app/api/event/[eventID]/participant/route.ts b/src/app/api/event/[eventID]/participant/route.ts index 1a6783f..91ce965 100644 --- a/src/app/api/event/[eventID]/participant/route.ts +++ b/src/app/api/event/[eventID]/participant/route.ts @@ -1,94 +1,40 @@ import { prisma } from '@/prisma'; import { auth } from '@/auth'; -import { NextResponse } from 'next/server'; -import { z } from 'zod/v4'; -import { userIdSchema } from '@/lib/validation/user'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { + ErrorResponseSchema, + ZodErrorResponseSchema, +} from '@/app/api/validation'; +import { + inviteParticipantSchema, + ParticipantResponseSchema, + ParticipantsResponseSchema, +} from './validation'; -const postParticipantSchema = z.object({ - userId: userIdSchema, -}); - -/** - * @swagger - * /api/event/{eventID}/participant: - * get: - * summary: Get participants of an event - * description: Returns all participants of a specific event. - * tags: - * - Event_Participant - * parameters: - * - in: path - * name: eventID - * required: true - * schema: - * type: string - * description: The ID of the event. - * responses: - * 200: - * description: List of participants for the event. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * participants: - * type: array - * items: - * $ref: "#/components/schemas/Participant" - * 401: - * description: Not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Not authenticated - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User not found - * 403: - * description: User is not a participant or organizer of this event. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User is not a participant or organizer of this event - */ export const GET = auth(async (req, { params }) => { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dbUser = await prisma.user.findUnique({ where: { - id: req.auth.user.id, + id: authCheck.user.id, }, }); - if (!dbUser) { - return NextResponse.json( + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User not found' }, { status: 404 }, ); - } const eventID = (await params).eventID; @@ -97,6 +43,9 @@ export const GET = auth(async (req, { params }) => { meeting_id: eventID, user_id: dbUser.id, }, + select: { + status: true, + }, }); const isOrganizer = await prisma.meeting.findFirst({ @@ -104,17 +53,20 @@ export const GET = auth(async (req, { params }) => { id: eventID, organizer_id: dbUser.id, }, + select: { + id: true, + }, }); - if (!isParticipant && !isOrganizer) { - return NextResponse.json( + if (!isParticipant && !isOrganizer) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User is not a participant or organizer of this event', }, { status: 403 }, ); - } const participants = await prisma.meetingParticipant.findMany({ where: { @@ -125,122 +77,43 @@ export const GET = auth(async (req, { params }) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, status: true, }, }); - return NextResponse.json({ + return returnZodTypeCheckedResponse(ParticipantsResponseSchema, { success: true, participants, }); }); -/** - * @swagger - * /api/event/{eventID}/participant: - * post: - * summary: Add a participant to an event - * description: Adds a user as a participant to a specific event. - * tags: - * - Event_Participant - * parameters: - * - in: path - * name: eventID - * required: true - * schema: - * type: string - * description: The ID of the event. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * userId: - * type: string - * description: The ID of the user to add as a participant. - * responses: - * 200: - * description: Participant added successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * participant: - * $ref: "#/components/schemas/Participant" - * 400: - * description: Invalid request data. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ZodErrorResponse" - * 401: - * description: Not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Not authenticated - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User not found - * 403: - * description: Only organizers can add participants. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Only organizers can add participants - * 409: - * description: User is already a participant of this event. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User is already a participant of this event - */ export const POST = auth(async (req, { params }) => { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dbUser = await prisma.user.findUnique({ where: { - id: req.auth.user.id, + id: authCheck.user.id, }, }); - if (!dbUser) { - return NextResponse.json( + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User not found' }, { status: 404 }, ); - } const eventID = (await params).eventID; @@ -251,17 +124,18 @@ export const POST = auth(async (req, { params }) => { }, }); - if (!isOrganizer) { - return NextResponse.json( + if (!isOrganizer) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'Only organizers can add participants' }, { status: 403 }, ); - } const dataRaw = await req.json(); - const data = await postParticipantSchema.safeParseAsync(dataRaw); + const data = await inviteParticipantSchema.safeParseAsync(dataRaw); if (!data.success) { - return NextResponse.json( + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, { success: false, message: 'Invalid request data', @@ -270,41 +144,55 @@ export const POST = auth(async (req, { params }) => { { status: 400 }, ); } - const { userId } = data.data; + const { user_id } = data.data; const participantExists = await prisma.meetingParticipant.findFirst({ where: { meeting_id: eventID, - user_id: userId, + user_id, }, }); - if (participantExists) { - return NextResponse.json( + if (participantExists) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User is already a participant of this event', }, { status: 409 }, ); - } const newParticipant = await prisma.meetingParticipant.create({ data: { meeting_id: eventID, - user_id: userId, + user_id, }, - include: { - user: true, + select: { + user: { + select: { + id: true, + name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, + }, + }, + status: true, }, }); - return NextResponse.json({ + return returnZodTypeCheckedResponse(ParticipantResponseSchema, { success: true, participant: { user: { id: newParticipant.user.id, name: newParticipant.user.name, + first_name: newParticipant.user.first_name, + last_name: newParticipant.user.last_name, + image: newParticipant.user.image, + timezone: newParticipant.user.timezone, }, status: newParticipant.status, }, diff --git a/src/app/api/event/[eventID]/participant/swagger.ts b/src/app/api/event/[eventID]/participant/swagger.ts new file mode 100644 index 0000000..919cfda --- /dev/null +++ b/src/app/api/event/[eventID]/participant/swagger.ts @@ -0,0 +1,72 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; +import { + ParticipantsResponseSchema, + ParticipantResponseSchema, + inviteParticipantSchema, +} from './validation'; +import { + invalidRequestDataResponse, + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; +import { EventIdParamSchema } from '@/app/api/validation'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/event/{eventID}/participant', + request: { + params: zod.object({ + eventID: EventIdParamSchema, + }), + }, + responses: { + 200: { + description: 'List participants for the event', + content: { + 'application/json': { + schema: ParticipantsResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event Participant'], + }); + + registry.registerPath({ + method: 'post', + path: '/event/{eventID}/participant', + request: { + params: zod.object({ + eventID: EventIdParamSchema, + }), + body: { + content: { + 'application/json': { + schema: inviteParticipantSchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'Participant invited successfully', + content: { + 'application/json': { + schema: ParticipantResponseSchema, + }, + }, + }, + ...invalidRequestDataResponse, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event Participant'], + }); +} diff --git a/src/app/api/event/[eventID]/participant/validation.ts b/src/app/api/event/[eventID]/participant/validation.ts new file mode 100644 index 0000000..bacb9ac --- /dev/null +++ b/src/app/api/event/[eventID]/participant/validation.ts @@ -0,0 +1,50 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; +import { + existingUserIdServerSchema, + PublicUserSchema, +} from '@/app/api/user/validation'; + +extendZodWithOpenApi(zod); + +export const participantStatusSchema = zod.enum([ + 'ACCEPTED', + 'DECLINED', + 'TENTATIVE', + 'PENDING', +]); + +export const inviteParticipantSchema = zod + .object({ + user_id: existingUserIdServerSchema, + }) + .openapi('InviteParticipant', { + description: 'Schema for inviting a participant to an event', + }); + +export const updateParticipantSchema = zod + .object({ + status: participantStatusSchema, + }) + .openapi('UpdateParticipant', { + description: 'Schema for updating participant status in an event', + }); + +export const ParticipantSchema = zod + .object({ + user: PublicUserSchema, + status: participantStatusSchema, + }) + .openapi('Participant', { + description: 'Participant information including user and status', + }); + +export const ParticipantResponseSchema = zod.object({ + success: zod.boolean(), + participant: ParticipantSchema, +}); + +export const ParticipantsResponseSchema = zod.object({ + success: zod.boolean(), + participants: zod.array(ParticipantSchema), +}); diff --git a/src/app/api/event/[eventID]/route.ts b/src/app/api/event/[eventID]/route.ts index c42b957..807c31b 100644 --- a/src/app/api/event/[eventID]/route.ts +++ b/src/app/api/event/[eventID]/route.ts @@ -1,90 +1,48 @@ import { prisma } from '@/prisma'; import { auth } from '@/auth'; -import { NextResponse } from 'next/server'; -import { z } from 'zod/v4'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { + ErrorResponseSchema, + SuccessResponseSchema, + ZodErrorResponseSchema, +} from '../../validation'; +import { EventResponseSchema } from '../validation'; +import { updateEventSchema } from '../validation'; -const patchEventSchema = z.object({ - title: z.string().optional(), - description: z.string().optional(), - start_time: z.iso.datetime().optional(), - end_time: z.iso.datetime().optional(), - location: z.string().optional(), - status: z.enum(['TENTATIVE', 'CONFIRMED', 'CANCELLED']).optional(), -}); - -/** - * @swagger - * /api/event/{eventID}: - * get: - * summary: Get details of a specific event - * description: Returns the details of an event by its ID. - * tags: - * - Event - * parameters: - * - in: path - * name: eventID - * required: true - * schema: - * type: string - * description: The ID of the event to retrieve. - * responses: - * 200: - * description: Event details retrieved successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * event: - * $ref: "#/components/schemas/Event" - * 401: - * description: Not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Not authenticated - * 404: - * description: User or event not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - */ export const GET = auth(async (req, { params }) => { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dbUser = await prisma.user.findUnique({ where: { - id: req.auth.user.id, + id: authCheck.user.id, }, }); - if (!dbUser) { - return NextResponse.json( + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User not found' }, { status: 404 }, ); - } const eventID = (await params).eventID; const event = await prisma.meeting.findUnique({ where: { id: eventID, + OR: [ + { organizer_id: dbUser.id }, + { participants: { some: { user_id: dbUser.id } } }, + ], }, select: { id: true, @@ -101,6 +59,10 @@ export const GET = auth(async (req, { params }) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, participants: { @@ -109,6 +71,10 @@ export const GET = auth(async (req, { params }) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, status: true, @@ -117,120 +83,67 @@ export const GET = auth(async (req, { params }) => { }, }); - if (!event) { - return NextResponse.json( + if (!event) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'Event not found' }, { status: 404 }, ); - } - return NextResponse.json( - { - success: true, - event: event, - }, + return returnZodTypeCheckedResponse( + EventResponseSchema, + { success: true, event }, { status: 200 }, ); }); -/** - * @swagger - * /api/event/{eventID}: - * delete: - * summary: Delete a specific event - * description: Deletes an event by its ID if the user is the organizer. - * tags: - * - Event - * parameters: - * - in: path - * name: eventID - * required: true - * schema: - * type: string - * description: The ID of the event to delete. - * responses: - * 200: - * description: Event deleted successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * 401: - * description: Not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Not authenticated - * 403: - * description: User is not the organizer. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: You are not the organizer of this event - * 404: - * description: User or event not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - */ export const DELETE = auth(async (req, { params }) => { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dbUser = await prisma.user.findUnique({ where: { - id: req.auth.user.id, + id: authCheck.user.id, }, }); - if (!dbUser) { - return NextResponse.json( + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User not found' }, { status: 404 }, ); - } const eventID = (await params).eventID; const event = await prisma.meeting.findUnique({ where: { id: eventID, + OR: [ + { organizer_id: dbUser.id }, + { participants: { some: { user_id: dbUser.id } } }, + ], }, }); - if (!event) { - return NextResponse.json( + if (!event) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'Event not found' }, { status: 404 }, ); - } - if (event.organizer_id !== dbUser.id) { - return NextResponse.json( + if (event.organizer_id !== dbUser.id) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'You are not the organizer of this event' }, { status: 403 }, ); - } await prisma.meeting.delete({ where: { @@ -238,146 +151,66 @@ export const DELETE = auth(async (req, { params }) => { }, }); - return NextResponse.json( + return returnZodTypeCheckedResponse( + SuccessResponseSchema, { success: true, message: 'Event deleted successfully' }, { status: 200 }, ); }); -/** - * @swagger - * /api/event/{eventID}: - * patch: - * summary: Update a specific event - * description: Updates an event by its ID if the user is the organizer. - * tags: - * - Event - * parameters: - * - in: path - * name: eventID - * required: true - * schema: - * type: string - * description: The ID of the event to update. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * title: - * type: string - * description: - * type: string - * start_time: - * type: string - * format: date-time - * end_time: - * type: string - * format: date-time - * location: - * type: string - * status: - * type: string - * enum: [TENTATIVE, CONFIRMED, CANCELLED] - * responses: - * 200: - * description: Event updated successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * event: - * $ref: "#/components/schemas/Event" - * 400: - * description: Invalid request data. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ZodErrorResponse" - * 401: - * description: Not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Not authenticated - * 403: - * description: User is not the organizer. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: You are not the organizer of this event - * 404: - * description: User or event not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User or event not found - */ export const PATCH = auth(async (req, { params }) => { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dbUser = await prisma.user.findUnique({ where: { - id: req.auth.user.id, + id: authCheck.user.id, }, }); - if (!dbUser) { - return NextResponse.json( + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User not found' }, { status: 404 }, ); - } const eventID = (await params).eventID; const event = await prisma.meeting.findUnique({ where: { id: eventID, + OR: [ + { organizer_id: dbUser.id }, + { participants: { some: { user_id: dbUser.id } } }, + ], }, }); - if (!event) { - return NextResponse.json( + if (!event) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'Event not found' }, { status: 404 }, ); - } - if (event.organizer_id !== dbUser.id) { - return NextResponse.json( + if (event.organizer_id !== dbUser.id) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'You are not the organizer of this event' }, { status: 403 }, ); - } const dataRaw = await req.json(); - const data = await patchEventSchema.safeParseAsync(dataRaw); + const data = await updateEventSchema.safeParseAsync(dataRaw); if (!data.success) { - return NextResponse.json( + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, { success: false, message: 'Invalid input data', @@ -416,6 +249,10 @@ export const PATCH = auth(async (req, { params }) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, participants: { @@ -424,6 +261,10 @@ export const PATCH = auth(async (req, { params }) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, status: true, @@ -432,7 +273,8 @@ export const PATCH = auth(async (req, { params }) => { }, }); - return NextResponse.json( + return returnZodTypeCheckedResponse( + EventResponseSchema, { success: true, event: updatedEvent, diff --git a/src/app/api/event/[eventID]/swagger.ts b/src/app/api/event/[eventID]/swagger.ts new file mode 100644 index 0000000..a293775 --- /dev/null +++ b/src/app/api/event/[eventID]/swagger.ts @@ -0,0 +1,94 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { EventResponseSchema, updateEventSchema } from '../validation'; +import { + invalidRequestDataResponse, + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; +import { + EventIdParamSchema, + SuccessResponseSchema, +} from '@/app/api/validation'; +import zod from 'zod/v4'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/event/{eventID}', + request: { + params: zod.object({ + eventID: EventIdParamSchema, + }), + }, + responses: { + 200: { + description: 'Event retrieved successfully', + content: { + 'application/json': { + schema: EventResponseSchema, + }, + }, + }, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event'], + }); + + registry.registerPath({ + method: 'delete', + path: '/event/{eventID}', + request: { + params: zod.object({ + eventID: EventIdParamSchema, + }), + }, + responses: { + 200: { + description: 'Event deleted successfully', + content: { + 'application/json': { + schema: SuccessResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event'], + }); + + registry.registerPath({ + method: 'patch', + path: '/event/{eventID}', + request: { + params: zod.object({ + eventID: EventIdParamSchema, + }), + body: { + content: { + 'application/json': { + schema: updateEventSchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'Event updated successfully', + content: { + 'application/json': { + schema: EventResponseSchema, + }, + }, + }, + ...invalidRequestDataResponse, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event'], + }); +} diff --git a/src/app/api/event/route.ts b/src/app/api/event/route.ts index f9d0936..fb734b1 100644 --- a/src/app/api/event/route.ts +++ b/src/app/api/event/route.ts @@ -1,95 +1,34 @@ import { prisma } from '@/prisma'; import { auth } from '@/auth'; -import { NextResponse } from 'next/server'; -import { z } from 'zod/v4'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { ErrorResponseSchema, ZodErrorResponseSchema } from '../validation'; +import { + createEventSchema, + EventResponseSchema, + EventsResponseSchema, +} from './validation'; -const postEventSchema = z - .object({ - title: z.string().min(1, 'Title is required'), - description: z.string().optional(), - start_time: z.iso.datetime(), - end_time: z.iso.datetime(), - location: z.string().optional().default(''), - participants: z.array(z.string()).optional(), - }) - .refine((data) => new Date(data.start_time) < new Date(data.end_time), { - error: 'Start time must be before end time', - }) - .refine( - async (data) => - !data.participants || - (await Promise.all( - data.participants.map(async (userId) => { - const user = await prisma.user.findUnique({ where: { id: userId } }); - return !!user; - }), - ).then((results) => results.every(Boolean))), - { - error: 'One or more participant user IDs are invalid', - }, - ); - -/** - * @swagger - * /api/event: - * get: - * summary: Get all events for the authenticated user - * description: Returns all events where the user is an organizer or a participant. - * tags: - * - Event - * responses: - * 200: - * description: List of events for the user. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * events: - * type: array - * items: - * $ref: "#/components/schemas/Event" - * 401: - * description: Not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Not authenticated - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User not found - */ export const GET = auth(async (req) => { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dbUser = await prisma.user.findUnique({ where: { - id: req.auth.user.id, + id: authCheck.user.id, }, }); if (!dbUser) - return NextResponse.json( + return returnZodTypeCheckedResponse( + ErrorResponseSchema, { success: false, message: 'User not found' }, { status: 404 }, ); @@ -115,6 +54,10 @@ export const GET = auth(async (req) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, participants: { @@ -123,6 +66,10 @@ export const GET = auth(async (req) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, status: true, @@ -131,101 +78,30 @@ export const GET = auth(async (req) => { }, }); - return NextResponse.json({ - success: true, - events: userEvents, - }); + return returnZodTypeCheckedResponse( + EventsResponseSchema, + { + success: true, + events: userEvents, + }, + { status: 200 }, + ); }); -/** - * @swagger - * /api/event: - * post: - * summary: Create a new event - * description: Creates a new event as the authenticated user (organizer). - * tags: - * - Event - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - title - * - start_time - * - end_time - * properties: - * title: - * type: string - * description: - * type: string - * start_time: - * type: string - * format: date-time - * end_time: - * type: string - * format: date-time - * location: - * type: string - * participants: - * type: array - * items: - * type: string - * description: User ID of a participant - * responses: - * 200: - * description: Event created successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * event: - * $ref: "#/components/schemas/Event" - * 400: - * description: Invalid request data. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ZodErrorResponse" - * 401: - * description: Not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: Not authenticated - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: User not found - */ export const POST = auth(async (req) => { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dataRaw = await req.json(); - const data = await postEventSchema.safeParseAsync(dataRaw); + const data = await createEventSchema.safeParseAsync(dataRaw); if (!data.success) { - return NextResponse.json( + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, { success: false, message: 'Invalid request data', @@ -244,7 +120,7 @@ export const POST = auth(async (req) => { start_time, end_time, location, - organizer_id: req.auth.user.id, + organizer_id: authCheck.user.id!, participants: participants ? { create: participants.map((userId) => ({ @@ -267,6 +143,10 @@ export const POST = auth(async (req) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, participants: { @@ -275,6 +155,10 @@ export const POST = auth(async (req) => { select: { id: true, name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, }, }, status: true, @@ -283,8 +167,12 @@ export const POST = auth(async (req) => { }, }); - return NextResponse.json({ - success: true, - event: newEvent, - }); + return returnZodTypeCheckedResponse( + EventResponseSchema, + { + success: true, + event: newEvent, + }, + { status: 201 }, + ); }); diff --git a/src/app/api/event/swagger.ts b/src/app/api/event/swagger.ts new file mode 100644 index 0000000..b78afef --- /dev/null +++ b/src/app/api/event/swagger.ts @@ -0,0 +1,62 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { + EventResponseSchema, + EventsResponseSchema, + createEventSchema, +} from './validation'; +import { + invalidRequestDataResponse, + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/api/event', + responses: { + 200: { + description: 'List of events for the authenticated user', + content: { + 'application/json': { + schema: EventsResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event'], + }); + + registry.registerPath({ + method: 'post', + path: '/api/event', + request: { + body: { + content: { + 'application/json': { + schema: createEventSchema, + }, + }, + }, + }, + responses: { + 201: { + description: 'Event created successfully.', + content: { + 'application/json': { + schema: EventResponseSchema, + }, + }, + }, + ...invalidRequestDataResponse, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event'], + }); +} diff --git a/src/app/api/event/validation.ts b/src/app/api/event/validation.ts new file mode 100644 index 0000000..da5912f --- /dev/null +++ b/src/app/api/event/validation.ts @@ -0,0 +1,168 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; +import { + existingUserIdServerSchema, + PublicUserSchema, +} from '../user/validation'; +import { ParticipantSchema } from './[eventID]/participant/validation'; + +extendZodWithOpenApi(zod); + +// ---------------------------------------- +// +// Event ID Validation +// +// ---------------------------------------- +export const eventIdSchema = zod.string().min(1, 'Event ID is required'); + +// ---------------------------------------- +// +// Event Title Validation +// +// ---------------------------------------- +export const eventTitleSchema = zod + .string() + .min(1, 'Title is required') + .max(100, 'Title must be at most 100 characters long'); + +// ---------------------------------------- +// +// Event Description Validation +// +// ---------------------------------------- +export const eventDescriptionSchema = zod + .string() + .max(500, 'Description must be at most 500 characters long'); + +// ---------------------------------------- +// +// Event start time Validation +// +// ---------------------------------------- +export const eventStartTimeSchema = zod.iso + .datetime() + .or(zod.date().transform((date) => date.toISOString())) + .refine((date) => !isNaN(new Date(date).getTime()), { + message: 'Invalid start time', + }); + +// ---------------------------------------- +// +// Event end time Validation +// +// ---------------------------------------- +export const eventEndTimeSchema = zod.iso.datetime().or( + zod + .date() + .transform((date) => date.toISOString()) + .refine((date) => !isNaN(new Date(date).getTime()), { + message: 'Invalid end time', + }), +); + +// ---------------------------------------- +// +// Event Location Validation +// +// ---------------------------------------- +export const eventLocationSchema = zod + .string() + .max(200, 'Location must be at most 200 characters long'); + +// ---------------------------------------- +// +// Event Participants Validation +// +// ---------------------------------------- +export const eventParticipantsSchema = zod.array(existingUserIdServerSchema); + +// ---------------------------------------- +// +// Event Status Validation +// +// ---------------------------------------- +export const eventStatusSchema = zod.enum([ + 'TENTATIVE', + 'CONFIRMED', + 'CANCELLED', +]); + +// ---------------------------------------- +// +// Create Event Schema +// +// ---------------------------------------- +export const createEventSchema = zod + .object({ + title: eventTitleSchema, + description: eventDescriptionSchema.optional(), + start_time: eventStartTimeSchema, + end_time: eventEndTimeSchema, + location: eventLocationSchema.optional().default(''), + participants: eventParticipantsSchema.optional(), + status: eventStatusSchema.optional().default('TENTATIVE'), + }) + .refine((data) => new Date(data.start_time) < new Date(data.end_time), { + message: 'Start time must be before end time', + }); + +// ---------------------------------------- +// +// Update Event Schema +// +// ---------------------------------------- +export const updateEventSchema = zod + .object({ + id: eventIdSchema, + title: eventTitleSchema.optional(), + description: eventDescriptionSchema.optional(), + start_time: eventStartTimeSchema.optional(), + end_time: eventEndTimeSchema.optional(), + location: eventLocationSchema.optional().default(''), + participants: eventParticipantsSchema.optional(), + status: eventStatusSchema.optional(), + }) + .refine( + (data) => { + if (data.start_time && data.end_time) { + return new Date(data.start_time) < new Date(data.end_time); + } + return true; + }, + { + message: 'Start time must be before end time', + }, + ); + +// ---------------------------------------- +// +// Event Schema Validation (for API responses) +// +// ---------------------------------------- +export const EventSchema = zod + .object({ + id: eventIdSchema, + title: eventTitleSchema, + description: eventDescriptionSchema.nullish(), + start_time: eventStartTimeSchema, + end_time: eventEndTimeSchema, + location: eventLocationSchema.nullish(), + status: eventStatusSchema, + created_at: zod.date(), + updated_at: zod.date(), + organizer: PublicUserSchema, + participants: zod.array(ParticipantSchema).nullish(), + }) + .openapi('Event', { + description: 'Event information including all fields', + }); + +export const EventResponseSchema = zod.object({ + success: zod.boolean(), + event: EventSchema, +}); + +export const EventsResponseSchema = zod.object({ + success: zod.boolean(), + events: zod.array(EventSchema), +}); diff --git a/src/app/api/search/user/route.ts b/src/app/api/search/user/route.ts index 5146acd..a8b6414 100644 --- a/src/app/api/search/user/route.ts +++ b/src/app/api/search/user/route.ts @@ -1,118 +1,29 @@ import { auth } from '@/auth'; -import { NextResponse } from 'next/server'; import { prisma } from '@/prisma'; -import { z } from 'zod/v4'; +import { searchUserSchema, searchUserResponseSchema } from './validation'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { + ErrorResponseSchema, + ZodErrorResponseSchema, +} from '@/app/api/validation'; -const getSearchUserSchema = z.object({ - query: z.string().optional().default(''), - count: z.coerce.number().min(1).max(100).default(10), - page: z.coerce.number().min(1).default(1), - sort_by: z - .enum(['created_at', 'name', 'first_name', 'last_name', 'id']) - .optional() - .default('created_at'), - sort_order: z.enum(['asc', 'desc']).optional().default('desc'), -}); - -/** - * @swagger - * /api/search/user: - * get: - * summary: Search for users - * description: Search for users by name, first name, or last name with pagination and sorting. - * tags: - * - Search - * parameters: - * - in: query - * name: query - * required: false - * schema: - * type: string - * description: The search query to filter users by name, first name, or last name. - * - in: query - * name: count - * required: false - * schema: - * type: integer - * description: The number of users to return per page (default is 10). - * - in: query - * name: page - * required: false - * schema: - * type: integer - * description: The page number to return (default is 1). - * - in: query - * name: sort_by - * required: false - * schema: - * type: string - * enum: ["created_at", "name", "first_name", "last_name", "id"] - * description: The field to sort by (default is 'created_at'). - * - in: query - * name: sort_order - * required: false - * schema: - * type: string - * enum: ["asc", "desc"] - * description: The order to sort by, either 'asc' or 'desc' (default is 'desc'). - * responses: - * 200: - * description: Users found successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * users: - * type: array - * items: - * $ref: "#/components/schemas/PublicUser" - * count: - * type: integer - * description: The number of users returned. - * 400: - * description: Invalid request data. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ZodErrorResponse" - * 401: - * description: User is not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: "Not authenticated" - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: "User not found" - */ export const GET = auth(async function GET(req) { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dataRaw = Object.fromEntries(new URL(req.url).searchParams); - const data = await getSearchUserSchema.safeParseAsync(dataRaw); - if (!data.success) { - return NextResponse.json( + const data = await searchUserSchema.safeParseAsync(dataRaw); + if (!data.success) + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, { success: false, message: 'Invalid request data', @@ -120,7 +31,6 @@ export const GET = auth(async function GET(req) { }, { status: 400 }, ); - } const { query, count, page, sort_by, sort_order } = data.data; const dbUsers = await prisma.user.findMany({ @@ -156,9 +66,14 @@ export const GET = auth(async function GET(req) { }, }); - return NextResponse.json({ - success: true, - users: dbUsers, - count: userCount, - }); + return returnZodTypeCheckedResponse( + searchUserResponseSchema, + { + success: true, + users: dbUsers, + total_count: userCount, + total_pages: Math.ceil(userCount / count), + }, + { status: 200 }, + ); }); diff --git a/src/app/api/search/user/swagger.ts b/src/app/api/search/user/swagger.ts new file mode 100644 index 0000000..90ca54e --- /dev/null +++ b/src/app/api/search/user/swagger.ts @@ -0,0 +1,33 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { searchUserResponseSchema, searchUserSchema } from './validation'; +import { + invalidRequestDataResponse, + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/api/search/user', + request: { + query: searchUserSchema, + }, + responses: { + 200: { + description: 'User search results', + content: { + 'application/json': { + schema: searchUserResponseSchema, + }, + }, + }, + ...invalidRequestDataResponse, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Search'], + }); +} diff --git a/src/app/api/search/user/validation.ts b/src/app/api/search/user/validation.ts new file mode 100644 index 0000000..c1662b0 --- /dev/null +++ b/src/app/api/search/user/validation.ts @@ -0,0 +1,20 @@ +import zod from 'zod/v4'; +import { PublicUserSchema } from '../../user/validation'; + +export const searchUserSchema = zod.object({ + query: zod.string().optional().default(''), + count: zod.coerce.number().min(1).max(100).default(10), + page: zod.coerce.number().min(1).default(1), + sort_by: zod + .enum(['created_at', 'name', 'first_name', 'last_name', 'id']) + .optional() + .default('created_at'), + sort_order: zod.enum(['asc', 'desc']).optional().default('desc'), +}); + +export const searchUserResponseSchema = zod.object({ + success: zod.boolean(), + users: zod.array(PublicUserSchema), + total_count: zod.number(), + total_pages: zod.number(), +}); diff --git a/src/app/api/user/[user]/route.ts b/src/app/api/user/[user]/route.ts index f918592..5773f61 100644 --- a/src/app/api/user/[user]/route.ts +++ b/src/app/api/user/[user]/route.ts @@ -1,64 +1,19 @@ import { auth } from '@/auth'; -import { NextResponse } from 'next/server'; import { prisma } from '@/prisma'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { PublicUserResponseSchema } from '../validation'; +import { ErrorResponseSchema } from '@/app/api/validation'; -/** - * @swagger - * /api/user/{user}: - * get: - * summary: Get user information by ID or name - * description: Retrieve the information of a specific user by ID or name. - * tags: - * - User - * parameters: - * - in: path - * name: user - * required: true - * schema: - * type: string - * description: The ID or name of the user to retrieve. - * responses: - * 200: - * description: User information retrieved successfully. - * content: - * application/json: - * schema: - * type: "object" - * properties: - * success: - * type: "boolean" - * default: true - * user: - * $ref: "#/components/schemas/PublicUser" - * 401: - * description: User is not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: "Not authenticated" - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: "User not found" - */ export const GET = auth(async function GET(req, { params }) { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const requestedUser = (await params).user; @@ -79,13 +34,21 @@ export const GET = auth(async function GET(req, { params }) { }); if (!dbUser) - return NextResponse.json( - { success: false, message: 'User not found' }, + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'User not found', + }, { status: 404 }, ); - return NextResponse.json({ - success: true, - user: dbUser, - }); + return returnZodTypeCheckedResponse( + PublicUserResponseSchema, + { + success: true, + user: dbUser, + }, + { status: 200 }, + ); }); diff --git a/src/app/api/user/[user]/swagger.ts b/src/app/api/user/[user]/swagger.ts new file mode 100644 index 0000000..741cbf9 --- /dev/null +++ b/src/app/api/user/[user]/swagger.ts @@ -0,0 +1,33 @@ +import { PublicUserResponseSchema } from '../validation'; +import { + notAuthenticatedResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; +import { UserIdParamSchema } from '../../validation'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/api/user/{user}', + request: { + params: zod.object({ + user: UserIdParamSchema, + }), + }, + responses: { + 200: { + description: 'User information retrieved successfully.', + content: { + 'application/json': { + schema: PublicUserResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + }, + tags: ['User'], + }); +} diff --git a/src/app/api/user/me/route.ts b/src/app/api/user/me/route.ts index 592529e..5ba9792 100644 --- a/src/app/api/user/me/route.ts +++ b/src/app/api/user/me/route.ts @@ -1,78 +1,28 @@ import { auth } from '@/auth'; -import { NextResponse } from 'next/server'; import { prisma } from '@/prisma'; +import { updateUserServerSchema } from './validation'; import { - userEmailSchema, - userFirstNameSchema, - userNameSchema, - userLastNameSchema, -} from '@/lib/validation/user'; -import { z } from 'zod/v4'; + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { FullUserResponseSchema } from '../validation'; +import { + ErrorResponseSchema, + ZodErrorResponseSchema, +} from '@/app/api/validation'; -const patchUserMeSchema = z.object({ - name: userNameSchema.optional(), - first_name: userFirstNameSchema.optional(), - last_name: userLastNameSchema.optional(), - email: userEmailSchema.optional(), - image: z.string().optional(), - timezone: z.string().optional(), -}); - -/** - * @swagger - * /api/user/me: - * get: - * summary: Get the currently authenticated user's information - * description: Retrieve the information of the currently authenticated user. - * tags: - * - User - * responses: - * 200: - * description: User information retrieved successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * default: true - * user: - * $ref: "#/components/schemas/User" - * 401: - * description: User is not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: "Not authenticated" - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: "User not found" - */ export const GET = auth(async function GET(req) { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user || !req.auth.user.id) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dbUser = await prisma.user.findUnique({ where: { - id: req.auth.user.id, + id: authCheck.user.id, }, select: { id: true, @@ -87,109 +37,35 @@ export const GET = auth(async function GET(req) { }, }); if (!dbUser) - return NextResponse.json( - { success: false, message: 'User not found' }, + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'User not found', + }, { status: 404 }, ); - return NextResponse.json( - { - success: true, - user: { - ...dbUser, - }, - }, - { status: 200 }, - ); + return returnZodTypeCheckedResponse(FullUserResponseSchema, { + success: true, + user: dbUser, + }); }); -/** - * @swagger - * /api/user/me: - * patch: - * summary: Update the currently authenticated user's information - * description: Update the information of the currently authenticated user. - * tags: - * - User - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * description: Username of the user. - * first_name: - * type: string - * description: First name of the user. - * last_name: - * type: string - * description: Last name of the user. - * email: - * type: string - * description: Email address of the user. - * image: - * type: string - * description: URL of the user's profile image. - * timezone: - * type: string - * description: Timezone of the user. - * responses: - * 200: - * description: User information updated successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * default: true - * user: - * $ref: "#/components/schemas/User" - * 401: - * description: User is not authenticated. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: "Not authenticated" - * 404: - * description: User not found. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ErrorResponse" - * example: - * success: false - * message: "User not found" - * 400: - * description: Invalid request data. - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ZodErrorResponse" - */ export const PATCH = auth(async function PATCH(req) { - if (!req.auth) - return NextResponse.json( - { success: false, message: 'Not authenticated' }, - { status: 401 }, - ); - if (!req.auth.user) - return NextResponse.json( - { success: false, message: 'User not found' }, - { status: 404 }, + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, ); const dataRaw = await req.json(); - const data = await patchUserMeSchema.safeParseAsync(dataRaw); + const data = await updateUserServerSchema.safeParseAsync(dataRaw); if (!data.success) { - return NextResponse.json( + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, { success: false, message: 'Invalid request data', @@ -198,19 +74,19 @@ export const PATCH = auth(async function PATCH(req) { { status: 400 }, ); } - const { name, first_name, last_name, email, image, timezone } = data.data; + if (Object.keys(data.data).length === 0) { + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { success: false, message: 'No data to update' }, + { status: 400 }, + ); + } + const updatedUser = await prisma.user.update({ where: { - id: req.auth.user.id, - }, - data: { - name, - first_name, - last_name, - email, - image, - timezone, + id: authCheck.user.id, }, + data: data.data, select: { id: true, name: true, @@ -224,11 +100,16 @@ export const PATCH = auth(async function PATCH(req) { }, }); if (!updatedUser) - return NextResponse.json( - { success: false, message: 'User not found' }, + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'User not found', + }, { status: 404 }, ); - return NextResponse.json( + return returnZodTypeCheckedResponse( + FullUserResponseSchema, { success: true, user: updatedUser, diff --git a/src/app/api/user/me/swagger.ts b/src/app/api/user/me/swagger.ts new file mode 100644 index 0000000..e0a36a1 --- /dev/null +++ b/src/app/api/user/me/swagger.ts @@ -0,0 +1,63 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { FullUserResponseSchema } from '../validation'; +import { updateUserServerSchema } from './validation'; +import { + invalidRequestDataResponse, + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/api/user/me', + description: 'Get the currently authenticated user', + responses: { + 200: { + description: 'User information retrieved successfully', + content: { + 'application/json': { + schema: FullUserResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['User'], + }); + + registry.registerPath({ + method: 'patch', + path: '/api/user/me', + description: 'Update the currently authenticated user', + request: { + body: { + description: 'User information to update', + required: true, + content: { + 'application/json': { + schema: updateUserServerSchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'User information updated successfully', + content: { + 'application/json': { + schema: FullUserResponseSchema, + }, + }, + }, + ...invalidRequestDataResponse, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['User'], + }); +} diff --git a/src/app/api/user/me/validation.ts b/src/app/api/user/me/validation.ts new file mode 100644 index 0000000..49c6219 --- /dev/null +++ b/src/app/api/user/me/validation.ts @@ -0,0 +1,21 @@ +import zod from 'zod/v4'; +import { + firstNameSchema, + lastNameSchema, + newUserEmailServerSchema, + newUserNameServerSchema, +} from '@/app/api/user/validation'; + +// ---------------------------------------- +// +// Update User Validation +// +// ---------------------------------------- +export const updateUserServerSchema = zod.object({ + name: newUserNameServerSchema.optional(), + first_name: firstNameSchema.optional(), + last_name: lastNameSchema.optional(), + email: newUserEmailServerSchema.optional(), + image: zod.string().optional(), + timezone: zod.string().optional(), +}); diff --git a/src/app/api/user/validation.ts b/src/app/api/user/validation.ts new file mode 100644 index 0000000..79b1e7e --- /dev/null +++ b/src/app/api/user/validation.ts @@ -0,0 +1,149 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { prisma } from '@/prisma'; +import zod from 'zod/v4'; + +extendZodWithOpenApi(zod); + +// ---------------------------------------- +// +// Email Validation +// +// ---------------------------------------- +export const emailSchema = zod + .email('Invalid email address') + .min(3, 'Email is required'); + +export const newUserEmailServerSchema = emailSchema.refine(async (val) => { + const existingUser = await prisma.user.findUnique({ + where: { email: val }, + }); + return !existingUser; +}, 'Email in use by another account'); + +export const existingUserEmailServerSchema = emailSchema.refine(async (val) => { + const existingUser = await prisma.user.findUnique({ + where: { email: val }, + }); + return !!existingUser; +}, 'Email not found'); + +// ---------------------------------------- +// +// First Name Validation +// +// ---------------------------------------- +export const firstNameSchema = zod + .string() + .min(1, 'First name is required') + .max(32, 'First name must be at most 32 characters long'); + +// ---------------------------------------- +// +// Last Name Validation +// +// ---------------------------------------- +export const lastNameSchema = zod + .string() + .min(1, 'Last name is required') + .max(32, 'Last name must be at most 32 characters long'); + +// ---------------------------------------- +// +// Username Validation +// +// ---------------------------------------- +export const userNameSchema = zod + .string() + .min(3, 'Username is required') + .max(32, 'Username must be at most 32 characters long') + .regex( + /^[a-zA-Z0-9_]+$/, + 'Username can only contain letters, numbers, and underscores', + ); + +export const newUserNameServerSchema = userNameSchema.refine(async (val) => { + const existingUser = await prisma.user.findUnique({ + where: { name: val }, + }); + return !existingUser; +}, 'Username in use by another account'); + +export const existingUserNameServerSchema = userNameSchema.refine( + async (val) => { + const existingUser = await prisma.user.findUnique({ + where: { name: val }, + }); + return !!existingUser; + }, + 'Username not found', +); + +// ---------------------------------------- +// +// User ID Validation +// +// ---------------------------------------- +export const existingUserIdServerSchema = zod + .string() + .min(1, 'User ID is required') + .refine(async (val) => { + const user = await prisma.user.findUnique({ + where: { id: val }, + }); + return !!user; + }, 'User not found'); + +// ---------------------------------------- +// +// Password Validation +// +// ---------------------------------------- +export const passwordSchema = zod + .string() + .min(8, 'Password must be at least 8 characters long') + .max(128, 'Password must be at most 128 characters long') + .regex( + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+={}\[\]:;"'<>,.?\/\\-]).{8,}$/, + 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character', + ); + +// ---------------------------------------- +// +// User Schema Validation (for API responses) +// +// ---------------------------------------- +export const FullUserSchema = zod + .object({ + id: zod.string(), + name: zod.string(), + first_name: zod.string().nullish(), + last_name: zod.string().nullish(), + email: zod.email(), + image: zod.string().nullish(), + timezone: zod.string(), + created_at: zod.date(), + updated_at: zod.date(), + }) + .openapi('FullUser', { + description: 'Full user information including all fields', + }); + +export const PublicUserSchema = FullUserSchema.pick({ + id: true, + name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, +}).openapi('PublicUser', { + description: 'Public user information excluding sensitive data', +}); + +export const FullUserResponseSchema = zod.object({ + success: zod.boolean(), + user: FullUserSchema, +}); +export const PublicUserResponseSchema = zod.object({ + success: zod.boolean(), + user: PublicUserSchema, +}); diff --git a/src/app/api/validation.ts b/src/app/api/validation.ts new file mode 100644 index 0000000..38b95bd --- /dev/null +++ b/src/app/api/validation.ts @@ -0,0 +1,87 @@ +import { registry } from '@/lib/swagger'; +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; + +extendZodWithOpenApi(zod); + +export const ErrorResponseSchema = zod + .object({ + success: zod.boolean(), + message: zod.string(), + }) + .openapi('ErrorResponseSchema', { + description: 'Error response schema', + example: { + success: false, + message: 'An error occurred', + }, + }); + +export const ZodErrorResponseSchema = ErrorResponseSchema.extend({ + errors: zod.array( + zod.object({ + expected: zod.string().optional(), + code: zod.string(), + path: zod.array( + zod + .string() + .or(zod.number()) + .or( + zod.symbol().openapi({ + type: 'string', + }), + ), + ), + message: zod.string(), + }), + ), +}).openapi('ZodErrorResponseSchema', { + description: 'Zod error response schema', + example: { + success: false, + message: 'Invalid request data', + errors: [ + { + expected: 'string', + code: 'invalid_type', + path: ['first_name'], + message: 'Invalid input: expected string, received number', + }, + ], + }, +}); + +export const SuccessResponseSchema = zod + .object({ + success: zod.boolean(), + message: zod.string().optional(), + }) + .openapi('SuccessResponseSchema', { + description: 'Success response schema', + example: { + success: true, + message: 'Operation completed successfully', + }, + }); + +export const UserIdParamSchema = registry.registerParameter( + 'UserIdOrNameParam', + zod.string().openapi({ + param: { + name: 'user', + in: 'path', + }, + example: '12345', + }), +); + +export const EventIdParamSchema = registry.registerParameter( + 'EventIdParam', + zod.string().openapi({ + param: { + name: 'eventID', + in: 'path', + }, + example: '67890', + }), +); diff --git a/src/auth.ts b/src/auth.ts index 1f5fee8..405b729 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -8,7 +8,7 @@ import Authentik from 'next-auth/providers/authentik'; import { PrismaAdapter } from '@auth/prisma-adapter'; import { prisma } from '@/prisma'; -import { loginClientSchema } from './lib/validation/user'; +import { loginSchema } from '@/lib/auth/validation'; import { ZodError } from 'zod/v4'; class InvalidLoginError extends CredentialsSignin { @@ -37,7 +37,7 @@ const providers: Provider[] = [ if (process.env.DISABLE_PASSWORD_LOGIN) return null; try { - const { email, password } = await loginClientSchema.parseAsync(c); + const { email, password } = await loginSchema.parseAsync(c); const user = await prisma.user.findFirst({ where: { OR: [{ email }, { name: email }] }, diff --git a/src/components/user/login-form.tsx b/src/components/user/login-form.tsx index 39d42d8..1b9f8eb 100644 --- a/src/components/user/login-form.tsx +++ b/src/components/user/login-form.tsx @@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation'; import LabeledInput from '@/components/labeled-input'; import { Button } from '@/components/custom-ui/button'; import useZodForm from '@/lib/hooks/useZodForm'; -import { loginClientSchema, registerClientSchema } from '@/lib/validation/user'; +import { loginSchema, registerSchema } from '@/lib/auth/validation'; import { loginAction } from '@/lib/auth/login'; import { registerAction } from '@/lib/auth/register'; @@ -18,7 +18,7 @@ function LoginFormElement({ formRef?: React.RefObject; }) { const { handleSubmit, formState, register, setError } = - useZodForm(loginClientSchema); + useZodForm(loginSchema); const router = useRouter(); const onSubmit = handleSubmit(async (data) => { @@ -95,7 +95,7 @@ function RegisterFormElement({ formRef?: React.RefObject; }) { const { handleSubmit, formState, register, setError } = - useZodForm(registerClientSchema); + useZodForm(registerSchema); const onSubmit = handleSubmit(async (data) => { try { diff --git a/src/lib/apiHelpers.ts b/src/lib/apiHelpers.ts new file mode 100644 index 0000000..7f5c28e --- /dev/null +++ b/src/lib/apiHelpers.ts @@ -0,0 +1,38 @@ +import { NextAuthRequest } from 'next-auth'; +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; +import { NextResponse } from 'next/server'; + +extendZodWithOpenApi(zod); + +export function userAuthenticated(req: NextAuthRequest) { + if (!req.auth || !req.auth.user || !req.auth.user.id) + return { + continue: false, + response: { success: false, message: 'Not authenticated' }, + metadata: { status: 401 }, + } as const; + return { continue: true, user: req.auth.user } as const; +} + +export function returnZodTypeCheckedResponse< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Schema extends zod.ZodType, +>( + expectedType: Schema, + response: zod.input, + metadata?: { status: number }, +): NextResponse { + const result = expectedType.safeParse(response); + if (!result.success) { + return NextResponse.json( + { + success: false, + message: 'Invalid response format', + errors: result.error.issues, + }, + { status: 500 }, + ); + } + return NextResponse.json(result.data, { status: metadata?.status || 200 }); +} diff --git a/src/lib/auth/login.ts b/src/lib/auth/login.ts index 41df5b9..1c03356 100644 --- a/src/lib/auth/login.ts +++ b/src/lib/auth/login.ts @@ -1,10 +1,10 @@ 'use server'; import { z } from 'zod/v4'; -import { loginClientSchema } from '@/lib/validation/user'; +import { loginSchema } from './validation'; import { signIn } from '@/auth'; -export async function loginAction(data: z.infer) { +export async function loginAction(data: z.infer) { try { await signIn('credentials', { ...data, diff --git a/src/lib/auth/register.ts b/src/lib/auth/register.ts index 26cdea4..0ca8eb3 100644 --- a/src/lib/auth/register.ts +++ b/src/lib/auth/register.ts @@ -2,12 +2,14 @@ import type { z } from 'zod/v4'; import bcrypt from 'bcryptjs'; -import { registerSchema } from '@/lib/validation/user'; +import { registerServerSchema } from './validation'; import { prisma } from '@/prisma'; -export async function registerAction(data: z.infer) { +export async function registerAction( + data: z.infer, +) { try { - const result = await registerSchema.safeParseAsync(data); + const result = await registerServerSchema.safeParseAsync(data); if (!result.success) { return { diff --git a/src/lib/auth/validation.ts b/src/lib/auth/validation.ts new file mode 100644 index 0000000..50a6b23 --- /dev/null +++ b/src/lib/auth/validation.ts @@ -0,0 +1,53 @@ +import zod from 'zod/v4'; +import { + emailSchema, + firstNameSchema, + lastNameSchema, + newUserEmailServerSchema, + newUserNameServerSchema, + passwordSchema, + userNameSchema, +} from '@/app/api/user/validation'; + +// ---------------------------------------- +// +// Login Validation +// +// ---------------------------------------- +export const loginSchema = zod.object({ + email: emailSchema.or(userNameSchema), + password: zod.string().min(1, 'Password is required'), +}); + +// ---------------------------------------- +// +// Register Validation +// +// ---------------------------------------- +export const registerServerSchema = zod + .object({ + firstName: firstNameSchema, + lastName: lastNameSchema, + email: newUserEmailServerSchema, + password: passwordSchema, + confirmPassword: passwordSchema, + username: newUserNameServerSchema, + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords do not match', + path: ['confirmPassword'], + }); + +export const registerSchema = zod + .object({ + firstName: firstNameSchema, + lastName: lastNameSchema, + email: emailSchema, + password: passwordSchema, + confirmPassword: passwordSchema, + username: userNameSchema, + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords do not match', + path: ['confirmPassword'], + }); diff --git a/src/lib/defaultApiResponses.ts b/src/lib/defaultApiResponses.ts new file mode 100644 index 0000000..3e8a314 --- /dev/null +++ b/src/lib/defaultApiResponses.ts @@ -0,0 +1,60 @@ +import { + ErrorResponseSchema, + ZodErrorResponseSchema, +} from '@/app/api/validation'; + +export const invalidRequestDataResponse = { + 400: { + description: 'Invalid request data', + content: { + 'application/json': { + schema: ZodErrorResponseSchema, + }, + }, + }, +}; + +export const notAuthenticatedResponse = { + 401: { + description: 'Not authenticated', + content: { + 'application/json': { + schema: ErrorResponseSchema, + example: { + success: false, + message: 'Not authenticated', + }, + }, + }, + }, +}; + +export const userNotFoundResponse = { + 404: { + description: 'User not found', + content: { + 'application/json': { + schema: ErrorResponseSchema, + example: { + success: false, + message: 'User not found', + }, + }, + }, + }, +}; + +export const serverReturnedDataValidationErrorResponse = { + 500: { + description: 'Server returned data validation error', + content: { + 'application/json': { + schema: ZodErrorResponseSchema, + example: { + success: false, + message: 'Server returned data validation error', + }, + }, + }, + }, +}; diff --git a/src/lib/swagger.ts b/src/lib/swagger.ts index 2d49fc6..dc6436e 100644 --- a/src/lib/swagger.ts +++ b/src/lib/swagger.ts @@ -1,8 +1,36 @@ -import { createSwaggerSpec } from 'next-swagger-doc'; +import { + OpenAPIRegistry, + OpenApiGeneratorV3, +} from '@asteasolutions/zod-to-openapi'; -import swaggerConfig from '../../next-swagger-doc.json'; +export const registry = new OpenAPIRegistry(); export const getApiDocs = async () => { - const spec = createSwaggerSpec(swaggerConfig); - return spec; + const swaggerFiles = require.context('../app', true, /swagger\.ts$/); + + swaggerFiles + .keys() + .sort((a, b) => b.length - a.length) + .forEach((file) => { + console.log(`Registering Swagger file: ${file}`); + swaggerFiles(file).default?.(registry); + }); + // eslint-disable-next-line @typescript-eslint/no-require-imports + require('@/app/api/validation'); + + try { + const generator = new OpenApiGeneratorV3(registry.definitions); + const spec = generator.generateDocument({ + openapi: '3.0.0', + info: { + version: '1.0.0', + title: 'MeetUP', + description: 'API documentation for MeetUP application', + }, + }); + return spec; + } catch (error) { + console.error('Error generating API docs:', error); + throw new Error('Failed to generate API documentation'); + } }; diff --git a/src/lib/validation/user.ts b/src/lib/validation/user.ts deleted file mode 100644 index f95da45..0000000 --- a/src/lib/validation/user.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { prisma } from '@/prisma'; -import zod from 'zod/v4'; - -export const userEmailClientSchema = zod - .email('Invalid email address') - .min(3, 'Email is required'); - -export const userEmailSchema = userEmailClientSchema.refine(async (val) => { - const existingUser = await prisma.user.findUnique({ - where: { email: val }, - }); - return !existingUser; -}, 'Email in use by another account'); - -export const userFirstNameSchema = zod - .string() - .min(1, 'First name is required') - .max(32, 'First name must be at most 32 characters long'); - -export const userLastNameSchema = zod - .string() - .min(1, 'Last name is required') - .max(32, 'Last name must be at most 32 characters long'); - -export const userNameClientSchema = zod - .string() - .min(3, 'Username is required') - .max(32, 'Username must be at most 32 characters long') - .regex( - /^[a-zA-Z0-9_]+$/, - 'Username can only contain letters, numbers, and underscores', - ) - .refine((val) => !disallowedUsernames.includes(val?.toLowerCase() || ''), { - error: 'Username is not allowed', - }); - -export const userNameSchema = userNameClientSchema.refine(async (val) => { - const existingUser = await prisma.user.findUnique({ - where: { name: val }, - }); - return !existingUser; -}, 'Username in use by another account'); - -export const loginClientSchema = zod.object({ - email: userEmailClientSchema.or(userNameClientSchema), - password: zod.string().min(1, 'Password is required'), -}); - -export const userIdSchema = zod - .string() - .min(1, 'User ID is required') - .refine(async (val) => { - const user = await prisma.user.findUnique({ - where: { id: val }, - }); - return !!user; - }, 'User not found'); - -export const registerSchema = zod - .object({ - firstName: userFirstNameSchema, - lastName: userLastNameSchema, - email: userEmailSchema, - password: zod - .string() - .min(8, 'Password must be at least 8 characters long') - .max(128, 'Password must be at most 128 characters long'), - confirmPassword: zod - .string() - .min(8, 'Password must be at least 8 characters long') - .max(128, 'Password must be at most 128 characters long'), - username: userNameSchema, - }) - .refine((data) => data.password === data.confirmPassword, { - error: 'Passwords do not match', - path: ['confirmPassword'], - }) - .refine( - (data) => - !data.password.includes(data.firstName) && - !data.password.includes(data.lastName) && - !data.password.includes(data.email) && - !data.password.includes(data.username), - { - error: - 'Password cannot contain your first name, last name, email, or username', - path: ['password'], - }, - ); - -export const registerClientSchema = zod - .object({ - firstName: userFirstNameSchema, - lastName: userLastNameSchema, - email: userEmailClientSchema, - password: zod - .string() - .min(8, 'Password must be at least 8 characters long') - .max(128, 'Password must be at most 128 characters long'), - confirmPassword: zod - .string() - .min(8, 'Password must be at least 8 characters long') - .max(128, 'Password must be at most 128 characters long'), - username: userNameClientSchema, - }) - .refine((data) => data.password === data.confirmPassword, { - error: 'Passwords do not match', - path: ['confirmPassword'], - }) - .refine( - (data) => - !data.password.includes(data.firstName) && - !data.password.includes(data.lastName) && - !data.password.includes(data.email) && - !data.password.includes(data.username), - { - error: - 'Password cannot contain your first name, last name, email, or username', - path: ['password'], - }, - ); - -export const disallowedUsernames = ['me', 'admin', 'search']; diff --git a/tsconfig.json b/tsconfig.json index c133409..251099e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,11 @@ "@/*": ["./src/*"] } }, + "ts-node": { + "compilerOptions": { + "module": "commonjs" + } + }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index 2b727c1..ca6ea01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,19 +33,7 @@ __metadata: languageName: node linkType: hard -"@apidevtools/json-schema-ref-parser@npm:^9.0.6": - version: 9.1.2 - resolution: "@apidevtools/json-schema-ref-parser@npm:9.1.2" - dependencies: - "@jsdevtools/ono": "npm:^7.1.3" - "@types/json-schema": "npm:^7.0.6" - call-me-maybe: "npm:^1.0.1" - js-yaml: "npm:^4.1.0" - checksum: 10c0/ebf952eb2e00bf0919f024e72897e047fd5012f0a9e47ac361873f6de0a733b9334513cdbc73205a6b43ac4a652b8c87f55e489c39b2d60bd0bc1cb2b411e218 - languageName: node - linkType: hard - -"@apidevtools/openapi-schemas@npm:^2.0.4, @apidevtools/openapi-schemas@npm:^2.1.0": +"@apidevtools/openapi-schemas@npm:^2.1.0": version: 2.1.0 resolution: "@apidevtools/openapi-schemas@npm:2.1.0" checksum: 10c0/f4aa0f9df32e474d166c84ef91bceb18fa1c4f44b5593879529154ef340846811ea57dc2921560f157f692262827d28d988dd6e19fb21f00320e9961964176b4 @@ -59,22 +47,6 @@ __metadata: languageName: node linkType: hard -"@apidevtools/swagger-parser@npm:10.0.3": - version: 10.0.3 - resolution: "@apidevtools/swagger-parser@npm:10.0.3" - dependencies: - "@apidevtools/json-schema-ref-parser": "npm:^9.0.6" - "@apidevtools/openapi-schemas": "npm:^2.0.4" - "@apidevtools/swagger-methods": "npm:^3.0.2" - "@jsdevtools/ono": "npm:^7.1.3" - call-me-maybe: "npm:^1.0.1" - z-schema: "npm:^5.0.1" - peerDependencies: - openapi-types: ">=7" - checksum: 10c0/3b43f719c2d647ac8dcf30f132834d413ce21cbf7a8d9c3b35ec91149dd25d608c8fd892358fcd61a8edd8c5140a7fb13676f948e2d87067d081a47b8c7107e9 - languageName: node - linkType: hard - "@apidevtools/swagger-parser@npm:^10.1.1": version: 10.1.1 resolution: "@apidevtools/swagger-parser@npm:10.1.1" @@ -92,6 +64,17 @@ __metadata: languageName: node linkType: hard +"@asteasolutions/zod-to-openapi@npm:^8.0.0-beta.4": + version: 8.0.0-beta.4 + resolution: "@asteasolutions/zod-to-openapi@npm:8.0.0-beta.4" + dependencies: + openapi3-ts: "npm:^4.1.2" + peerDependencies: + zod: ~3.25.1 + checksum: 10c0/596a149bc1c132f640befc1a9c58ca31d651146ea83ee98861cf65bddca3be6dbb324d82179ef45717f49c4e73114e3f86213711671c9065eaf01d594b408ff2 + languageName: node + linkType: hard + "@asyncapi/specs@npm:^6.8.0": version: 6.8.1 resolution: "@asyncapi/specs@npm:6.8.1" @@ -152,6 +135,15 @@ __metadata: languageName: node linkType: hard +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 10c0/05c5368c13b662ee4c122c7bfbe5dc0b613416672a829f3e78bc49a357a197e0218d6e74e7c66cfcd04e15a179acab080bd3c69658c9fbefd0e1ccd950a07fc6 + languageName: node + linkType: hard + "@emnapi/core@npm:^1.4.3": version: 1.4.3 resolution: "@emnapi/core@npm:1.4.3" @@ -861,7 +853,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.1.0": +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e @@ -882,6 +874,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: 10c0/fa425b606d7c7ee5bfa6a31a7b050dd5814b4082f318e0e4190f991902181b4330f43f4805db1dd4f2433fd0ed9cc7a7b9c2683f1deeab1df1b0a98b1e24055b + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.24": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" @@ -937,10 +939,10 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:15.3.3": - version: 15.3.3 - resolution: "@next/env@npm:15.3.3" - checksum: 10c0/b47ef78c4194900f52a274270932a633ba21f39377fc6ad478839c3c1e3fffccb8ad25b286a1beb11f91fe9d09a299087ccb9c205a4e610ad95af65f24e49e5a +"@next/env@npm:15.4.0-canary.85": + version: 15.4.0-canary.85 + resolution: "@next/env@npm:15.4.0-canary.85" + checksum: 10c0/2a11c5530bd87edf993413044af5175f7e230d6db41145e3c56059a0cd1fc675b6150e1644b1e8731ff1c590927b4d6c27550ca1bafae3318fffd6483e75d1da languageName: node linkType: hard @@ -953,58 +955,58 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:15.3.3": - version: 15.3.3 - resolution: "@next/swc-darwin-arm64@npm:15.3.3" +"@next/swc-darwin-arm64@npm:15.4.0-canary.85": + version: 15.4.0-canary.85 + resolution: "@next/swc-darwin-arm64@npm:15.4.0-canary.85" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:15.3.3": - version: 15.3.3 - resolution: "@next/swc-darwin-x64@npm:15.3.3" +"@next/swc-darwin-x64@npm:15.4.0-canary.85": + version: 15.4.0-canary.85 + resolution: "@next/swc-darwin-x64@npm:15.4.0-canary.85" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:15.3.3": - version: 15.3.3 - resolution: "@next/swc-linux-arm64-gnu@npm:15.3.3" +"@next/swc-linux-arm64-gnu@npm:15.4.0-canary.85": + version: 15.4.0-canary.85 + resolution: "@next/swc-linux-arm64-gnu@npm:15.4.0-canary.85" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:15.3.3": - version: 15.3.3 - resolution: "@next/swc-linux-arm64-musl@npm:15.3.3" +"@next/swc-linux-arm64-musl@npm:15.4.0-canary.85": + version: 15.4.0-canary.85 + resolution: "@next/swc-linux-arm64-musl@npm:15.4.0-canary.85" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:15.3.3": - version: 15.3.3 - resolution: "@next/swc-linux-x64-gnu@npm:15.3.3" +"@next/swc-linux-x64-gnu@npm:15.4.0-canary.85": + version: 15.4.0-canary.85 + resolution: "@next/swc-linux-x64-gnu@npm:15.4.0-canary.85" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:15.3.3": - version: 15.3.3 - resolution: "@next/swc-linux-x64-musl@npm:15.3.3" +"@next/swc-linux-x64-musl@npm:15.4.0-canary.85": + version: 15.4.0-canary.85 + resolution: "@next/swc-linux-x64-musl@npm:15.4.0-canary.85" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:15.3.3": - version: 15.3.3 - resolution: "@next/swc-win32-arm64-msvc@npm:15.3.3" +"@next/swc-win32-arm64-msvc@npm:15.4.0-canary.85": + version: 15.4.0-canary.85 + resolution: "@next/swc-win32-arm64-msvc@npm:15.4.0-canary.85" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:15.3.3": - version: 15.3.3 - resolution: "@next/swc-win32-x64-msvc@npm:15.3.3" +"@next/swc-win32-x64-msvc@npm:15.4.0-canary.85": + version: 15.4.0-canary.85 + resolution: "@next/swc-win32-x64-msvc@npm:15.4.0-canary.85" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -2797,13 +2799,6 @@ __metadata: languageName: node linkType: hard -"@swc/counter@npm:0.1.3": - version: 0.1.3 - resolution: "@swc/counter@npm:0.1.3" - checksum: 10c0/8424f60f6bf8694cfd2a9bca45845bce29f26105cda8cf19cdb9fd3e78dc6338699e4db77a89ae449260bafa1cc6bec307e81e7fb96dbf7dcfce0eea55151356 - languageName: node - linkType: hard - "@swc/helpers@npm:0.5.15": version: 0.5.15 resolution: "@swc/helpers@npm:0.5.15" @@ -3013,6 +3008,34 @@ __metadata: languageName: node linkType: hard +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node10@npm:1.0.11" + checksum: 10c0/28a0710e5d039e0de484bdf85fee883bfd3f6a8980601f4d44066b0a6bcd821d31c4e231d1117731c4e24268bd4cf2a788a6787c12fc7f8d11014c07d582783c + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 10c0/dddca2b553e2bee1308a056705103fc8304e42bb2d2cbd797b84403a223b25c78f2c683ec3e24a095e82cd435387c877239bffcb15a590ba817cd3f6b9a99fd9 + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 10c0/67c1316d065fdaa32525bc9449ff82c197c4c19092b9663b23213c8cbbf8d88b6ed6a17898e0cbc2711950fbfaf40388938c1c748a2ee89f7234fc9e7fe2bf44 + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 10c0/05f8f2734e266fb1839eb1d57290df1664fe2aa3b0fdd685a9035806daa635f7519bf6d5d9b33f6e69dd545b8c46bd6e2b5c79acb2b1f146e885f7f11a42a5bb + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.9.0": version: 0.9.0 resolution: "@tybys/wasm-util@npm:0.9.0" @@ -3056,7 +3079,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7": +"@types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.7": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db @@ -3115,13 +3138,6 @@ __metadata: languageName: node linkType: hard -"@types/swagger-jsdoc@npm:6.0.4": - version: 6.0.4 - resolution: "@types/swagger-jsdoc@npm:6.0.4" - checksum: 10c0/fbe17d91a12e1e60a255b02e6def6877c81b356c75ffcd0e5167fbaf1476e2d6600cd7eea79e6b3e0ff7929dec33ade345147509ed3b98026f63c782b74514f6 - languageName: node - linkType: hard - "@types/swagger-ui-react@npm:^5": version: 5.18.0 resolution: "@types/swagger-ui-react@npm:5.18.0" @@ -3166,6 +3182,13 @@ __metadata: languageName: node linkType: hard +"@types/webpack-env@npm:^1.18.8": + version: 1.18.8 + resolution: "@types/webpack-env@npm:1.18.8" + checksum: 10c0/527a5d1eb75c5243e4f3665d956c7c340f899955dd25d16c9fd9750406f32e95a3a17d207640295038e8235c0c2a2daf084f420e088e58b965d82fc74f6012d7 + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version: 8.34.1 resolution: "@typescript-eslint/eslint-plugin@npm:8.34.1" @@ -3462,7 +3485,16 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.14.1, acorn@npm:^8.15.0": +"acorn-walk@npm:^8.1.1": + version: 8.3.4 + resolution: "acorn-walk@npm:8.3.4" + dependencies: + acorn: "npm:^8.11.0" + checksum: 10c0/76537ac5fb2c37a64560feaf3342023dadc086c46da57da363e64c6148dc21b57d49ace26f949e225063acb6fb441eabffd89f7a3066de5ad37ab3e328927c62 + languageName: node + linkType: hard + +"acorn@npm:^8.11.0, acorn@npm:^8.14.1, acorn@npm:^8.15.0, acorn@npm:^8.4.1": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -3581,6 +3613,13 @@ __metadata: languageName: node linkType: hard +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 10c0/070ff801a9d236a6caa647507bdcc7034530604844d64408149a26b9e87c2f97650055c0f049abd1efc024b334635c01f29e0b632b371ac3f26130f4cf65997a + languageName: node + linkType: hard + "argparse@npm:^1.0.10": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -3851,15 +3890,6 @@ __metadata: languageName: node linkType: hard -"busboy@npm:1.6.0": - version: 1.6.0 - resolution: "busboy@npm:1.6.0" - dependencies: - streamsearch: "npm:^1.1.0" - checksum: 10c0/fa7e836a2b82699b6e074393428b91ae579d4f9e21f5ac468e1b459a244341d722d2d22d10920cdd849743dbece6dca11d72de939fb75a7448825cf2babfba1f - languageName: node - linkType: hard - "cac@npm:^6.7.14": version: 6.7.14 resolution: "cac@npm:6.7.14" @@ -4003,16 +4033,6 @@ __metadata: languageName: node linkType: hard -"cleye@npm:1.3.2": - version: 1.3.2 - resolution: "cleye@npm:1.3.2" - dependencies: - terminal-columns: "npm:^1.4.1" - type-flag: "npm:^3.0.0" - checksum: 10c0/2cd63c194d8476230cb9730dae87ae106995ff36b5e43436965caf14ebc6386b7a33e61cc12f90f27e42f4490383fb6032f1f1956bbbc12a3bb13e5cc74cbd78 - languageName: node - linkType: hard - "client-only@npm:0.0.1": version: 0.0.1 resolution: "client-only@npm:0.0.1" @@ -4090,20 +4110,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:6.2.0": - version: 6.2.0 - resolution: "commander@npm:6.2.0" - checksum: 10c0/1b701c6726fc2b6c6a7d9ab017be9465153546a05767cdd0e15e9f9a11c07f88f64d47684b90b07e5fb103d173efb6afdf4a21f6d6c4c25f7376bd027d21062c - languageName: node - linkType: hard - -"commander@npm:^9.4.1": - version: 9.5.0 - resolution: "commander@npm:9.5.0" - checksum: 10c0/5f7784fbda2aaec39e89eb46f06a999e00224b3763dc65976e05929ec486e174fe9aac2655f03ba6a5e83875bd173be5283dc19309b7c65954701c02025b3c1d - languageName: node - linkType: hard - "compare-versions@npm:^6.1.1": version: 6.1.1 resolution: "compare-versions@npm:6.1.1" @@ -4134,6 +4140,13 @@ __metadata: languageName: node linkType: hard +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: 10c0/157cbc59b2430ae9a90034a5f3a1b398b6738bf510f713edc4d4e45e169bc514d3d99dd34d8d01ca7ae7830b5b8b537e46ae8f3c8f932371b0875c0151d7ec91 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -4291,6 +4304,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: 10c0/81b91f9d39c4eaca068eb0c1eb0e4afbdc5bb2941d197f513dd596b820b956fef43485876226d65d497bebc15666aa2aa82c679e84f65d5f2bfbf14ee46e32c1 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -4300,15 +4320,6 @@ __metadata: languageName: node linkType: hard -"doctrine@npm:3.0.0": - version: 3.0.0 - resolution: "doctrine@npm:3.0.0" - dependencies: - esutils: "npm:^2.0.2" - checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520 - languageName: node - linkType: hard - "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -5251,13 +5262,6 @@ __metadata: languageName: node linkType: hard -"fs.realpath@npm:^1.0.0": - version: 1.0.0 - resolution: "fs.realpath@npm:1.0.0" - checksum: 10c0/444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 - languageName: node - linkType: hard - "function-bind@npm:^1.1.2": version: 1.1.2 resolution: "function-bind@npm:1.1.2" @@ -5373,20 +5377,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.1.6": - version: 7.1.6 - resolution: "glob@npm:7.1.6" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^3.0.4" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10c0/2575cce9306ac534388db751f0aa3e78afedb6af8f3b529ac6b2354f66765545145dba8530abf7bff49fb399a047d3f9b6901c38ee4c9503f592960d9af67763 - languageName: node - linkType: hard - "glob@npm:^10.2.2": version: 10.4.5 resolution: "glob@npm:10.4.5" @@ -5648,17 +5638,7 @@ __metadata: languageName: node linkType: hard -"inflight@npm:^1.0.4": - version: 1.0.6 - resolution: "inflight@npm:1.0.6" - dependencies: - once: "npm:^1.3.0" - wrappy: "npm:1" - checksum: 10c0/7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 - languageName: node - linkType: hard - -"inherits@npm:2, inherits@npm:^2.0.1": +"inherits@npm:^2.0.1": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 @@ -5988,7 +5968,7 @@ __metadata: languageName: node linkType: hard -"isarray@npm:2.0.5, isarray@npm:^2.0.5": +"isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" checksum: 10c0/4199f14a7a13da2177c66c31080008b7124331956f47bca57dd0b6ea9f11687aa25e565a2c7a2b519bc86988d10398e3049a1f5df13c9f6b7664154690ae79fd @@ -6130,6 +6110,15 @@ __metadata: languageName: node linkType: hard +"json5@npm:^2.2.2": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 10c0/5a04eed94810fa55c5ea138b2f7a5c12b97c3750bc63d11e511dcecbfef758003861522a070c2272764ee0f4e3e323862f386945aeb5b85b87ee43f084ba586c + languageName: node + linkType: hard + "jsonc-parser@npm:~2.2.1": version: 2.2.1 resolution: "jsonc-parser@npm:2.2.1" @@ -6367,13 +6356,6 @@ __metadata: languageName: node linkType: hard -"lodash.get@npm:^4.4.2": - version: 4.4.2 - resolution: "lodash.get@npm:4.4.2" - checksum: 10c0/48f40d471a1654397ed41685495acb31498d5ed696185ac8973daef424a749ca0c7871bf7b665d5c14f5cc479394479e0307e781f61d5573831769593411be6e - languageName: node - linkType: hard - "lodash.isempty@npm:^4.4.0": version: 4.4.0 resolution: "lodash.isempty@npm:4.4.0" @@ -6381,13 +6363,6 @@ __metadata: languageName: node linkType: hard -"lodash.isequal@npm:^4.5.0": - version: 4.5.0 - resolution: "lodash.isequal@npm:4.5.0" - checksum: 10c0/dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f - languageName: node - linkType: hard - "lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" @@ -6395,13 +6370,6 @@ __metadata: languageName: node linkType: hard -"lodash.mergewith@npm:^4.6.2": - version: 4.6.2 - resolution: "lodash.mergewith@npm:4.6.2" - checksum: 10c0/4adbed65ff96fd65b0b3861f6899f98304f90fd71e7f1eb36c1270e05d500ee7f5ec44c02ef979b5ddbf75c0a0b9b99c35f0ad58f4011934c4d4e99e5200b3b5 - languageName: node - linkType: hard - "lodash.omitby@npm:^4.6.0": version: 4.6.0 resolution: "lodash.omitby@npm:4.6.0" @@ -6511,6 +6479,13 @@ __metadata: languageName: node linkType: hard +"make-error@npm:^1.1.1": + version: 1.3.6 + resolution: "make-error@npm:1.3.6" + checksum: 10c0/171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f + languageName: node + linkType: hard + "make-fetch-happen@npm:^14.0.3": version: 14.0.3 resolution: "make-fetch-happen@npm:14.0.3" @@ -6564,6 +6539,7 @@ __metadata: version: 0.0.0-use.local resolution: "meetup@workspace:." dependencies: + "@asteasolutions/zod-to-openapi": "npm:^8.0.0-beta.4" "@auth/prisma-adapter": "npm:^2.9.1" "@eslint/eslintrc": "npm:3.3.1" "@fortawesome/fontawesome-svg-core": "npm:^6.7.2" @@ -6588,6 +6564,7 @@ __metadata: "@types/react": "npm:19.1.8" "@types/react-dom": "npm:19.1.6" "@types/swagger-ui-react": "npm:^5" + "@types/webpack-env": "npm:^1.18.8" bcryptjs: "npm:^3.0.2" class-variance-authority: "npm:^0.7.1" clsx: "npm:^2.1.1" @@ -6596,9 +6573,8 @@ __metadata: eslint-config-next: "npm:15.3.3" eslint-config-prettier: "npm:10.1.5" lucide-react: "npm:^0.511.0" - next: "npm:15.3.3" + next: "npm:15.4.0-canary.85" next-auth: "npm:^5.0.0-beta.25" - next-swagger-doc: "npm:^0.4.1" next-themes: "npm:^0.4.6" orval: "npm:^7.10.0" postcss: "npm:8.5.6" @@ -6610,8 +6586,10 @@ __metadata: swagger-ui-react: "npm:^5.24.1" tailwind-merge: "npm:^3.2.0" tailwindcss: "npm:4.1.10" + ts-node: "npm:^10.9.2" + tsconfig-paths: "npm:^4.2.0" tw-animate-css: "npm:1.3.4" - typescript: "npm:5.8.3" + typescript: "npm:^5.8.3" zod: "npm:^3.25.60" languageName: unknown linkType: soft @@ -6672,7 +6650,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:3.1.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.2": +"minimatch@npm:3.1.2, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -6868,22 +6846,6 @@ __metadata: languageName: node linkType: hard -"next-swagger-doc@npm:^0.4.1": - version: 0.4.1 - resolution: "next-swagger-doc@npm:0.4.1" - dependencies: - "@types/swagger-jsdoc": "npm:6.0.4" - cleye: "npm:1.3.2" - isarray: "npm:2.0.5" - swagger-jsdoc: "npm:6.2.8" - peerDependencies: - next: ">=9" - bin: - next-swagger-doc-cli: dist/cli.js - checksum: 10c0/8ff0d33aad41296eb88f03243a627f9f0db528ea6889e528bbdbf66e38499069bfb9b7c29d2787c49f90f0b2a86a9c3e3475218d5af7a13dd4b0a37bec959703 - languageName: node - linkType: hard - "next-themes@npm:^0.4.6": version: 0.4.6 resolution: "next-themes@npm:0.4.6" @@ -6894,29 +6856,27 @@ __metadata: languageName: node linkType: hard -"next@npm:15.3.3": - version: 15.3.3 - resolution: "next@npm:15.3.3" +"next@npm:15.4.0-canary.85": + version: 15.4.0-canary.85 + resolution: "next@npm:15.4.0-canary.85" dependencies: - "@next/env": "npm:15.3.3" - "@next/swc-darwin-arm64": "npm:15.3.3" - "@next/swc-darwin-x64": "npm:15.3.3" - "@next/swc-linux-arm64-gnu": "npm:15.3.3" - "@next/swc-linux-arm64-musl": "npm:15.3.3" - "@next/swc-linux-x64-gnu": "npm:15.3.3" - "@next/swc-linux-x64-musl": "npm:15.3.3" - "@next/swc-win32-arm64-msvc": "npm:15.3.3" - "@next/swc-win32-x64-msvc": "npm:15.3.3" - "@swc/counter": "npm:0.1.3" + "@next/env": "npm:15.4.0-canary.85" + "@next/swc-darwin-arm64": "npm:15.4.0-canary.85" + "@next/swc-darwin-x64": "npm:15.4.0-canary.85" + "@next/swc-linux-arm64-gnu": "npm:15.4.0-canary.85" + "@next/swc-linux-arm64-musl": "npm:15.4.0-canary.85" + "@next/swc-linux-x64-gnu": "npm:15.4.0-canary.85" + "@next/swc-linux-x64-musl": "npm:15.4.0-canary.85" + "@next/swc-win32-arm64-msvc": "npm:15.4.0-canary.85" + "@next/swc-win32-x64-msvc": "npm:15.4.0-canary.85" "@swc/helpers": "npm:0.5.15" - busboy: "npm:1.6.0" caniuse-lite: "npm:^1.0.30001579" postcss: "npm:8.4.31" sharp: "npm:^0.34.1" styled-jsx: "npm:5.1.6" peerDependencies: "@opentelemetry/api": ^1.1.0 - "@playwright/test": ^1.41.2 + "@playwright/test": ^1.51.1 babel-plugin-react-compiler: "*" react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -6951,7 +6911,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 10c0/b519d348efd905ac63b2e5cb1e5a3d8e5d11d992aba436f4eef28c66f4555f155bb2bd489d0d029867e926539b31a3f14dd81b0ebca54ce9f3d65a883fb94d4b + checksum: 10c0/756f84e1412b690b7c4687f3da4984ed3134c571a5d1568dcf7e764e4328ed7789f1898b43490682021a3b27f38975804bc9770495ae495925aeb50cd706dfdb languageName: node linkType: hard @@ -7237,15 +7197,6 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0": - version: 1.4.0 - resolution: "once@npm:1.4.0" - dependencies: - wrappy: "npm:1" - checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 - languageName: node - linkType: hard - "onetime@npm:^5.1.2": version: 5.1.2 resolution: "onetime@npm:5.1.2" @@ -7282,7 +7233,7 @@ __metadata: languageName: node linkType: hard -"openapi3-ts@npm:4.4.0, openapi3-ts@npm:^4.2.2": +"openapi3-ts@npm:4.4.0, openapi3-ts@npm:^4.1.2, openapi3-ts@npm:^4.2.2": version: 4.4.0 resolution: "openapi3-ts@npm:4.4.0" dependencies: @@ -7414,13 +7365,6 @@ __metadata: languageName: node linkType: hard -"path-is-absolute@npm:^1.0.0": - version: 1.0.1 - resolution: "path-is-absolute@npm:1.0.1" - checksum: 10c0/127da03c82172a2a50099cddbf02510c1791fc2cc5f7713ddb613a56838db1e8168b121a920079d052e0936c23005562059756d653b7c544c53185efe53be078 - languageName: node - linkType: hard - "path-key@npm:^3.0.0, path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -8539,13 +8483,6 @@ __metadata: languageName: node linkType: hard -"streamsearch@npm:^1.1.0": - version: 1.1.0 - resolution: "streamsearch@npm:1.1.0" - checksum: 10c0/fbd9aecc2621364384d157f7e59426f4bfd385e8b424b5aaa79c83a6f5a1c8fd2e4e3289e95de1eb3511cb96bb333d6281a9919fafce760e4edb35b2cd2facab - languageName: node - linkType: hard - "string-argv@npm:^0.3.2": version: 0.3.2 resolution: "string-argv@npm:0.3.2" @@ -8752,31 +8689,6 @@ __metadata: languageName: node linkType: hard -"swagger-jsdoc@npm:6.2.8": - version: 6.2.8 - resolution: "swagger-jsdoc@npm:6.2.8" - dependencies: - commander: "npm:6.2.0" - doctrine: "npm:3.0.0" - glob: "npm:7.1.6" - lodash.mergewith: "npm:^4.6.2" - swagger-parser: "npm:^10.0.3" - yaml: "npm:2.0.0-1" - bin: - swagger-jsdoc: bin/swagger-jsdoc.js - checksum: 10c0/7e20f08e8d90cc1e787cd82c096291cf12533359f89c70fbe4295a01f7c4734f2e82a03ba94027127bcd3da04b817abfe979f00d00ef0cd8283e449250a66215 - languageName: node - linkType: hard - -"swagger-parser@npm:^10.0.3": - version: 10.0.3 - resolution: "swagger-parser@npm:10.0.3" - dependencies: - "@apidevtools/swagger-parser": "npm:10.0.3" - checksum: 10c0/d1a5c05f651f21a23508a36416071630b83e91dfffd52a6d44b06ca2cd1b86304c0dd2f4c04526c999b70062fa89bde3f5d54a1436626f4350590b6c6265a098 - languageName: node - linkType: hard - "swagger-ui-react@npm:^5.24.1": version: 5.24.2 resolution: "swagger-ui-react@npm:5.24.2" @@ -8879,13 +8791,6 @@ __metadata: languageName: node linkType: hard -"terminal-columns@npm:^1.4.1": - version: 1.4.1 - resolution: "terminal-columns@npm:1.4.1" - checksum: 10c0/e79135b0e9605d247ac26addf2afc0a4313ca4b70e6ff5b8571ab95c93703ab60db5b7b8a61a715e0ea5b6067434cfe1ae58f4392e01ad86c514b347a2a34e34 - languageName: node - linkType: hard - "tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" @@ -8973,6 +8878,44 @@ __metadata: languageName: node linkType: hard +"ts-node@npm:^10.9.2": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2" + dependencies: + "@cspotcode/source-map-support": "npm:^0.8.0" + "@tsconfig/node10": "npm:^1.0.7" + "@tsconfig/node12": "npm:^1.0.7" + "@tsconfig/node14": "npm:^1.0.0" + "@tsconfig/node16": "npm:^1.0.2" + acorn: "npm:^8.4.1" + acorn-walk: "npm:^8.1.1" + arg: "npm:^4.1.0" + create-require: "npm:^1.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + v8-compile-cache-lib: "npm:^3.0.1" + yn: "npm:3.1.1" + 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 + 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 + checksum: 10c0/5f29938489f96982a25ba650b64218e83a3357d76f7bede80195c65ab44ad279c8357264639b7abdd5d7e75fc269a83daa0e9c62fd8637a3def67254ecc9ddc2 + languageName: node + linkType: hard + "ts-toolbelt@npm:^9.6.0": version: 9.6.0 resolution: "ts-toolbelt@npm:9.6.0" @@ -9006,6 +8949,17 @@ __metadata: languageName: node linkType: hard +"tsconfig-paths@npm:^4.2.0": + version: 4.2.0 + resolution: "tsconfig-paths@npm:4.2.0" + dependencies: + json5: "npm:^2.2.2" + minimist: "npm:^1.2.6" + strip-bom: "npm:^3.0.0" + checksum: 10c0/09a5877402d082bb1134930c10249edeebc0211f36150c35e1c542e5b91f1047b1ccf7da1e59babca1ef1f014c525510f4f870de7c9bda470c73bb4e2721b3ea + languageName: node + linkType: hard + "tslib@npm:^1.14.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -9043,13 +8997,6 @@ __metadata: languageName: node linkType: hard -"type-flag@npm:^3.0.0": - version: 3.0.0 - resolution: "type-flag@npm:3.0.0" - checksum: 10c0/b1015d4eb18cd85432fa3bcd0228149dd6893a9c1360a2ad619f79b72e19acfb648a932e2a9ccf2e2db0f0c4b86385bd886a3364e221afb116424e0ae6d1b1ba - languageName: node - linkType: hard - "typed-array-buffer@npm:^1.0.3": version: 1.0.3 resolution: "typed-array-buffer@npm:1.0.3" @@ -9138,7 +9085,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:5.8.3, typescript@npm:^5.6.3": +"typescript@npm:^5.6.3, typescript@npm:^5.8.3": version: 5.8.3 resolution: "typescript@npm:5.8.3" bin: @@ -9148,7 +9095,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A5.8.3#optional!builtin, typescript@patch:typescript@npm%3A^5.6.3#optional!builtin": +"typescript@patch:typescript@npm%3A^5.6.3#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" bin: @@ -9363,7 +9310,14 @@ __metadata: languageName: node linkType: hard -"validator@npm:^13.11.0, validator@npm:^13.7.0": +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1" + checksum: 10c0/bdc36fb8095d3b41df197f5fb6f11e3a26adf4059df3213e3baa93810d8f0cc76f9a74aaefc18b73e91fe7e19154ed6f134eda6fded2e0f1c8d2272ed2d2d391 + languageName: node + linkType: hard + +"validator@npm:^13.11.0": version: 13.15.15 resolution: "validator@npm:13.15.15" checksum: 10c0/f5349d1fbb9cc36f9f6c5dab1880764ddad1d0d2b084e2a71e5964f7de1635d20e406611559df9a3db24828ce775cbee5e3b6dd52f0d555a61939ed7ea5990bd @@ -9513,13 +9467,6 @@ __metadata: languageName: node linkType: hard -"wrappy@npm:1": - version: 1.0.2 - resolution: "wrappy@npm:1.0.2" - checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 - languageName: node - linkType: hard - "xml-but-prettier@npm:^1.0.1": version: 1.0.1 resolution: "xml-but-prettier@npm:1.0.1" @@ -9564,13 +9511,6 @@ __metadata: languageName: node linkType: hard -"yaml@npm:2.0.0-1": - version: 2.0.0-1 - resolution: "yaml@npm:2.0.0-1" - checksum: 10c0/e76eba2fbae37cd3e5bff057184be7cdca849895149d2f5660386871a501d76d2e1ec5906c48269a9fe798f214df31d342675b37bcd9d09af7c12eb6fb46a740 - languageName: node - linkType: hard - "yaml@npm:^1.10.0": version: 1.10.2 resolution: "yaml@npm:1.10.2" @@ -9609,6 +9549,13 @@ __metadata: languageName: node linkType: hard +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: 10c0/0732468dd7622ed8a274f640f191f3eaf1f39d5349a1b72836df484998d7d9807fbea094e2f5486d6b0cd2414aad5775972df0e68f8604db89a239f0f4bf7443 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" @@ -9616,23 +9563,6 @@ __metadata: languageName: node linkType: hard -"z-schema@npm:^5.0.1": - version: 5.0.5 - resolution: "z-schema@npm:5.0.5" - dependencies: - commander: "npm:^9.4.1" - lodash.get: "npm:^4.4.2" - lodash.isequal: "npm:^4.5.0" - validator: "npm:^13.7.0" - dependenciesMeta: - commander: - optional: true - bin: - z-schema: bin/z-schema - checksum: 10c0/e4c812cfe6468c19b2a21d07d4ff8fb70359062d33400b45f89017eaa3efe9d51e85963f2b115eaaa99a16b451782249bf9b1fa8b31d35cc473e7becb3e44264 - languageName: node - linkType: hard - "zenscroll@npm:^4.0.2": version: 4.0.2 resolution: "zenscroll@npm:4.0.2"