From be946be2edf15862b6a95f02f7ed580af540c45b Mon Sep 17 00:00:00 2001 From: anuragShingare30 Date: Thu, 29 Jan 2026 02:59:16 +0530 Subject: [PATCH 1/3] docs(swagger-api): add swagger api documentation --- .gitignore | 3 + package-lock.json | 47 ++++ package.json | 5 +- src/docs/definitions/analytics.definitions.ts | 136 +++++++++++ .../definitions/conversation.definitions.ts | 195 +++++++++++++++ .../definitions/forgotPassword.definitions.ts | 94 +++++++ src/docs/definitions/friend.definitions.ts | 230 ++++++++++++++++++ src/docs/definitions/index.ts | 69 ++++++ .../definitions/leaderboard.definitions.ts | 112 +++++++++ .../definitions/notification.definitions.ts | 121 +++++++++ src/docs/definitions/otp.definitions.ts | 91 +++++++ src/docs/definitions/profile.definitions.ts | 103 ++++++++ src/docs/definitions/user.definitions.ts | 129 ++++++++++ src/docs/swagger.config.ts | 41 ++++ src/docs/swagger.generator.ts | 69 ++++++ src/docs/swagger.setup.ts | 106 ++++++++ src/index.ts | 4 + 17 files changed, 1554 insertions(+), 1 deletion(-) create mode 100644 src/docs/definitions/analytics.definitions.ts create mode 100644 src/docs/definitions/conversation.definitions.ts create mode 100644 src/docs/definitions/forgotPassword.definitions.ts create mode 100644 src/docs/definitions/friend.definitions.ts create mode 100644 src/docs/definitions/index.ts create mode 100644 src/docs/definitions/leaderboard.definitions.ts create mode 100644 src/docs/definitions/notification.definitions.ts create mode 100644 src/docs/definitions/otp.definitions.ts create mode 100644 src/docs/definitions/profile.definitions.ts create mode 100644 src/docs/definitions/user.definitions.ts create mode 100644 src/docs/swagger.config.ts create mode 100644 src/docs/swagger.generator.ts create mode 100644 src/docs/swagger.setup.ts diff --git a/.gitignore b/.gitignore index ed1409e..55067fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules dist .env tsconfig.tsbuildinfo + +# Swagger generated documentation +src/docs/swagger-output.json diff --git a/package-lock.json b/package-lock.json index b3ed008..8bea247 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,9 +27,11 @@ "@types/cors": "^2.8.13", "@types/express": "^4.17.17", "@types/node": "^24.0.3", + "@types/swagger-ui-express": "^4.1.8", "@types/ws": "^8.18.1", "nodemon": "^3.0.1", "prisma": "^6.10.0", + "swagger-ui-express": "^5.0.1", "ts-node": "^10.9.2", "tsx": "^4.20.3", "typescript": "^5.8.3" @@ -1277,6 +1279,14 @@ "@prisma/debug": "6.10.0" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@smithy/abort-controller": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.0.tgz", @@ -2038,6 +2048,17 @@ "@types/send": "*" } }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -3690,6 +3711,32 @@ "node": ">=4" } }, + "node_modules/swagger-ui-dist": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 151a517..b1eaf04 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "tsc", "start": "node dist/index.js", "prisma:generate": "prisma generate", - "prisma:migrate": "prisma migrate dev" + "prisma:migrate": "prisma migrate dev", + "generate:swagger": "ts-node src/docs/swagger.generator.ts" }, "keywords": [], "author": "", @@ -19,9 +20,11 @@ "@types/cors": "^2.8.13", "@types/express": "^4.17.17", "@types/node": "^24.0.3", + "@types/swagger-ui-express": "^4.1.8", "@types/ws": "^8.18.1", "nodemon": "^3.0.1", "prisma": "^6.10.0", + "swagger-ui-express": "^5.0.1", "ts-node": "^10.9.2", "tsx": "^4.20.3", "typescript": "^5.8.3" diff --git a/src/docs/definitions/analytics.definitions.ts b/src/docs/definitions/analytics.definitions.ts new file mode 100644 index 0000000..f61194d --- /dev/null +++ b/src/docs/definitions/analytics.definitions.ts @@ -0,0 +1,136 @@ +/** + * Analytics API Definitions + * Documentation for analytics-related endpoints + */ + +export const analyticsDefinitions = { + schemas: { + PresenceData: { + type: 'object', + properties: { + date: { type: 'string', format: 'date' }, + totalMinutes: { type: 'number' }, + sessions: { type: 'number' }, + }, + }, + HourlyPresence: { + type: 'object', + properties: { + hour: { type: 'number', minimum: 0, maximum: 23 }, + minutes: { type: 'number' }, + }, + }, + TabUsage: { + type: 'object', + properties: { + domain: { type: 'string' }, + title: { type: 'string' }, + totalMinutes: { type: 'number' }, + visits: { type: 'number' }, + }, + }, + WeeklyPresence: { + type: 'object', + properties: { + data: { + type: 'array', + items: { $ref: '#/definitions/PresenceData' }, + }, + totalMinutes: { type: 'number' }, + }, + }, + }, + + endpoints: { + '/api/analytics/presence/today': { + get: { + tags: ['Analytics'], + summary: 'Get today\'s presence', + description: 'Get browser presence data for today', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'Today\'s presence data', schema: { $ref: '#/definitions/PresenceData' } }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/analytics/presence/weekly': { + get: { + tags: ['Analytics'], + summary: 'Get weekly presence', + description: 'Get browser presence data for the past week', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'Weekly presence data', schema: { $ref: '#/definitions/WeeklyPresence' } }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/analytics/presence/hourly': { + get: { + tags: ['Analytics'], + summary: 'Get hourly presence', + description: 'Get browser presence data broken down by hour', + security: [{ bearerAuth: [] }], + responses: { + 200: { + description: 'Hourly presence data', + schema: { + type: 'array', + items: { $ref: '#/definitions/HourlyPresence' }, + }, + }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/analytics/tab-usage/today': { + get: { + tags: ['Analytics'], + summary: 'Get today\'s tab usage', + description: 'Get tab/website usage data for today', + security: [{ bearerAuth: [] }], + responses: { + 200: { + description: 'Today\'s tab usage', + schema: { + type: 'array', + items: { $ref: '#/definitions/TabUsage' }, + }, + }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/analytics/tab-usage/weekly': { + get: { + tags: ['Analytics'], + summary: 'Get weekly tab usage', + description: 'Get tab/website usage data for the past week', + security: [{ bearerAuth: [] }], + responses: { + 200: { + description: 'Weekly tab usage', + schema: { + type: 'array', + items: { $ref: '#/definitions/TabUsage' }, + }, + }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/analytics/flush': { + post: { + tags: ['Analytics'], + summary: 'Flush analytics', + description: 'Manually trigger analytics data flush from cache to database', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'Analytics flushed successfully' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + }, +}; diff --git a/src/docs/definitions/conversation.definitions.ts b/src/docs/definitions/conversation.definitions.ts new file mode 100644 index 0000000..0fdcafd --- /dev/null +++ b/src/docs/definitions/conversation.definitions.ts @@ -0,0 +1,195 @@ +/** + * Conversation API Definitions + * Documentation for messaging and conversation endpoints + */ + +export const conversationDefinitions = { + schemas: { + SendMessageRequest: { + type: 'object', + required: ['recipientId', 'content'], + properties: { + recipientId: { type: 'string', description: 'ID of the message recipient' }, + content: { type: 'string', description: 'Message content' }, + conversationId: { type: 'string', description: 'Existing conversation ID (optional)' }, + }, + }, + Message: { + type: 'object', + properties: { + id: { type: 'string' }, + senderId: { type: 'string' }, + content: { type: 'string' }, + createdAt: { type: 'string', format: 'date-time' }, + seen: { type: 'boolean' }, + }, + }, + Conversation: { + type: 'object', + properties: { + id: { type: 'string' }, + participants: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + username: { type: 'string' }, + }, + }, + }, + lastMessage: { $ref: '#/definitions/Message' }, + unreadCount: { type: 'number' }, + createdAt: { type: 'string', format: 'date-time' }, + }, + }, + }, + + endpoints: { + '/api/conversation/send': { + post: { + tags: ['Conversations'], + summary: 'Send message', + description: 'Send a message to another user', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/SendMessageRequest' }, + }, + ], + responses: { + 201: { description: 'Message sent', schema: { $ref: '#/definitions/Message' } }, + 400: { description: 'Invalid request' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/conversation/{conversationId}/messages': { + get: { + tags: ['Conversations'], + summary: 'Get messages', + description: 'Get messages from a conversation', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'path', + name: 'conversationId', + required: true, + type: 'string', + description: 'ID of the conversation', + }, + { + in: 'query', + name: 'page', + type: 'number', + description: 'Page number for pagination', + }, + { + in: 'query', + name: 'limit', + type: 'number', + description: 'Number of messages per page', + }, + ], + responses: { + 200: { + description: 'List of messages', + schema: { + type: 'array', + items: { $ref: '#/definitions/Message' }, + }, + }, + 404: { description: 'Conversation not found' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/conversation/{conversationId}/mark-seen': { + post: { + tags: ['Conversations'], + summary: 'Mark conversation as seen', + description: 'Mark all messages in a conversation as seen', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'path', + name: 'conversationId', + required: true, + type: 'string', + description: 'ID of the conversation', + }, + ], + responses: { + 200: { description: 'Conversation marked as seen' }, + 404: { description: 'Conversation not found' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/conversation/{conversationId}/accept': { + post: { + tags: ['Conversations'], + summary: 'Accept conversation invite', + description: 'Accept a conversation invite from another user', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'path', + name: 'conversationId', + required: true, + type: 'string', + description: 'ID of the conversation', + }, + ], + responses: { + 200: { description: 'Conversation invite accepted' }, + 404: { description: 'Conversation not found' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/conversation/{conversationId}/reject': { + post: { + tags: ['Conversations'], + summary: 'Reject conversation invite', + description: 'Reject a conversation invite from another user', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'path', + name: 'conversationId', + required: true, + type: 'string', + description: 'ID of the conversation', + }, + ], + responses: { + 200: { description: 'Conversation invite rejected' }, + 404: { description: 'Conversation not found' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/conversation': { + get: { + tags: ['Conversations'], + summary: 'Get user conversations', + description: 'Get all conversations for the authenticated user', + security: [{ bearerAuth: [] }], + responses: { + 200: { + description: 'List of conversations', + schema: { + type: 'array', + items: { $ref: '#/definitions/Conversation' }, + }, + }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + }, +}; diff --git a/src/docs/definitions/forgotPassword.definitions.ts b/src/docs/definitions/forgotPassword.definitions.ts new file mode 100644 index 0000000..9e7e990 --- /dev/null +++ b/src/docs/definitions/forgotPassword.definitions.ts @@ -0,0 +1,94 @@ +/** + * Forgot Password API Definitions + * Documentation for password recovery endpoints + */ + +export const forgotPasswordDefinitions = { + schemas: { + ForgotPasswordOTPRequest: { + type: 'object', + required: ['email'], + properties: { + email: { type: 'string', format: 'email', description: 'Email address for password reset' }, + }, + }, + VerifyForgotPasswordOTPRequest: { + type: 'object', + required: ['email', 'otp'], + properties: { + email: { type: 'string', format: 'email', description: 'Email address' }, + otp: { type: 'string', description: 'OTP code received via email' }, + }, + }, + ResetPasswordRequest: { + type: 'object', + required: ['email', 'token', 'newPassword'], + properties: { + email: { type: 'string', format: 'email', description: 'Email address' }, + token: { type: 'string', description: 'Reset token received after OTP verification' }, + newPassword: { type: 'string', minLength: 6, description: 'New password' }, + }, + }, + }, + + endpoints: { + '/api/forgot-password/request-otp': { + post: { + tags: ['Forgot Password'], + summary: 'Request password reset OTP', + description: 'Request an OTP code for password reset', + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/ForgotPasswordOTPRequest' }, + }, + ], + responses: { + 200: { description: 'OTP sent successfully' }, + 400: { description: 'Invalid email or rate limited' }, + 404: { description: 'User not found' }, + }, + }, + }, + '/api/forgot-password/verify-otp': { + post: { + tags: ['Forgot Password'], + summary: 'Verify password reset OTP', + description: 'Verify OTP and receive a reset token', + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/VerifyForgotPasswordOTPRequest' }, + }, + ], + responses: { + 200: { description: 'OTP verified, reset token returned' }, + 400: { description: 'Invalid or expired OTP' }, + }, + }, + }, + '/api/forgot-password/reset-password': { + post: { + tags: ['Forgot Password'], + summary: 'Reset password', + description: 'Reset password using the reset token', + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/ResetPasswordRequest' }, + }, + ], + responses: { + 200: { description: 'Password reset successfully' }, + 400: { description: 'Invalid token or weak password' }, + }, + }, + }, + }, +}; diff --git a/src/docs/definitions/friend.definitions.ts b/src/docs/definitions/friend.definitions.ts new file mode 100644 index 0000000..bbc57c2 --- /dev/null +++ b/src/docs/definitions/friend.definitions.ts @@ -0,0 +1,230 @@ +/** + * Friend API Definitions + * Documentation for friend-related endpoints + */ + +export const friendDefinitions = { + schemas: { + FriendRequest: { + type: 'object', + required: ['friendId'], + properties: { + friendId: { type: 'string', description: 'ID of the user to send friend request to' }, + }, + }, + Friend: { + type: 'object', + properties: { + id: { type: 'string' }, + username: { type: 'string' }, + email: { type: 'string' }, + status: { type: 'string', enum: ['PENDING', 'ACCEPTED', 'IGNORED'] }, + }, + }, + FriendshipStatus: { + type: 'object', + properties: { + status: { type: 'string', enum: ['NONE', 'PENDING', 'ACCEPTED', 'IGNORED'] }, + requestId: { type: 'string' }, + }, + }, + }, + + endpoints: { + '/api/friends/request': { + post: { + tags: ['Friends'], + summary: 'Send friend request', + description: 'Send a friend request to another user', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/FriendRequest' }, + }, + ], + responses: { + 201: { description: 'Friend request sent successfully' }, + 400: { description: 'Invalid request or already friends' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/friends/accept/{requestId}': { + patch: { + tags: ['Friends'], + summary: 'Accept friend request', + description: 'Accept a pending friend request', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'path', + name: 'requestId', + required: true, + type: 'string', + description: 'ID of the friend request', + }, + ], + responses: { + 200: { description: 'Friend request accepted' }, + 404: { description: 'Friend request not found' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/friends/ignore/{requestId}': { + patch: { + tags: ['Friends'], + summary: 'Ignore friend request', + description: 'Ignore a pending friend request', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'path', + name: 'requestId', + required: true, + type: 'string', + description: 'ID of the friend request', + }, + ], + responses: { + 200: { description: 'Friend request ignored' }, + 404: { description: 'Friend request not found' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/friends/request/{requestId}': { + delete: { + tags: ['Friends'], + summary: 'Cancel friend request', + description: 'Cancel a pending friend request you sent', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'path', + name: 'requestId', + required: true, + type: 'string', + description: 'ID of the friend request to cancel', + }, + ], + responses: { + 200: { description: 'Friend request cancelled' }, + 404: { description: 'Friend request not found' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/friends/{friendId}': { + delete: { + tags: ['Friends'], + summary: 'Remove friend', + description: 'Remove a user from your friends list', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'path', + name: 'friendId', + required: true, + type: 'string', + description: 'ID of the friend to remove', + }, + ], + responses: { + 200: { description: 'Friend removed' }, + 404: { description: 'Friend not found' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/friends': { + get: { + tags: ['Friends'], + summary: 'Get all friends', + description: 'Get list of all accepted friends', + security: [{ bearerAuth: [] }], + responses: { + 200: { + description: 'List of friends', + schema: { + type: 'array', + items: { $ref: '#/definitions/Friend' }, + }, + }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/friends/firends-with-status': { + get: { + tags: ['Friends'], + summary: 'Get friends with status', + description: 'Get all friends with their online status', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'List of friends with status' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/friends/requests/pending': { + get: { + tags: ['Friends'], + summary: 'Get pending friend requests', + description: 'Get all pending friend requests received', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'List of pending friend requests' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/friends/requests/sent': { + get: { + tags: ['Friends'], + summary: 'Get sent friend requests', + description: 'Get all pending friend requests sent by you', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'List of sent friend requests' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/friends/requests/ignored': { + get: { + tags: ['Friends'], + summary: 'Get ignored friend requests', + description: 'Get all ignored friend requests', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'List of ignored friend requests' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/friends/status': { + get: { + tags: ['Friends'], + summary: 'Get friendship status', + description: 'Get the friendship status with a specific user', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'query', + name: 'userId', + type: 'string', + description: 'ID of the user to check status with', + }, + ], + responses: { + 200: { description: 'Friendship status', schema: { $ref: '#/definitions/FriendshipStatus' } }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + }, +}; diff --git a/src/docs/definitions/index.ts b/src/docs/definitions/index.ts new file mode 100644 index 0000000..0ac7e92 --- /dev/null +++ b/src/docs/definitions/index.ts @@ -0,0 +1,69 @@ +/** + * Index file for all API definitions + * Aggregates all endpoint and schema definitions for Swagger documentation + */ + +import { userDefinitions } from './user.definitions'; +import { friendDefinitions } from './friend.definitions'; +import { profileDefinitions } from './profile.definitions'; +import { analyticsDefinitions } from './analytics.definitions'; +import { leaderboardDefinitions } from './leaderboard.definitions'; +import { conversationDefinitions } from './conversation.definitions'; +import { notificationDefinitions } from './notification.definitions'; +import { otpDefinitions } from './otp.definitions'; +import { forgotPasswordDefinitions } from './forgotPassword.definitions'; + +// Aggregate all definitions +const allDefinitions = [ + userDefinitions, + friendDefinitions, + profileDefinitions, + analyticsDefinitions, + leaderboardDefinitions, + conversationDefinitions, + notificationDefinitions, + otpDefinitions, + forgotPasswordDefinitions, +]; + +/** + * Merge all schemas from definitions into a single object + */ +export function getAllSchemas(): Record { + const schemas: Record = {}; + + for (const def of allDefinitions) { + if (def.schemas) { + Object.assign(schemas, def.schemas); + } + } + + return schemas; +} + +/** + * Merge all endpoint paths from definitions into a single object + */ +export function getAllPaths(): Record { + const paths: Record = {}; + + for (const def of allDefinitions) { + if (def.endpoints) { + Object.assign(paths, def.endpoints); + } + } + + return paths; +} + +export { + userDefinitions, + friendDefinitions, + profileDefinitions, + analyticsDefinitions, + leaderboardDefinitions, + conversationDefinitions, + notificationDefinitions, + otpDefinitions, + forgotPasswordDefinitions, +}; diff --git a/src/docs/definitions/leaderboard.definitions.ts b/src/docs/definitions/leaderboard.definitions.ts new file mode 100644 index 0000000..1e64a1c --- /dev/null +++ b/src/docs/definitions/leaderboard.definitions.ts @@ -0,0 +1,112 @@ +/** + * Leaderboard API Definitions + * Documentation for leaderboard-related endpoints + */ + +export const leaderboardDefinitions = { + schemas: { + LeaderboardEntry: { + type: 'object', + properties: { + rank: { type: 'number' }, + userId: { type: 'string' }, + username: { type: 'string' }, + totalMinutes: { type: 'number' }, + avatarUrl: { type: 'string' }, + }, + }, + UserRank: { + type: 'object', + properties: { + rank: { type: 'number' }, + totalMinutes: { type: 'number' }, + percentile: { type: 'number' }, + }, + }, + UserPosition: { + type: 'object', + properties: { + position: { type: 'number' }, + totalUsers: { type: 'number' }, + percentile: { type: 'number' }, + }, + }, + }, + + endpoints: { + '/api/leaderboard/rank': { + get: { + tags: ['Leaderboard'], + summary: 'Get user rank', + description: 'Get the authenticated user\'s rank on the leaderboard', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'User rank information', schema: { $ref: '#/definitions/UserRank' } }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/leaderboard/top': { + get: { + tags: ['Leaderboard'], + summary: 'Get top users', + description: 'Get the top users on the leaderboard', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'query', + name: 'limit', + type: 'number', + description: 'Number of top users to return (default: 10)', + }, + ], + responses: { + 200: { + description: 'Top users list', + schema: { + type: 'array', + items: { $ref: '#/definitions/LeaderboardEntry' }, + }, + }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/leaderboard/public-top': { + get: { + tags: ['Leaderboard'], + summary: 'Get public top users', + description: 'Get the public leaderboard (no authentication required)', + parameters: [ + { + in: 'query', + name: 'limit', + type: 'number', + description: 'Number of top users to return (default: 10)', + }, + ], + responses: { + 200: { + description: 'Public top users list', + schema: { + type: 'array', + items: { $ref: '#/definitions/LeaderboardEntry' }, + }, + }, + }, + }, + }, + '/api/leaderboard/user-position': { + get: { + tags: ['Leaderboard'], + summary: 'Get user position', + description: 'Get the authenticated user\'s position on the leaderboard', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'User position information', schema: { $ref: '#/definitions/UserPosition' } }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + }, +}; diff --git a/src/docs/definitions/notification.definitions.ts b/src/docs/definitions/notification.definitions.ts new file mode 100644 index 0000000..d8ee570 --- /dev/null +++ b/src/docs/definitions/notification.definitions.ts @@ -0,0 +1,121 @@ +/** + * Notification API Definitions + * Documentation for notification-related endpoints + */ + +export const notificationDefinitions = { + schemas: { + Notification: { + type: 'object', + properties: { + id: { type: 'string' }, + type: { type: 'string', enum: ['FRIEND_REQUEST', 'FRIEND_ACCEPTED', 'MESSAGE', 'SYSTEM'] }, + title: { type: 'string' }, + message: { type: 'string' }, + read: { type: 'boolean' }, + createdAt: { type: 'string', format: 'date-time' }, + data: { type: 'object', description: 'Additional notification data' }, + }, + }, + NotificationList: { + type: 'object', + properties: { + notifications: { + type: 'array', + items: { $ref: '#/definitions/Notification' }, + }, + total: { type: 'number' }, + page: { type: 'number' }, + limit: { type: 'number' }, + hasMore: { type: 'boolean' }, + }, + }, + UnreadCount: { + type: 'object', + properties: { + count: { type: 'number' }, + }, + }, + }, + + endpoints: { + '/api/notifications': { + get: { + tags: ['Notifications'], + summary: 'Get user notifications', + description: 'Get paginated list of notifications for the authenticated user', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'query', + name: 'page', + type: 'number', + description: 'Page number (default: 1)', + }, + { + in: 'query', + name: 'limit', + type: 'number', + description: 'Items per page (default: 20)', + }, + { + in: 'query', + name: 'unreadOnly', + type: 'boolean', + description: 'Filter to show only unread notifications', + }, + ], + responses: { + 200: { description: 'List of notifications', schema: { $ref: '#/definitions/NotificationList' } }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/notifications/unread-count': { + get: { + tags: ['Notifications'], + summary: 'Get unread notification count', + description: 'Get the count of unread notifications', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'Unread count', schema: { $ref: '#/definitions/UnreadCount' } }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/notifications/{notificationId}/read': { + patch: { + tags: ['Notifications'], + summary: 'Mark notification as read', + description: 'Mark a specific notification as read', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'path', + name: 'notificationId', + required: true, + type: 'string', + description: 'ID of the notification', + }, + ], + responses: { + 200: { description: 'Notification marked as read' }, + 404: { description: 'Notification not found' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/notifications/read-all': { + patch: { + tags: ['Notifications'], + summary: 'Mark all notifications as read', + description: 'Mark all notifications as read for the authenticated user', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'All notifications marked as read' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + }, +}; diff --git a/src/docs/definitions/otp.definitions.ts b/src/docs/definitions/otp.definitions.ts new file mode 100644 index 0000000..7602e9e --- /dev/null +++ b/src/docs/definitions/otp.definitions.ts @@ -0,0 +1,91 @@ +/** + * OTP API Definitions + * Documentation for OTP (email verification) endpoints + */ + +export const otpDefinitions = { + schemas: { + RequestOTPRequest: { + type: 'object', + required: ['email'], + properties: { + email: { type: 'string', format: 'email', description: 'Email address to send OTP to' }, + }, + }, + VerifyOTPRequest: { + type: 'object', + required: ['email', 'otp'], + properties: { + email: { type: 'string', format: 'email', description: 'Email address' }, + otp: { type: 'string', description: 'OTP code received via email' }, + }, + }, + VerificationStatus: { + type: 'object', + properties: { + verified: { type: 'boolean' }, + email: { type: 'string' }, + }, + }, + }, + + endpoints: { + '/api/otp/request-otp': { + post: { + tags: ['OTP'], + summary: 'Request OTP', + description: 'Request an OTP code to be sent to the specified email', + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/RequestOTPRequest' }, + }, + ], + responses: { + 200: { description: 'OTP sent successfully' }, + 400: { description: 'Invalid email or rate limited' }, + }, + }, + }, + '/api/otp/verify-otp': { + post: { + tags: ['OTP'], + summary: 'Verify OTP', + description: 'Verify the OTP code for email verification', + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/VerifyOTPRequest' }, + }, + ], + responses: { + 200: { description: 'OTP verified successfully' }, + 400: { description: 'Invalid or expired OTP' }, + }, + }, + }, + '/api/otp/check-verification': { + get: { + tags: ['OTP'], + summary: 'Check email verification status', + description: 'Check if an email has been verified', + parameters: [ + { + in: 'query', + name: 'email', + type: 'string', + required: true, + description: 'Email address to check', + }, + ], + responses: { + 200: { description: 'Verification status', schema: { $ref: '#/definitions/VerificationStatus' } }, + }, + }, + }, + }, +}; diff --git a/src/docs/definitions/profile.definitions.ts b/src/docs/definitions/profile.definitions.ts new file mode 100644 index 0000000..7e1873e --- /dev/null +++ b/src/docs/definitions/profile.definitions.ts @@ -0,0 +1,103 @@ +/** + * Profile API Definitions + * Documentation for profile-related endpoints + */ + +export const profileDefinitions = { + schemas: { + Profile: { + type: 'object', + properties: { + id: { type: 'string' }, + username: { type: 'string' }, + email: { type: 'string' }, + bio: { type: 'string' }, + avatarUrl: { type: 'string' }, + createdAt: { type: 'string', format: 'date-time' }, + }, + }, + UpdateProfileRequest: { + type: 'object', + properties: { + bio: { type: 'string', description: 'User bio/description' }, + avatarUrl: { type: 'string', description: 'URL to avatar image' }, + }, + }, + PrivacySettings: { + type: 'object', + properties: { + emailPrivacy: { type: 'string', enum: ['PUBLIC', 'FRIENDS', 'PRIVATE'] }, + onlinePrivacy: { type: 'string', enum: ['PUBLIC', 'FRIENDS', 'PRIVATE'] }, + lastOnlinePrivacy: { type: 'string', enum: ['PUBLIC', 'FRIENDS', 'PRIVATE'] }, + tabPrivacyLevel: { type: 'string', enum: ['PUBLIC', 'FRIENDS', 'PRIVATE'] }, + }, + }, + }, + + endpoints: { + '/api/profile/{username}': { + get: { + tags: ['Profile'], + summary: 'Get profile by username', + description: 'Get a user profile by their username', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'path', + name: 'username', + required: true, + type: 'string', + description: 'Username of the profile to fetch', + }, + ], + responses: { + 200: { description: 'User profile', schema: { $ref: '#/definitions/Profile' } }, + 404: { description: 'User not found' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/profile/privacy': { + patch: { + tags: ['Profile'], + summary: 'Update privacy settings', + description: 'Update privacy settings for the authenticated user', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/PrivacySettings' }, + }, + ], + responses: { + 200: { description: 'Privacy settings updated' }, + 400: { description: 'Invalid settings' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/profile': { + patch: { + tags: ['Profile'], + summary: 'Update profile', + description: 'Update profile information for the authenticated user', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/UpdateProfileRequest' }, + }, + ], + responses: { + 200: { description: 'Profile updated', schema: { $ref: '#/definitions/Profile' } }, + 400: { description: 'Invalid data' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + }, +}; diff --git a/src/docs/definitions/user.definitions.ts b/src/docs/definitions/user.definitions.ts new file mode 100644 index 0000000..e8cb1c0 --- /dev/null +++ b/src/docs/definitions/user.definitions.ts @@ -0,0 +1,129 @@ +/** + * User API Definitions + * Documentation for user-related endpoints + */ + +export const userDefinitions = { + // Request/Response schemas + schemas: { + SignupRequest: { + type: 'object', + required: ['email', 'password', 'username'], + properties: { + email: { type: 'string', format: 'email', example: 'user@example.com' }, + password: { type: 'string', minLength: 6, example: 'securePassword123' }, + username: { type: 'string', example: 'johndoe' }, + }, + }, + LoginRequest: { + type: 'object', + required: ['email', 'password'], + properties: { + email: { type: 'string', format: 'email', example: 'user@example.com' }, + password: { type: 'string', example: 'securePassword123' }, + }, + }, + AuthResponse: { + type: 'object', + properties: { + token: { type: 'string', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' }, + user: { + type: 'object', + properties: { + id: { type: 'string', example: 'uuid-string' }, + email: { type: 'string', example: 'user@example.com' }, + username: { type: 'string', example: 'johndoe' }, + }, + }, + }, + }, + UserSearchResult: { + type: 'object', + properties: { + id: { type: 'string' }, + username: { type: 'string' }, + email: { type: 'string' }, + }, + }, + }, + + // Endpoint documentation + endpoints: { + '/api/users/signup': { + post: { + tags: ['Users'], + summary: 'Register a new user', + description: 'Create a new user account with email, password, and username', + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/SignupRequest' }, + }, + ], + responses: { + 201: { description: 'User created successfully', schema: { $ref: '#/definitions/AuthResponse' } }, + 400: { description: 'Validation error or user already exists' }, + }, + }, + }, + '/api/users/login': { + post: { + tags: ['Users'], + summary: 'User login', + description: 'Authenticate user with email and password', + parameters: [ + { + in: 'body', + name: 'body', + required: true, + schema: { $ref: '#/definitions/LoginRequest' }, + }, + ], + responses: { + 200: { description: 'Login successful', schema: { $ref: '#/definitions/AuthResponse' } }, + 401: { description: 'Invalid credentials' }, + }, + }, + }, + '/api/users/logout': { + post: { + tags: ['Users'], + summary: 'User logout', + description: 'Logout the authenticated user', + security: [{ bearerAuth: [] }], + responses: { + 200: { description: 'Logout successful' }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + '/api/users/search': { + get: { + tags: ['Users'], + summary: 'Search users', + description: 'Search for users by username or email', + security: [{ bearerAuth: [] }], + parameters: [ + { + in: 'query', + name: 'query', + type: 'string', + description: 'Search query string', + }, + ], + responses: { + 200: { + description: 'Search results', + schema: { + type: 'array', + items: { $ref: '#/definitions/UserSearchResult' }, + }, + }, + 401: { description: 'Unauthorized' }, + }, + }, + }, + }, +}; diff --git a/src/docs/swagger.config.ts b/src/docs/swagger.config.ts new file mode 100644 index 0000000..64a3423 --- /dev/null +++ b/src/docs/swagger.config.ts @@ -0,0 +1,41 @@ +/** + * Swagger Configuration + * Contains the base configuration for Swagger documentation + */ + +export const swaggerConfig = { + info: { + title: 'BrowsePing API', + version: '1.0.0', + description: 'API documentation for BrowsePing - A browser activity tracking and social platform', + contact: { + name: 'BrowsePing Support', + }, + }, + host: 'localhost:3000', + basePath: '/', + schemes: ['http', 'https'], + securityDefinitions: { + bearerAuth: { + type: 'apiKey', + in: 'header', + name: 'Authorization', + description: 'Bearer token for authentication. Format: Bearer ', + }, + }, + tags: [ + { name: 'Users', description: 'User authentication and management' }, + { name: 'Friends', description: 'Friend requests and relationships' }, + { name: 'Profile', description: 'User profile management' }, + { name: 'Analytics', description: 'Browser activity analytics' }, + { name: 'Leaderboard', description: 'User rankings and leaderboard' }, + { name: 'Conversations', description: 'Messaging and conversations' }, + { name: 'Notifications', description: 'User notifications' }, + { name: 'OTP', description: 'Email verification via OTP' }, + { name: 'Forgot Password', description: 'Password recovery' }, + ], + definitions: {}, +}; + +export const outputFile = './src/docs/swagger-output.json'; +export const endpointsFiles = ['./src/routes/*.ts', './src/routes/**/*.ts']; diff --git a/src/docs/swagger.generator.ts b/src/docs/swagger.generator.ts new file mode 100644 index 0000000..bd8faf6 --- /dev/null +++ b/src/docs/swagger.generator.ts @@ -0,0 +1,69 @@ +/** + * Swagger Documentation Generator Script + * + * This script generates the swagger-output.json file from the API definitions. + * command to generate swagger file: npm run generate:swagger + * the swagger docs will be available at: http://localhost:3000/api-docs + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { swaggerConfig } from './swagger.config'; +import { getAllSchemas, getAllPaths } from './definitions'; + +interface SwaggerDoc { + swagger: string; + info: typeof swaggerConfig.info; + host: string; + basePath: string; + schemes: string[]; + securityDefinitions: typeof swaggerConfig.securityDefinitions; + tags: typeof swaggerConfig.tags; + paths: Record; + definitions: Record; +} + +function generateSwaggerDoc(): SwaggerDoc { + console.log('Starting Swagger documentation generation...'); + + const schemas = getAllSchemas(); + const paths = getAllPaths(); + + const swaggerDoc: SwaggerDoc = { + swagger: '2.0', + info: swaggerConfig.info, + host: swaggerConfig.host, + basePath: swaggerConfig.basePath, + schemes: swaggerConfig.schemes, + securityDefinitions: swaggerConfig.securityDefinitions, + tags: swaggerConfig.tags, + paths: paths, + definitions: schemas, + }; + + console.log(`Generated documentation for ${Object.keys(paths).length} endpoints`); + console.log(`Generated ${Object.keys(schemas).length} schema definitions`); + + return swaggerDoc; +} + +function writeSwaggerFile(doc: SwaggerDoc): void { + const outputPath = path.join(__dirname, 'swagger-output.json'); + + try { + fs.writeFileSync(outputPath, JSON.stringify(doc, null, 2), 'utf-8'); + console.log(`Swagger documentation generated successfully!`); + console.log(`Output file: ${outputPath}`); + } catch (error) { + console.error('āŒ Error writing swagger output file:', error); + process.exit(1); + } +} + +// Main execution +const swaggerDoc = generateSwaggerDoc(); +writeSwaggerFile(swaggerDoc); + +console.log('\nšŸ“š To view the documentation:'); +console.log(' 1. Start the dev server: npm run dev'); +console.log(' 2. Open http://localhost:3000/api-docs in your browser'); diff --git a/src/docs/swagger.setup.ts b/src/docs/swagger.setup.ts new file mode 100644 index 0000000..d0af778 --- /dev/null +++ b/src/docs/swagger.setup.ts @@ -0,0 +1,106 @@ +/** + * Swagger Documentation Setup + * + * Documentation is only available in development mode and when the + * swagger-output.json file has been generated. + */ + +import { Express, Request, Response, NextFunction } from 'express'; +import * as path from 'path'; +import * as fs from 'fs'; + +const SWAGGER_OUTPUT_PATH = path.join(__dirname, 'swagger-output.json'); + +/** + * Check if API documentation should be enabled + */ +function isDocsEnabled(): boolean { + const nodeEnv = process.env.NODE_ENV || 'development'; + const enableDocs = process.env.ENABLE_API_DOCS; + + // Explicitly disabled + if (enableDocs === 'false') { + return false; + } + + // Explicitly enabled + if (enableDocs === 'true') { + return true; + } + + // Default: enabled in development, disabled in production + return nodeEnv !== 'production'; +} + +/** + * Check if swagger output file exists + */ +function swaggerFileExists(): boolean { + return fs.existsSync(SWAGGER_OUTPUT_PATH); +} + +/** + * Setup Swagger documentation routes + * + * @param app Express application instance + */ +export async function setupSwaggerDocs(app: Express): Promise { + if (!isDocsEnabled()) { + console.log('šŸ“š API documentation is disabled'); + return; + } + + if (!swaggerFileExists()) { + console.log('āš ļø Swagger documentation not found. Run "npm run generate:swagger" to generate.'); + + // Add a placeholder route that informs developers + app.get('/api-docs', (_req: Request, res: Response) => { + res.status(503).json({ + error: 'API documentation not available', + message: 'Swagger documentation has not been generated. Run "npm run generate:swagger" first.', + }); + }); + return; + } + + try { + // Dynamic import to avoid requiring these in production + const swaggerUi = await import('swagger-ui-express'); + const swaggerDocument = JSON.parse(fs.readFileSync(SWAGGER_OUTPUT_PATH, 'utf-8')); + + // Update host based on current environment + const port = process.env.PORT || 3000; + swaggerDocument.host = `localhost:${port}`; + + // Swagger UI options + const swaggerOptions = { + explorer: true, + customCss: '.swagger-ui .topbar { display: none }', + customSiteTitle: 'BrowsePing API Documentation', + }; + + // Serve Swagger UI + app.use( + '/api-docs', + swaggerUi.serve, + swaggerUi.setup(swaggerDocument, swaggerOptions) + ); + + // Serve raw swagger JSON + app.get('/api-docs.json', (_req: Request, res: Response) => { + res.json(swaggerDocument); + }); + + console.log(`šŸ“š API documentation available at /api-docs`); + } catch (error) { + console.error('āŒ Failed to setup Swagger documentation:', error); + + // Add error route + app.get('/api-docs', (_req: Request, res: Response) => { + res.status(500).json({ + error: 'API documentation setup failed', + message: 'There was an error loading the Swagger documentation.', + }); + }); + } +} diff --git a/src/index.ts b/src/index.ts index 47d4cf1..51a6188 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { userRoutes, import http from 'http'; import { initializeWebSocketServer } from './websocket/server'; import { startAnalyticsFlushWorker } from './workers/analyticsFlushWorker'; +import { setupSwaggerDocs } from './docs/swagger.setup'; const app = express(); @@ -20,6 +21,9 @@ const PORT = process.env.PORT || 3000; app.use(cors()); app.use(express.json()); +// Setup Swagger API documentation (development only) +setupSwaggerDocs(app); + // Routes app.use('/api/users', userRoutes); app.use('/api/friends', friendRoutes); From 10ae6629c34ef40f0dc62b828ea56462147ad638 Mon Sep 17 00:00:00 2001 From: Akash Kumar <154402631+akash-kumar-dev@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:56:12 +0530 Subject: [PATCH 2/3] remove inline icons --- src/docs/swagger.generator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docs/swagger.generator.ts b/src/docs/swagger.generator.ts index bd8faf6..be9265d 100644 --- a/src/docs/swagger.generator.ts +++ b/src/docs/swagger.generator.ts @@ -55,7 +55,7 @@ function writeSwaggerFile(doc: SwaggerDoc): void { console.log(`Swagger documentation generated successfully!`); console.log(`Output file: ${outputPath}`); } catch (error) { - console.error('āŒ Error writing swagger output file:', error); + console.error('Error writing swagger output file:', error); process.exit(1); } } @@ -64,6 +64,6 @@ function writeSwaggerFile(doc: SwaggerDoc): void { const swaggerDoc = generateSwaggerDoc(); writeSwaggerFile(swaggerDoc); -console.log('\nšŸ“š To view the documentation:'); +console.log('\n To view the documentation:'); console.log(' 1. Start the dev server: npm run dev'); console.log(' 2. Open http://localhost:3000/api-docs in your browser'); From ab0ad3705797e1a8cd1888395ec011b65e6e8086 Mon Sep 17 00:00:00 2001 From: Akash Kumar <154402631+akash-kumar-dev@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:57:12 +0530 Subject: [PATCH 3/3] Remove emojis from Swagger setup logs --- src/docs/swagger.setup.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/docs/swagger.setup.ts b/src/docs/swagger.setup.ts index d0af778..824ccbc 100644 --- a/src/docs/swagger.setup.ts +++ b/src/docs/swagger.setup.ts @@ -46,12 +46,12 @@ function swaggerFileExists(): boolean { */ export async function setupSwaggerDocs(app: Express): Promise { if (!isDocsEnabled()) { - console.log('šŸ“š API documentation is disabled'); + console.log('API documentation is disabled'); return; } if (!swaggerFileExists()) { - console.log('āš ļø Swagger documentation not found. Run "npm run generate:swagger" to generate.'); + console.log('Swagger documentation not found. Run "npm run generate:swagger" to generate.'); // Add a placeholder route that informs developers app.get('/api-docs', (_req: Request, res: Response) => { @@ -91,9 +91,9 @@ export async function setupSwaggerDocs(app: Express): Promise { res.json(swaggerDocument); }); - console.log(`šŸ“š API documentation available at /api-docs`); + console.log(`API documentation available at /api-docs`); } catch (error) { - console.error('āŒ Failed to setup Swagger documentation:', error); + console.error('Failed to setup Swagger documentation:', error); // Add error route app.get('/api-docs', (_req: Request, res: Response) => {