add changes
This commit is contained in:
148
backend/config/logger.js
Normal file
148
backend/config/logger.js
Normal file
@@ -0,0 +1,148 @@
|
||||
const winston = require('winston');
|
||||
const DailyRotateFile = require('winston-daily-rotate-file');
|
||||
const path = require('path');
|
||||
|
||||
// Define log format
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
winston.format.json()
|
||||
);
|
||||
|
||||
// Console format for development
|
||||
const consoleFormat = winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf(({ timestamp, level, message, stack }) => {
|
||||
return stack
|
||||
? `${timestamp} [${level}]: ${message}\n${stack}`
|
||||
: `${timestamp} [${level}]: ${message}`;
|
||||
})
|
||||
);
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
const fs = require('fs');
|
||||
const logsDir = path.join(__dirname, '../logs');
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir);
|
||||
}
|
||||
|
||||
// Daily rotate file transport for error logs
|
||||
const errorRotateTransport = new DailyRotateFile({
|
||||
filename: path.join(logsDir, 'error-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
level: 'error',
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d',
|
||||
format: logFormat
|
||||
});
|
||||
|
||||
// Daily rotate file transport for combined logs
|
||||
const combinedRotateTransport = new DailyRotateFile({
|
||||
filename: path.join(logsDir, 'combined-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '20m',
|
||||
maxFiles: '30d',
|
||||
format: logFormat
|
||||
});
|
||||
|
||||
// Daily rotate file transport for HTTP logs
|
||||
const httpRotateTransport = new DailyRotateFile({
|
||||
filename: path.join(logsDir, 'http-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '20m',
|
||||
maxFiles: '7d',
|
||||
format: logFormat
|
||||
});
|
||||
|
||||
// Create the Winston logger
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: logFormat,
|
||||
defaultMeta: { service: 'interview-quiz-api' },
|
||||
transports: [
|
||||
errorRotateTransport,
|
||||
combinedRotateTransport
|
||||
],
|
||||
exceptionHandlers: [
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, 'exceptions.log')
|
||||
})
|
||||
],
|
||||
rejectionHandlers: [
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, 'rejections.log')
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Add console transport in development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: consoleFormat
|
||||
}));
|
||||
}
|
||||
|
||||
// HTTP logger for request logging
|
||||
const httpLogger = winston.createLogger({
|
||||
level: 'http',
|
||||
format: logFormat,
|
||||
defaultMeta: { service: 'interview-quiz-api' },
|
||||
transports: [httpRotateTransport]
|
||||
});
|
||||
|
||||
// Stream for Morgan middleware
|
||||
logger.stream = {
|
||||
write: (message) => {
|
||||
httpLogger.http(message.trim());
|
||||
}
|
||||
};
|
||||
|
||||
// Helper functions for structured logging
|
||||
logger.logRequest = (req, message) => {
|
||||
logger.info(message, {
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
ip: req.ip,
|
||||
userId: req.user?.id,
|
||||
userAgent: req.get('user-agent')
|
||||
});
|
||||
};
|
||||
|
||||
logger.logError = (error, req = null) => {
|
||||
const errorLog = {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
statusCode: error.statusCode || 500
|
||||
};
|
||||
|
||||
if (req) {
|
||||
errorLog.method = req.method;
|
||||
errorLog.url = req.originalUrl;
|
||||
errorLog.ip = req.ip;
|
||||
errorLog.userId = req.user?.id;
|
||||
errorLog.body = req.body;
|
||||
}
|
||||
|
||||
logger.error('Application Error', errorLog);
|
||||
};
|
||||
|
||||
logger.logDatabaseQuery = (query, duration) => {
|
||||
logger.debug('Database Query', {
|
||||
query,
|
||||
duration: `${duration}ms`
|
||||
});
|
||||
};
|
||||
|
||||
logger.logSecurityEvent = (event, req) => {
|
||||
logger.warn('Security Event', {
|
||||
event,
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent')
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = logger;
|
||||
289
backend/config/redis.js
Normal file
289
backend/config/redis.js
Normal file
@@ -0,0 +1,289 @@
|
||||
const Redis = require('ioredis');
|
||||
const logger = require('./logger');
|
||||
|
||||
/**
|
||||
* Redis Connection Configuration
|
||||
* Supports both single instance and cluster modes
|
||||
*/
|
||||
|
||||
const redisConfig = {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: parseInt(process.env.REDIS_PORT) || 6379,
|
||||
password: process.env.REDIS_PASSWORD || undefined,
|
||||
db: parseInt(process.env.REDIS_DB) || 0,
|
||||
retryStrategy: (times) => {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
},
|
||||
maxRetriesPerRequest: 3,
|
||||
enableReadyCheck: true,
|
||||
enableOfflineQueue: true,
|
||||
lazyConnect: false,
|
||||
connectTimeout: 10000,
|
||||
keepAlive: 30000,
|
||||
family: 4, // IPv4
|
||||
// Connection pool settings
|
||||
minReconnectInterval: 100,
|
||||
maxReconnectInterval: 3000
|
||||
};
|
||||
|
||||
// Create Redis client
|
||||
let redisClient = null;
|
||||
let isConnected = false;
|
||||
|
||||
try {
|
||||
redisClient = new Redis(redisConfig);
|
||||
|
||||
// Connection events
|
||||
redisClient.on('connect', () => {
|
||||
logger.info('Redis client connecting...');
|
||||
});
|
||||
|
||||
redisClient.on('ready', () => {
|
||||
isConnected = true;
|
||||
logger.info('Redis client connected and ready');
|
||||
});
|
||||
|
||||
redisClient.on('error', (err) => {
|
||||
isConnected = false;
|
||||
logger.error('Redis client error:', err);
|
||||
});
|
||||
|
||||
redisClient.on('close', () => {
|
||||
isConnected = false;
|
||||
logger.warn('Redis client connection closed');
|
||||
});
|
||||
|
||||
redisClient.on('reconnecting', () => {
|
||||
logger.info('Redis client reconnecting...');
|
||||
});
|
||||
|
||||
redisClient.on('end', () => {
|
||||
isConnected = false;
|
||||
logger.warn('Redis client connection ended');
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to create Redis client:', error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Redis is connected
|
||||
*/
|
||||
const isRedisConnected = () => {
|
||||
return isConnected && redisClient && redisClient.status === 'ready';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Redis client
|
||||
*/
|
||||
const getRedisClient = () => {
|
||||
if (!isRedisConnected()) {
|
||||
logger.warn('Redis client not connected');
|
||||
return null;
|
||||
}
|
||||
return redisClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Close Redis connection gracefully
|
||||
*/
|
||||
const closeRedis = async () => {
|
||||
if (redisClient) {
|
||||
await redisClient.quit();
|
||||
logger.info('Redis connection closed');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache helper functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get cached data
|
||||
* @param {string} key - Cache key
|
||||
* @returns {Promise<any>} - Parsed JSON data or null
|
||||
*/
|
||||
const getCache = async (key) => {
|
||||
try {
|
||||
if (!isRedisConnected()) {
|
||||
logger.warn('Redis not connected, cache miss');
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await redisClient.get(key);
|
||||
if (!data) return null;
|
||||
|
||||
logger.debug(`Cache hit: ${key}`);
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
logger.error(`Cache get error for key ${key}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set cached data
|
||||
* @param {string} key - Cache key
|
||||
* @param {any} value - Data to cache
|
||||
* @param {number} ttl - Time to live in seconds (default: 300 = 5 minutes)
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
const setCache = async (key, value, ttl = 300) => {
|
||||
try {
|
||||
if (!isRedisConnected()) {
|
||||
logger.warn('Redis not connected, skipping cache set');
|
||||
return false;
|
||||
}
|
||||
|
||||
const serialized = JSON.stringify(value);
|
||||
await redisClient.setex(key, ttl, serialized);
|
||||
|
||||
logger.debug(`Cache set: ${key} (TTL: ${ttl}s)`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`Cache set error for key ${key}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete cached data
|
||||
* @param {string} key - Cache key or pattern
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
const deleteCache = async (key) => {
|
||||
try {
|
||||
if (!isRedisConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Support pattern deletion (e.g., "user:*")
|
||||
if (key.includes('*')) {
|
||||
const keys = await redisClient.keys(key);
|
||||
if (keys.length > 0) {
|
||||
await redisClient.del(...keys);
|
||||
logger.debug(`Cache deleted: ${keys.length} keys matching ${key}`);
|
||||
}
|
||||
} else {
|
||||
await redisClient.del(key);
|
||||
logger.debug(`Cache deleted: ${key}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`Cache delete error for key ${key}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
const clearCache = async () => {
|
||||
try {
|
||||
if (!isRedisConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await redisClient.flushdb();
|
||||
logger.info('All cache cleared');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Cache clear error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get multiple keys at once
|
||||
* @param {string[]} keys - Array of cache keys
|
||||
* @returns {Promise<object>} - Object with key-value pairs
|
||||
*/
|
||||
const getCacheMultiple = async (keys) => {
|
||||
try {
|
||||
if (!isRedisConnected() || !keys || keys.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const values = await redisClient.mget(...keys);
|
||||
const result = {};
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
if (values[index]) {
|
||||
try {
|
||||
result[key] = JSON.parse(values[index]);
|
||||
} catch (err) {
|
||||
result[key] = null;
|
||||
}
|
||||
} else {
|
||||
result[key] = null;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Cache mget error:', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Increment a counter
|
||||
* @param {string} key - Cache key
|
||||
* @param {number} increment - Amount to increment (default: 1)
|
||||
* @param {number} ttl - Time to live in seconds (optional)
|
||||
* @returns {Promise<number>} - New value
|
||||
*/
|
||||
const incrementCache = async (key, increment = 1, ttl = null) => {
|
||||
try {
|
||||
if (!isRedisConnected()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const newValue = await redisClient.incrby(key, increment);
|
||||
|
||||
if (ttl) {
|
||||
await redisClient.expire(key, ttl);
|
||||
}
|
||||
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
logger.error(`Cache increment error for key ${key}:`, error);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if key exists
|
||||
* @param {string} key - Cache key
|
||||
* @returns {Promise<boolean>} - Exists status
|
||||
*/
|
||||
const cacheExists = async (key) => {
|
||||
try {
|
||||
if (!isRedisConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const exists = await redisClient.exists(key);
|
||||
return exists === 1;
|
||||
} catch (error) {
|
||||
logger.error(`Cache exists error for key ${key}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
redisClient,
|
||||
isRedisConnected,
|
||||
getRedisClient,
|
||||
closeRedis,
|
||||
getCache,
|
||||
setCache,
|
||||
deleteCache,
|
||||
clearCache,
|
||||
getCacheMultiple,
|
||||
incrementCache,
|
||||
cacheExists
|
||||
};
|
||||
348
backend/config/swagger.js
Normal file
348
backend/config/swagger.js
Normal file
@@ -0,0 +1,348 @@
|
||||
const swaggerJsdoc = require('swagger-jsdoc');
|
||||
|
||||
const options = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'Interview Quiz Application API',
|
||||
version: '1.0.0',
|
||||
description: 'Comprehensive API documentation for the Interview Quiz Application. This API provides endpoints for user authentication, quiz management, guest sessions, bookmarks, and admin operations.',
|
||||
contact: {
|
||||
name: 'API Support',
|
||||
email: 'support@interviewquiz.com'
|
||||
},
|
||||
license: {
|
||||
name: 'MIT',
|
||||
url: 'https://opensource.org/licenses/MIT'
|
||||
}
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3000/api',
|
||||
description: 'Development server'
|
||||
},
|
||||
{
|
||||
url: 'https://api.interviewquiz.com/api',
|
||||
description: 'Production server'
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'Enter your JWT token in the format: Bearer {token}'
|
||||
}
|
||||
},
|
||||
schemas: {
|
||||
Error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Error message'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
description: 'Detailed error information'
|
||||
}
|
||||
}
|
||||
},
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: 'User ID'
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
description: 'Unique username'
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
description: 'User email address'
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
enum: ['user', 'admin'],
|
||||
description: 'User role'
|
||||
},
|
||||
isActive: {
|
||||
type: 'boolean',
|
||||
description: 'Account activation status'
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: 'Account creation timestamp'
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: 'Last update timestamp'
|
||||
}
|
||||
}
|
||||
},
|
||||
Category: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: 'Category ID'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Category name'
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Category description'
|
||||
},
|
||||
questionCount: {
|
||||
type: 'integer',
|
||||
description: 'Number of questions in category'
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
}
|
||||
}
|
||||
},
|
||||
Question: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: 'Question ID'
|
||||
},
|
||||
categoryId: {
|
||||
type: 'integer',
|
||||
description: 'Associated category ID'
|
||||
},
|
||||
questionText: {
|
||||
type: 'string',
|
||||
description: 'Question content'
|
||||
},
|
||||
difficulty: {
|
||||
type: 'string',
|
||||
enum: ['easy', 'medium', 'hard'],
|
||||
description: 'Question difficulty level'
|
||||
},
|
||||
options: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
description: 'Answer options'
|
||||
},
|
||||
correctAnswer: {
|
||||
type: 'string',
|
||||
description: 'Correct answer (admin only)'
|
||||
}
|
||||
}
|
||||
},
|
||||
QuizSession: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: 'Quiz session ID'
|
||||
},
|
||||
userId: {
|
||||
type: 'integer',
|
||||
description: 'User ID (null for guest)'
|
||||
},
|
||||
guestSessionId: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
description: 'Guest session ID (null for authenticated user)'
|
||||
},
|
||||
categoryId: {
|
||||
type: 'integer',
|
||||
description: 'Quiz category ID'
|
||||
},
|
||||
totalQuestions: {
|
||||
type: 'integer',
|
||||
description: 'Total questions in quiz'
|
||||
},
|
||||
currentQuestionIndex: {
|
||||
type: 'integer',
|
||||
description: 'Current question position'
|
||||
},
|
||||
score: {
|
||||
type: 'integer',
|
||||
description: 'Current score'
|
||||
},
|
||||
isCompleted: {
|
||||
type: 'boolean',
|
||||
description: 'Quiz completion status'
|
||||
},
|
||||
completedAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: 'Completion timestamp'
|
||||
},
|
||||
startedAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: 'Start timestamp'
|
||||
}
|
||||
}
|
||||
},
|
||||
Bookmark: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: 'Bookmark ID'
|
||||
},
|
||||
userId: {
|
||||
type: 'integer',
|
||||
description: 'User ID'
|
||||
},
|
||||
questionId: {
|
||||
type: 'integer',
|
||||
description: 'Bookmarked question ID'
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Optional user notes'
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
},
|
||||
Question: {
|
||||
$ref: '#/components/schemas/Question'
|
||||
}
|
||||
}
|
||||
},
|
||||
GuestSession: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
guestSessionId: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
description: 'Unique guest session identifier'
|
||||
},
|
||||
convertedUserId: {
|
||||
type: 'integer',
|
||||
description: 'User ID after conversion (null if not converted)'
|
||||
},
|
||||
expiresAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: 'Session expiration timestamp'
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
UnauthorizedError: {
|
||||
description: 'Authentication token is missing or invalid',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
},
|
||||
example: {
|
||||
message: 'No token provided'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ForbiddenError: {
|
||||
description: 'User does not have permission to access this resource',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
},
|
||||
example: {
|
||||
message: 'Access denied. Admin only.'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
NotFoundError: {
|
||||
description: 'The requested resource was not found',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
},
|
||||
example: {
|
||||
message: 'Resource not found'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ValidationError: {
|
||||
description: 'Request validation failed',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
},
|
||||
example: {
|
||||
message: 'Validation error',
|
||||
error: 'Invalid input data'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
name: 'Authentication',
|
||||
description: 'User authentication and authorization endpoints'
|
||||
},
|
||||
{
|
||||
name: 'Users',
|
||||
description: 'User profile and account management'
|
||||
},
|
||||
{
|
||||
name: 'Categories',
|
||||
description: 'Quiz category management'
|
||||
},
|
||||
{
|
||||
name: 'Questions',
|
||||
description: 'Question management and retrieval'
|
||||
},
|
||||
{
|
||||
name: 'Quiz',
|
||||
description: 'Quiz session lifecycle and answer submission'
|
||||
},
|
||||
{
|
||||
name: 'Bookmarks',
|
||||
description: 'User question bookmarks'
|
||||
},
|
||||
{
|
||||
name: 'Guest',
|
||||
description: 'Guest user session management'
|
||||
},
|
||||
{
|
||||
name: 'Admin',
|
||||
description: 'Administrative operations (admin only)'
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: ['./routes/*.js'] // Path to the API routes
|
||||
};
|
||||
|
||||
const swaggerSpec = swaggerJsdoc(options);
|
||||
|
||||
module.exports = swaggerSpec;
|
||||
Reference in New Issue
Block a user