{
  "slug": "hybrid-auth",
  "runtimes": {
    "node": {
      "frameworks": {
        "express": {
          "databases": {
            "mongodb": {
              "orms": {
                "mongoose": {
                  "dependencies": {
                    "runtime": [
                      "express",
                      "mongoose",
                      "argon2",
                      "cloudinary",
                      "cookie-parser",
                      "cors",
                      "express-rate-limit",
                      "helmet",
                      "multer",
                      "resend",
                      "redis",
                      "jsonwebtoken",
                      "nodemailer",
                      "passport",
                      "passport-github2",
                      "passport-facebook",
                      "passport-google-oauth20",
                      "pino",
                      "pino-pretty",
                      "zod",
                      "dotenv-flow",
                      "cross-env",
                      "source-map-support",
                      "swagger-autogen",
                      "swagger-ui-express",
                      "ejs"
                    ],
                    "dev": [
                      "@types/express",
                      "@types/cookie-parser",
                      "@types/cors",
                      "@types/jsonwebtoken",
                      "@types/morgan",
                      "@types/multer",
                      "@types/nodemailer",
                      "@types/passport",
                      "@types/passport-github2",
                      "@types/passport-facebook",
                      "@types/passport-google-oauth20",
                      "morgan",
                      "@types/source-map-support",
                      "@types/swagger-ui-express",
                      "@types/ejs"
                    ]
                  },
                  "env": [
                    "PORT",
                    "NODE_ENV",
                    "LOG_LEVEL",
                    "CORS_ORIGIN",
                    "CRYPTO_SECRET",
                    "DATABASE_URL",
                    "JWT_ACCESS_SECRET",
                    "JWT_REFRESH_SECRET",
                    "RESEND_API_KEY",
                    "REDIS_URL",
                    "EMAIL_FROM",
                    "CLOUDINARY_CLOUD_NAME",
                    "CLOUDINARY_API_KEY",
                    "CLOUDINARY_API_SECRET",
                    "GITHUB_CLIENT_ID",
                    "GITHUB_CLIENT_SECRET",
                    "GITHUB_REDIRECT_URI",
                    "GOOGLE_CLIENT_ID",
                    "GOOGLE_CLIENT_SECRET",
                    "GOOGLE_REDIRECT_URI",
                    "FACEBOOK_APP_ID",
                    "FACEBOOK_APP_SECRET",
                    "FACEBOOK_REDIRECT_URI"
                  ],
                  "architectures": {
                    "mvc": {
                      "files": [
                        {
                          "type": "file",
                          "path": "swagger.config.ts",
                          "content": "import swaggerAutoGen from \"swagger-autogen\";\n\nconst doc = {\n  info: {\n    title: \"Hybrid Auth API\",\n    description: \"Hybrid Auth API\",\n    version: \"1.0.0\"\n  },\n  host: \"localhost:9000/api\",\n  schemes: [\"http\"]\n};\n\nconst outputFile = \"./src/docs/swagger.json\";\nconst endpointsFiles = [\"./src/routes/*.ts\"];\n\nswaggerAutoGen(outputFile, endpointsFiles, doc);\n"
                        },
                        {
                          "type": "file",
                          "path": "README.md",
                          "content": "# Hybrid Auth MongoDB MVC\n\nMinimal Node.js + Express + TypeScript MVC starter using MongoDB and hybrid authentication (local and OAuth via Passport or similar).\n\n## Features\n\n- Express + TypeScript MVC structure\n- MongoDB integration\n- Hybrid auth: local credentials and OAuth providers (e.g., Google, GitHub, Facebook)\n- Session-based authentication compatible with production\n- Environment-driven configuration with `.env`\n- Dev and production scripts\n\n## What This Provides\n\n- A clean starting point for credential and OAuth login\n- Prewired Express app with routing and session middleware\n- MongoDB connection wiring ready for your data models\n- TypeScript configuration and scripts for iterative dev and production builds\n- Example environment keys you can enable as needed\n\n## Quick Start\n\n1. Install dependencies:\n   - `npm install`\n2. Configure environment:\n   - Create `.env` (copy from `.env.example` if present).\n   - Set variables shown below.\n3. Run in development:\n   - `npm run dev`\n4. Build and run in production:\n   - `npm run build`\n   - `npm start`\n\n## Requirements\n\n- Node.js 18+\n- MongoDB (local or Atlas)\n\n## Environment Variables\n\n- `MONGO_URI` — MongoDB connection string\n- `SESSION_SECRET` — random strong string\n- `PORT` — server port (e.g., 3000)\n- `NODE_ENV` — `development` or `production`\n- Optional OAuth (enable what you use):\n  - `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_CALLBACK_URL`\n  - `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GITHUB_CALLBACK_URL`\n  - `FACEBOOK_APP_ID`, `FACEBOOK_APP_SECRET`, `FACEBOOK_CALLBACK_URL`\n\n## Scripts\n\n- `npm run dev` — start development server\n- `npm run build` — compile TypeScript\n- `npm start` — start compiled app\n\n## Notes\n\n- Never commit `.env` or secrets.\n- Ensure indexes and users collections exist if your auth flow relies on them.\n"
                        },
                        {
                          "type": "file",
                          "path": "package.json",
                          "content": "{\n  \"name\": \"servercn-hybrid-auth\",\n  \"version\": \"1.0.0\",\n  \"main\": \"dist/server.js\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development npx tsx watch src/server.ts\",\n    \"build\": \"rm -rf dist && tsc && tsc-alias\",\n    \"start\": \"cross-env NODE_ENV=production node dist/server.js\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"docs\": \"npx tsx swagger.config.ts\",\n    \"prepare\": \"husky\",\n    \"lint:check\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"format:check\": \"npx prettier . --check\",\n    \"format:fix\": \"npx prettier . --write\"\n  },\n  \"lint-staged\": {\n    \"src/**/*.ts\": [\n      \"eslint --fix\",\n      \"prettier --write\",\n      \"tsc --noEmit\"\n    ]\n  },\n  \"dependencies\": {},\n  \"devDependencies\": {}\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/server.ts",
                          "content": "import app from \"./app\";\nimport { connectDB } from \"./configs/db\";\nimport env from \"./configs/env\";\nimport redisClient from \"./configs/redis\";\nimport { logger } from \"./utils/logger\";\nimport { configureGracefulShutdown } from \"./utils/shutdown\";\n\nconst port = env.PORT || 9000;\n\nconnectDB().then(() => {\n  redisClient\n    .connect()\n    .then(() => {\n      logger.info(\"Redis Connection Success\");\n      const server = app.listen(port, () => {\n        logger.info(`[server]: Server is running at http://localhost:${port}`);\n        logger.info(`[server]: Environment: ${env.NODE_ENV}`);\n        logger.info(\n          `[server]: Swagger docs are available at http://localhost:${port}/api/docs`\n        );\n      });\n\n      configureGracefulShutdown(server);\n    })\n    .catch(error => {\n      logger.error(error, \"Redis Connection Failed\");\n      process.exit(1);\n    });\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/app.ts",
                          "content": "import express, { Express, Request, Response } from \"express\";\nimport cookieParser from \"cookie-parser\";\nimport morgan from \"morgan\";\nimport { notFoundHandler } from \"./middlewares/not-found-handler\";\nimport { errorHandler } from \"./middlewares/error-handler\";\nimport env from \"./configs/env\";\nimport { configureSecurityHeaders } from \"./middlewares/security-header\";\n\nimport Routes from \"./routes/index\";\n\nimport \"./configs/passport\";\nimport sourceMapSupport from \"source-map-support\";\nimport { setupSwagger } from \"./configs/swagger\";\nsourceMapSupport.install();\n\nconst app: Express = express();\n\n//? Apply security headers before other middlewares and routes\nconfigureSecurityHeaders(app);\n\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\napp.use(cookieParser());\napp.use(morgan(env.NODE_ENV === \"development\" ? \"dev\" : \"combined\"));\n\n//? Swagger Setup\nsetupSwagger(app);\n\n//? Routes\napp.get(\"/\", (req: Request, res: Response) => {\n  res.redirect(\"/api/v1/health\");\n});\n\napp.use(\"/api\", Routes);\n\n//? Not-found-handler (should be after routes)\napp.use(notFoundHandler);\n\n//? Global error handler (should be last)\napp.use(errorHandler);\n\nexport default app;\n"
                        },
                        {
                          "type": "file",
                          "path": ".husky/pre-commit",
                          "content": "npx lint-staged\n"
                        },
                        {
                          "type": "file",
                          "path": "src/validators/auth.ts",
                          "content": "import * as z from \"zod\";\nimport { OTP_TYPES } from \"../constants/auth\";\n\nexport const nameSchema = z\n  .string({ error: \"Name must be a string\" })\n  .trim()\n  .min(3, {\n    message: \"Name must be at least 3 characters long\"\n  })\n  .max(50, {\n    message: \"Name must be at most 50 characters long\"\n  });\n\nexport const passwordSchema = z\n  .string({ error: \"Password must be a string\" })\n  .trim()\n  .min(6, {\n    message: \"Password must be at least 6 characters long\"\n  })\n  .max(80, {\n    message: \"Password must be at most 80 characters long\"\n  });\n\nexport const emailSchema = z\n  .email({ message: \"Please enter a valid email address.\" })\n  .max(100, { message: \"Email must be no more than 100 characters.\" });\n\nexport const roleSchema = z\n  .enum([\"user\", \"admin\"], {\n    error: \"Role must be either applicant, recruiter, or admin\"\n  })\n  .default(\"user\");\n\nexport const SigninSchema = z.object({\n  email: emailSchema,\n  password: z.string({ error: \"Password must be a string\" }).trim().min(1, {\n    message: \"Password is required\"\n  })\n});\n\nexport const SignupSchema = z\n  .object({\n    name: nameSchema,\n    email: emailSchema,\n    password: passwordSchema,\n    confirmPassword: passwordSchema,\n    role: roleSchema\n  })\n  .refine(\n    data => {\n      return data.password === data.confirmPassword;\n    },\n    {\n      message: \"Passwords do not match\",\n      path: [\"confirmPassword\"]\n    }\n  );\n\nexport const RequestOtpSchema = z.object({\n  email: emailSchema,\n  otpType: z.enum(OTP_TYPES, { error: \"Invalid otp type\" })\n});\n\nexport const VerifyOtpSchema = z.object({\n  otpCode: z.string().min(6, \"Please enter a valid OTP\"),\n  email: emailSchema\n});\n\nexport const ResetPasswordSchema = z.object({\n  email: emailSchema,\n  newPassword: passwordSchema\n});\n\nexport const ChangePasswordSchema = z.object({\n  oldPassword: z.string({ error: \"Password must be a string\" }).min(1, {\n    message: \"Old password is required\"\n  }),\n  newPassword: passwordSchema\n});\n\nexport const UpdateProfileSchema = z.object({\n  name: nameSchema.optional(),\n  avatar: z.string().optional()\n});\n\nexport const GoogleSigninSchema = z.object({\n  name: nameSchema,\n  email: emailSchema,\n  provider: z.enum([\"google\", \"github\"]).default(\"google\"),\n  providerId: z.string({ error: \"Provider id must be a string\" }).min(1, {\n    message: \"Provider id is required\"\n  }),\n  avatar: z.string().optional(),\n  isEmailVerified: z.boolean().default(false)\n});\n\nexport const DeleteAccountSchema = z.object({\n  userId: z.string({ error: \"User id must be a string\" }).min(1, {\n    message: \"User id is required\"\n  }),\n  type: z\n    .enum([\"soft\", \"hard\"], { error: \"Type must be either soft or hard\" })\n    .default(\"soft\")\n});\n\nexport type SignupUserType = z.infer<typeof SignupSchema>;\nexport type SigninUserType = z.infer<typeof SigninSchema>;\nexport type RequestOtpType = z.infer<typeof RequestOtpSchema>;\nexport type VerifyOtpType = z.infer<typeof VerifyOtpSchema>;\nexport type ResetPasswordType = z.infer<typeof ResetPasswordSchema>;\nexport type ChangePasswordType = z.infer<typeof ChangePasswordSchema>;\nexport type UpdateProfileType = z.infer<typeof UpdateProfileSchema>;\nexport type GoogleSigninType = z.infer<typeof GoogleSigninSchema>;\nexport type DeleteAccountType = z.infer<typeof DeleteAccountSchema>;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/types/user.d.ts",
                          "content": "import { Request } from \"express\";\nimport { OTP_TYPES } from \"../constants/auth\";\n\nexport type OTPType = (typeof OTP_TYPES)[number];\n\nexport interface UserRequest extends Request {\n  user?: {\n    _id?: string | undefined;\n    role?: \"user\" | \"admin\" | undefined;\n    sessionId?: string | undefined;\n  };\n}\n\nexport interface IUser {\n  _id: string;\n  name: string;\n  email: string;\n  password?: string;\n  role: \"user\" | \"admin\";\n  isEmailVerified: boolean;\n  lastLoginAt?: Date;\n  failedLoginAttempts: number;\n  lockUntil?: Date;\n  avatar?: {\n    url: string;\n    publicId: string;\n    size: number;\n  };\n  provider: \"local\" | \"google\" | \"github\";\n  providerId?: string;\n  isDeleted: boolean;\n  deletedAt?: Date;\n  reActivateAvailableAt?: Date;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nexport type RefreshTokenData = {\n  userId: string;\n  tokenHash: string;\n  expiresAt: Date;\n};\n\nexport type SessionData = {\n  userId: string;\n  sessionId: string;\n  refreshTokenHash: string;\n  userAgent: string;\n  ip: string;\n  createdAt: Date;\n  expiresAt: Date;\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/shutdown.ts",
                          "content": "import { Server } from \"http\";\nimport { logger } from \"./logger\";\n\nexport const configureGracefulShutdown = (server: Server) => {\n  const signals = [\"SIGTERM\", \"SIGINT\"];\n\n  signals.forEach(signal => {\n    process.on(signal, () => {\n      logger.info(`\\n${signal} signal received. Shutting down gracefully...`);\n\n      server.close(err => {\n        if (err) {\n          logger.error(err, \"Error during server close\");\n          process.exit(1);\n        }\n\n        logger.info(\"HTTP server closed.\");\n        process.exit(0);\n      });\n\n      // Force shutdown after 10 seconds\n      setTimeout(() => {\n        logger.error(\n          \"Could not close connections in time, forcefully shutting down\"\n        );\n        process.exit(1);\n      }, 10000);\n    });\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/send-mail.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport env from \"../configs/env\";\nimport { resend } from \"../configs/resend\";\nimport { renderEmailTemplates } from \"./render-email-template\";\n\nexport type SendMailType = {\n  from?: string;\n  subject: string;\n  data: Record<string, any>;\n  email: string;\n  html?: string;\n  templateName: string;\n};\n\nexport async function sendEmail({\n  from,\n  email,\n  subject,\n  data,\n  html,\n  templateName\n}: SendMailType) {\n  const htmlContent =\n    (await renderEmailTemplates(templateName, data)) || html || \"\";\n\n  return await resend.emails.send({\n    from: from || env.EMAIL_FROM,\n    to: email,\n    subject,\n    replyTo: email,\n    html: htmlContent\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/render-email-template.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport ejs from \"ejs\"; // npm i --save-dev @types/ejs\nimport path from \"node:path\";\n\nexport async function renderEmailTemplates(\n  templateName: string,\n  data: Record<string, any>\n) {\n  const templatePath = path.join(\n    process.cwd(),\n    \"src\",\n    \"email-templates\",\n    `${templateName}.ejs`\n  );\n  return ejs.renderFile(templatePath, data);\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/logger.ts",
                          "content": "import pino from \"pino\";\nimport env from \"../configs/env\";\n\nexport const logger = pino({\n  level: env.LOG_LEVEL,\n  transport:\n    env.NODE_ENV !== \"production\"\n      ? {\n          target: \"pino-pretty\",\n          options: {\n            colorize: true,\n            translateTime: \"yyyy-mm-dd HH:MM:ss\",\n            ignore: \"pid,hostname\"\n          }\n        }\n      : undefined\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/jwt.ts",
                          "content": "import jwt from \"jsonwebtoken\";\nimport env from \"../configs/env\";\n\nconst JWT_ACCESS_TOKEN_EXPIRY = \"15m\";\nconst JWT_REFRESH_TOKEN_EXPIRY = \"7d\";\n\nexport function generateAccessToken(user: {\n  _id: string;\n  role: \"user\" | \"admin\";\n  sessionId: string;\n}) {\n  return jwt.sign(\n    { _id: user._id, role: user.role, sessionId: user.sessionId },\n    env.JWT_ACCESS_SECRET!,\n    {\n      expiresIn: JWT_ACCESS_TOKEN_EXPIRY\n    }\n  );\n}\n\nexport function generateRefreshToken(user: { _id: string; sessionId: string }) {\n  return jwt.sign(\n    { _id: user._id, sessionId: user.sessionId },\n    env.JWT_REFRESH_SECRET!,\n    {\n      expiresIn: JWT_REFRESH_TOKEN_EXPIRY\n    }\n  );\n}\n\nexport function verifyAccessToken(token: string) {\n  return jwt.verify(token, env.JWT_ACCESS_SECRET!) as {\n    _id: string;\n    role: \"user\" | \"admin\";\n    sessionId: string;\n  };\n}\n\nexport function verifyRefreshToken(token: string) {\n  return jwt.verify(token, env.JWT_REFRESH_SECRET!) as {\n    _id: string;\n    sessionId: string;\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/date.ts",
                          "content": "export function getRemainingTime(date: Date) {\n  const now = new Date();\n  let diff = date.getTime() - now.getTime();\n\n  if (diff <= 0) {\n    return {\n      days: 0,\n      minutes: 0,\n      seconds: 0\n    };\n  }\n\n  const seconds = Math.floor((diff / 1000) % 60);\n  const minutes = Math.floor((diff / (1000 * 60)) % 60);\n  const days = Math.floor(diff / (1000 * 60 * 60 * 24));\n\n  return {\n    days,\n    minutes,\n    seconds\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/async-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\n\nexport type AsyncRouteHandler = (\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => Promise<unknown>;\n\nexport function AsyncHandler(fn: AsyncRouteHandler) {\n  return function (req: Request, res: Response, next: NextFunction) {\n    Promise.resolve()\n      .then(() => fn(req, res, next))\n      .catch(next);\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/api-response.ts",
                          "content": "import { STATUS_CODES, StatusCode } from \"../constants/status-codes\";\nimport type { Response } from \"express\";\n\ntype ApiResponseParams<T> = {\n  success: boolean;\n  message: string;\n  statusCode: StatusCode;\n  data?: T | null;\n  errors?: unknown;\n};\n\nexport class ApiResponse<T = unknown> {\n  public readonly success: boolean;\n  public readonly message: string;\n  public readonly statusCode: StatusCode;\n  public readonly data?: T | null;\n  public readonly errors?: unknown;\n\n  constructor({\n    success,\n    message,\n    statusCode,\n    data,\n    errors\n  }: ApiResponseParams<T>) {\n    this.success = success;\n    this.message = message;\n    this.statusCode = statusCode;\n    this.data = data;\n    this.errors = errors;\n  }\n\n  send(res: Response): Response {\n    return res.status(this.statusCode).json({\n      success: this.success,\n      message: this.message,\n      statusCode: this.statusCode,\n      ...(this.data !== undefined && { data: this.data }),\n      ...(this.errors !== undefined && { errors: this.errors })\n    });\n  }\n\n  static Success<T>(\n    res: Response,\n    message: string,\n    data?: T,\n    statusCode: StatusCode = STATUS_CODES.OK\n  ): Response {\n    return new ApiResponse<T>({\n      success: true,\n      message,\n      data,\n      statusCode\n    }).send(res);\n  }\n\n  static ok<T>(res: Response, message = \"OK\", data?: T) {\n    return ApiResponse.Success(res, message, data, STATUS_CODES.OK);\n  }\n\n  static created<T>(res: Response, message = \"Created\", data?: T) {\n    return ApiResponse.Success(res, message, data, STATUS_CODES.CREATED);\n  }\n}\n\n/*\n * Usage:\n * ApiResponse.ok(res, \"OK\", data);\n * ApiResponse.created(res, \"Created\", data);\n */\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/api-error.ts",
                          "content": "import { STATUS_CODES, StatusCode } from \"../constants/status-codes\";\n\nexport class ApiError extends Error {\n  public readonly statusCode: StatusCode;\n  public readonly isOperational: boolean;\n  public readonly errors?: unknown;\n\n  constructor(\n    statusCode: StatusCode,\n    message: string,\n    errors?: unknown,\n    isOperational = true\n  ) {\n    super(message);\n    this.name = \"ApiError\";\n    this.statusCode = statusCode;\n    this.errors = errors;\n    this.isOperational = isOperational;\n\n    Error.captureStackTrace(this, this.constructor);\n  }\n\n  static badRequest(message = \"Bad Request\", errors?: unknown) {\n    return new ApiError(STATUS_CODES.BAD_REQUEST, message, errors);\n  }\n\n  static unauthorized(message = \"Unauthorized\") {\n    return new ApiError(STATUS_CODES.UNAUTHORIZED, message);\n  }\n\n  static forbidden(message = \"Forbidden\") {\n    return new ApiError(STATUS_CODES.FORBIDDEN, message);\n  }\n\n  static notFound(message = \"Not Found\") {\n    return new ApiError(STATUS_CODES.NOT_FOUND, message);\n  }\n\n  static conflict(message = \"Conflict\") {\n    return new ApiError(STATUS_CODES.CONFLICT, message);\n  }\n\n  static server(message = \"Internal Server Error\") {\n    return new ApiError(STATUS_CODES.INTERNAL_SERVER_ERROR, message);\n  }\n\n  static unprocessableEntity(message = \"Unprocessable Entity\") {\n    return new ApiError(STATUS_CODES.UNPROCESSABLE_ENTITY, message);\n  }\n\n  static tooManyRequests(message = \"Too Many Requests\") {\n    return new ApiError(STATUS_CODES.TOO_MANY_REQUESTS, message);\n  }\n}\n\n/*\n  ? Usage:\n  * throw new ApiError(404, \"Not found\");\n  * throw ApiError.badRequest(\"Bad request\");\n */\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/otp.service.ts",
                          "content": "import { logger } from \"@/utils/logger\";\nimport redis from \"../configs/redis\";\nimport {\n  OTP_CODE_LENGTH,\n  OTP_EXPIRES_IN,\n  OTP_MAX_ATTEMPTS,\n  OTP_SPAM_LOCK_TIME,\n  OTP_COOL_DOWN\n} from \"../constants/auth\";\nimport { generateOTP } from \"../helpers/token.helpers\";\nimport { ApiError } from \"../utils/api-error\";\nimport { sendEmail } from \"../utils/send-mail\";\n\ntype SendOtpBase = {\n  name: string;\n  email: string;\n  templateName: string;\n  subject: string;\n};\n\ntype SendOtpWithCode = SendOtpBase & {\n  code: string;\n  hashCode: string;\n};\n\ntype SendOtpWithoutCode = SendOtpBase & {\n  code?: never;\n  hashCode?: never;\n};\n\nexport type SendOtpType = SendOtpWithCode | SendOtpWithoutCode;\n\nexport class OtpService {\n  static async checkOtpRestrictions(email: string) {\n    const otpLock = await redis.get(`otp_lock:${email}`);\n    if (otpLock) {\n      throw ApiError.badRequest(\n        \"Your Account is locked due to multiple failed attempts. Please try again after 30 minutes.\"\n      );\n    }\n\n    if (await redis.get(`otp_spam_lock:${email}`)) {\n      throw ApiError.tooManyRequests(\n        \"Too many otp requests. Please try again after 1 hour before requesting again.\"\n      );\n    }\n\n    if (await redis.get(`otp_cooldown:${email}`)) {\n      throw ApiError.tooManyRequests(\n        \"Too many otp requests. Please try again after 1 minute before requesting new otp.\"\n      );\n    }\n  }\n\n  static async trackOtpRequests(email: string) {\n    try {\n      const otpRequestKey = `otp_request_count:${email}`;\n      let otpRequestsCount = parseInt((await redis.get(otpRequestKey)) || \"0\");\n      if (otpRequestsCount >= OTP_MAX_ATTEMPTS) {\n        await redis.set(`otp_spam_lock:${email}`, \"locked\", {\n          expiration: {\n            type: \"EX\",\n            value: 3600\n          }\n        });\n        throw ApiError.tooManyRequests(\n          \"Too many otp requests. Please try again after 1 hour before requesting again.\"\n        );\n      }\n\n      await redis.set(otpRequestKey, otpRequestsCount + 1, {\n        expiration: {\n          type: \"EX\",\n          value: 3600\n        }\n      });\n    } catch (error) {\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to track otp requests!\");\n    }\n  }\n\n  static async sendOtp({\n    name,\n    email,\n    templateName,\n    code,\n    hashCode,\n    subject\n  }: SendOtpType) {\n    try {\n      const newOtp = generateOTP(OTP_CODE_LENGTH);\n\n      logger.info(`OTP generated successfully: ${code ? code : newOtp.code}`);\n\n      await sendEmail({\n        email,\n        subject,\n        data: {\n          code: code ? code : newOtp.code,\n          name\n        },\n        templateName\n      });\n\n      await redis.set(`otp:${email}`, hashCode ? hashCode : newOtp.hashCode, {\n        expiration: {\n          type: \"EX\",\n          value: OTP_EXPIRES_IN / 1000\n        }\n      });\n\n      await redis.set(`otp_cooldown:${email}`, OTP_COOL_DOWN, {\n        expiration: {\n          type: \"EX\",\n          value: OTP_COOL_DOWN\n        }\n      });\n    } catch (error) {\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to send otp!\");\n    }\n  }\n\n  static async verifyOtp(hashCode: string, email: string) {\n    const hashOtpCodeKey = await redis.get(`otp:${email}`);\n\n    if (!hashOtpCodeKey) {\n      throw ApiError.badRequest(\"Invalid or expired otp\");\n    }\n\n    const failedAttemptsKey = `otp_attempts:${email}`;\n    const failedAttempts = parseInt(\n      (await redis.get(failedAttemptsKey)) || \"0\"\n    );\n\n    if (hashOtpCodeKey !== hashCode) {\n      if (failedAttempts >= OTP_MAX_ATTEMPTS) {\n        await redis.set(`otp_lock:${email}`, \"locked\", {\n          EX: OTP_SPAM_LOCK_TIME / 1000\n        });\n        throw ApiError.tooManyRequests(\n          \"Too many failed attempts. Please try again after 1 hour.\"\n        );\n      }\n      await redis.set(failedAttemptsKey, failedAttempts + 1, {\n        EX: OTP_EXPIRES_IN / 1000\n      });\n      throw ApiError.badRequest(\n        `Incorrect OTP. ${OTP_MAX_ATTEMPTS - failedAttempts} attempts left.`\n      );\n    }\n\n    await redis.del([`otp:${email}`, failedAttemptsKey]);\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/oauth.service.ts",
                          "content": "import { AuthService, CookieOptionsType } from \"./auth.service\";\nimport User from \"../models/user.model\";\n\ntype OAuthProfile = {\n  provider: string;\n  providerId: string;\n  name: string;\n  email: string | undefined;\n  isEmailVerified: boolean;\n  avatar: string | undefined;\n  ip: string;\n  userAgent: string;\n};\n\nexport class OAuthService {\n  static async handleOAuthLogin(\n    user: OAuthProfile,\n    context: CookieOptionsType\n  ) {\n    const existingUser = await User.findOne({ email: user.email });\n\n    if (existingUser) {\n      await User.findByIdAndUpdate(existingUser._id, {\n        provider: user.provider,\n        providerId: user.providerId,\n        isEmailVerified: user.isEmailVerified,\n        avatar: {\n          url: user.avatar\n        }\n      });\n      await AuthService.handleToken(\n        {\n          _id: existingUser._id.toString(),\n          role: existingUser.role,\n          ip: user.ip,\n          userAgent: user.userAgent\n        },\n        context\n      );\n      return existingUser;\n    }\n\n    const newUser = await User.create({\n      name: user.name,\n      email: user.email,\n      isEmailVerified: user.isEmailVerified,\n\n      provider: user.provider,\n      providerId: user.providerId,\n\n      avatar: {\n        url: user.avatar\n      }\n    });\n\n    await AuthService.handleToken(\n      {\n        _id: newUser._id.toString(),\n        role: newUser.role,\n        ip: user.ip,\n        userAgent: user.userAgent\n      },\n      context\n    );\n\n    return newUser;\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/cloudinary.service.ts",
                          "content": "import { DeleteApiResponse } from \"cloudinary\";\nimport cloudinary from \"../configs/cloudinary\";\n\nexport interface UploadOptions {\n  folder: string;\n  resource_type?: \"image\" | \"video\" | \"raw\" | \"auto\";\n}\n\nexport interface CloudinaryUploadResult {\n  url: string;\n  public_id: string;\n  size: number;\n}\n\nexport const uploadToCloudinary = (\n  buffer: Buffer,\n  options: UploadOptions\n): Promise<CloudinaryUploadResult> => {\n  return new Promise((resolve, reject) => {\n    const stream = cloudinary.uploader.upload_stream(\n      {\n        folder: options.folder || \"uploads\",\n        resource_type: options.resource_type || \"auto\"\n      },\n      (error, result) => {\n        if (error || !result) {\n          return reject(error);\n        }\n        resolve({\n          url: result.secure_url,\n          public_id: result.public_id,\n          size: result.bytes\n        });\n      }\n    );\n\n    stream.end(buffer);\n  });\n};\n\nexport const deleteFileFromCloudinary = (\n  publicIds: string[]\n): Promise<DeleteApiResponse> => {\n  return new Promise((resolve, reject) => {\n    cloudinary.api.delete_resources(publicIds, (error, result) => {\n      if (error || !result) {\n        return reject(error);\n      }\n      resolve(result);\n    });\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/auth.service.ts",
                          "content": "import { NextFunction } from \"express\";\nimport User from \"../models/user.model\";\nimport { ApiError } from \"../utils/api-error\";\nimport { hashPassword, verifyPassword } from \"../helpers/auth.helpers\";\nimport { SignupUserType, VerifyOtpType } from \"../validators/auth\";\nimport {\n  DELETE_ACCOUNT_TOKEN_EXPIRY,\n  LOCK_TIME_MS,\n  LOGIN_MAX_ATTEMPTS,\n  OTP_CODE_LENGTH,\n  OTP_EXPIRES_IN,\n  REACTIVATION_AVAILABLE_AT,\n  REFRESH_TOKEN_EXPIRY,\n  RESET_PASSWORD_TOKEN_EXPIRY,\n  SESSION_EXPIRY\n} from \"../constants/auth\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../utils/jwt\";\nimport {\n  generateHashedToken,\n  generateOTP,\n  generateSecureToken,\n  generateUUID\n} from \"../helpers/token.helpers\";\nimport { IUser, RefreshTokenData, SessionData } from \"../types/user\";\nimport { OtpService } from \"./otp.service\";\nimport { deleteFileFromCloudinary } from \"./cloudinary.service\";\nimport redisClient from \"@/configs/redis\";\nimport { logger } from \"@/utils/logger\";\nimport env from \"@/configs/env\";\nimport { sendEmail } from \"@/utils/send-mail\";\nimport { getRemainingTime } from \"@/utils/date\";\n\nexport type CookieOptionsType = {\n  setAuthCookie?: (\n    accessToken: string,\n    refreshToken: string,\n    sessionId: string\n  ) => void;\n};\n\nexport class AuthService {\n  static async registerUser(user: Omit<SignupUserType, \"confirmPassword\">) {\n    try {\n      const { name, email, password, role } = user;\n      const existingUser = await User.findOne({\n        email\n      }).select(\"+password\");\n\n      if (existingUser) {\n        throw ApiError.conflict(\"User with this email already exists\");\n      }\n\n      const pending = await redisClient.get(`user:pending:${email}`);\n\n      if (pending) {\n        throw ApiError.conflict(\n          \"Signup already in progress. Check your email for OTP.\"\n        );\n      }\n\n      const hashedPassword = await hashPassword(password);\n\n      await OtpService.checkOtpRestrictions(email);\n      await OtpService.trackOtpRequests(email);\n\n      const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n      const redisKey = `user:${email}:${hashCode}`;\n      const indexKey = `user:pending:${email}`;\n      await redisClient.set(indexKey, hashCode, {\n        expiration: {\n          type: \"PX\",\n          value: OTP_EXPIRES_IN\n        }\n      });\n      const userData = JSON.stringify({\n        name,\n        email,\n        role,\n        password: hashedPassword\n      });\n\n      await OtpService.sendOtp({\n        name,\n        email,\n        templateName: \"email-verification\",\n        code,\n        hashCode,\n        subject: \"Email Verification\"\n      });\n\n      await redisClient.set(redisKey, userData, {\n        expiration: {\n          type: \"PX\",\n          value: OTP_EXPIRES_IN\n        }\n      });\n    } catch (error) {\n      logger.error(error, \"Failed to register user\");\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to register user\");\n    }\n  }\n\n  static async verifyUser({ email, otpCode }: VerifyOtpType) {\n    const hashCode = generateHashedToken(otpCode);\n\n    await OtpService.verifyOtp(hashCode, email);\n\n    const userData = await redisClient.get(`user:${email}:${hashCode}`);\n\n    if (!userData) {\n      throw ApiError.badRequest(\"Invalid or expired otp\");\n    }\n\n    const { name, email: userEmail, role, password } = JSON.parse(userData);\n\n    const user = await User.create({\n      name,\n      email: userEmail,\n      role,\n      password,\n      isEmailVerified: true\n    });\n\n    await redisClient.del(`user:${email}:${hashCode}`);\n    await redisClient.del(`user:pending:${email}`);\n\n    return {\n      _id: user._id,\n      name,\n      email,\n      role: role,\n      isEmailVerified: true\n    };\n  }\n\n  static async signinUser(\n    {\n      email,\n      password,\n      ip,\n      userAgent\n    }: {\n      email: string;\n      password: string;\n      ip: string;\n      userAgent: string;\n    },\n    setCookie: CookieOptionsType\n  ) {\n    try {\n      const user = await User.findOne({\n        email\n      }).select(\"+password\");\n      if (!user) {\n        throw ApiError.unauthorized(\"Invalid credentials\");\n      }\n\n      if (!user.isEmailVerified) {\n        throw ApiError.unauthorized(\"Email not verified\");\n      }\n\n      if (user.lockUntil && new Date() < user.lockUntil) {\n        throw ApiError.forbidden(\n          `Your account has been locked. Please try again after ${getRemainingTime(user.lockUntil).minutes} minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        );\n      }\n\n      const isPasswordValid = await verifyPassword(\n        password,\n        user.password || \"\"\n      );\n      if (!isPasswordValid) {\n        let lockUntil = null;\n\n        let newAttempts = user.failedLoginAttempts + 1;\n\n        if (newAttempts >= LOGIN_MAX_ATTEMPTS) {\n          lockUntil = new Date(Date.now() + LOCK_TIME_MS);\n        }\n\n        await User.updateOne(\n          {\n            _id: user._id\n          },\n          {\n            failedLoginAttempts: newAttempts,\n            lockUntil\n          }\n        );\n\n        throw ApiError.unauthorized(\"Invalid credentials\");\n      }\n\n      await User.updateOne(\n        {\n          _id: user._id\n        },\n        {\n          $set: {\n            failedLoginAttempts: 0\n          },\n          $unset: {\n            lockUntil: 1\n          }\n        }\n      );\n\n      await AuthService.handleToken(\n        {\n          _id: user._id.toString(),\n          role: user.role,\n          ip,\n          userAgent\n        },\n        setCookie\n      );\n\n      return {\n        id: user.id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        isEmailVerified: user.isEmailVerified\n      };\n    } catch (err) {\n      if (err instanceof ApiError) {\n        throw err;\n      }\n      throw ApiError.server(\"Signin failed\");\n    }\n  }\n\n  static async handleToken(\n    user: Pick<IUser, \"_id\" | \"role\"> & {\n      ip: string;\n      userAgent: string;\n    },\n    context: CookieOptionsType\n  ) {\n    const sessionId = generateUUID();\n\n    const accessToken = generateAccessToken({\n      _id: user._id,\n      role: user.role,\n      sessionId\n    });\n\n    const refreshToken = generateRefreshToken({\n      _id: user._id.toString(),\n      sessionId\n    });\n\n    const hashedRefreshToken = generateHashedToken(refreshToken);\n\n    const refreshTokenData: RefreshTokenData = {\n      userId: user._id,\n      tokenHash: hashedRefreshToken,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    };\n\n    const sessionData: SessionData = {\n      userId: user._id,\n      sessionId,\n      refreshTokenHash: hashedRefreshToken,\n      userAgent: user.userAgent,\n      ip: user.ip,\n      createdAt: new Date(),\n      expiresAt: new Date(Date.now() + SESSION_EXPIRY)\n    };\n\n    const refreshTokenKey = `refreshToken:${hashedRefreshToken}`;\n\n    await redisClient.set(refreshTokenKey, JSON.stringify(refreshTokenData), {\n      expiration: {\n        type: \"PX\",\n        value: REFRESH_TOKEN_EXPIRY\n      }\n    });\n\n    const sessionKey = `session:${sessionId}`;\n\n    const userSessionsKey = `user_sessions:${user._id}`;\n\n    await redisClient.set(sessionKey, JSON.stringify(sessionData), {\n      expiration: {\n        type: \"PX\",\n        value: SESSION_EXPIRY\n      }\n    });\n\n    // add sessionId to user's set\n    await redisClient.sAdd(userSessionsKey, sessionId);\n\n    context.setAuthCookie &&\n      context.setAuthCookie(accessToken, refreshToken, sessionId);\n\n    await User.updateOne(\n      {\n        _id: user._id\n      },\n      {\n        $set: {\n          lastLogin: new Date(),\n          failedLoginAttempts: 0\n        },\n        $unset: {\n          lockUntil: 1\n        }\n      }\n    );\n  }\n\n  static async getUserProfile(userId: string) {\n    const user = await User.findById(userId);\n    return user;\n  }\n\n  static async refreshTokens(accessToken: string | null, refreshToken: string) {\n    if (!refreshToken) {\n      throw ApiError.unauthorized(\"Unauthorized, please login.\");\n    }\n\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n\n    if (!decodedRefresh?._id) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const refreshTokenKey = `refreshToken:${refreshTokenHash}`;\n\n    const storedToken = await redisClient.get(refreshTokenKey);\n    if (!storedToken) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    const { userId, tokenHash, expiresAt } = JSON.parse(\n      storedToken\n    ) as RefreshTokenData;\n\n    if (userId !== decodedRefresh._id) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    // Reuse detection\n    if (!storedToken) {\n      throw ApiError.unauthorized(\"Token reuse detected. Please login again.\");\n    }\n\n    if (expiresAt < new Date()) {\n      throw ApiError.unauthorized(\"Refresh token expired.\");\n    }\n\n    const session = await redisClient.get(\n      `session:${decodedRefresh.sessionId}`\n    );\n\n    if (!session) {\n      throw ApiError.unauthorized(\"Session not found.\");\n    }\n\n    const storedSessionData = JSON.parse(session) as SessionData;\n\n    if (\n      decodedRefresh.sessionId !== storedSessionData.sessionId ||\n      decodedRefresh._id !== storedSessionData.userId\n    ) {\n      throw ApiError.unauthorized(\"Token-session mismatch\");\n    }\n\n    if (accessToken) {\n      const decodedAccess = verifyAccessToken(accessToken);\n      if (decodedAccess._id !== decodedRefresh._id) {\n        throw ApiError.unauthorized(\"Token mismatch.\");\n      }\n    }\n\n    const user = await User.findById(decodedRefresh._id);\n    if (!user) {\n      throw ApiError.unauthorized(\"User not found.\");\n    }\n\n    const newAccessToken = generateAccessToken({\n      _id: user._id.toString(),\n      role: user.role,\n      sessionId: storedSessionData.sessionId\n    });\n\n    const newRefreshToken = generateRefreshToken({\n      _id: user._id.toString(),\n      sessionId: storedSessionData.sessionId\n    });\n    const newRefreshTokenHash = generateHashedToken(newRefreshToken);\n\n    //? Rotate token\n    await Promise.all([\n      redisClient.del(`refreshToken:${tokenHash}`),\n      redisClient.del(`session:${storedSessionData.sessionId}`)\n    ]);\n\n    const refreshTokenData: RefreshTokenData = {\n      userId: user._id.toString(),\n      tokenHash: newRefreshTokenHash,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    };\n    const sessionData: SessionData = {\n      userId: user._id.toString(),\n      sessionId: storedSessionData.sessionId,\n      refreshTokenHash: newRefreshTokenHash,\n      userAgent: storedSessionData.userAgent,\n      ip: storedSessionData.ip,\n      createdAt: new Date(),\n      expiresAt: new Date(Date.now() + SESSION_EXPIRY)\n    };\n\n    const newRefreshTokenKey = `refreshToken:${newRefreshTokenHash}`;\n    const newSessionKey = `session:${storedSessionData.sessionId}`;\n\n    await Promise.all([\n      redisClient.set(newRefreshTokenKey, JSON.stringify(refreshTokenData), {\n        expiration: {\n          type: \"PX\",\n          value: REFRESH_TOKEN_EXPIRY\n        }\n      }),\n      redisClient.set(newSessionKey, JSON.stringify(sessionData), {\n        expiration: {\n          type: \"PX\",\n          value: SESSION_EXPIRY\n        }\n      })\n    ]);\n\n    //? delete old refresh token\n    await redisClient.del(refreshTokenKey);\n\n    return {\n      accessToken: newAccessToken,\n      refreshToken: newRefreshToken,\n      sessionId: storedSessionData.sessionId\n    };\n  }\n\n  static async logoutUser(userId: string, sessionId: string) {\n    const sessionKey = `session:${sessionId}`;\n    const sessionData = await redisClient.get(sessionKey);\n    const userSessionsKey = `user_sessions:${userId}`;\n    if (!sessionData) {\n      throw ApiError.unauthorized(\"Session not found.\");\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.userId !== userId) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const refreshTokenKey = `refreshToken:${session.refreshTokenHash}`;\n\n    await redisClient.del(sessionKey);\n    await redisClient.del(refreshTokenKey);\n    await redisClient.sRem(userSessionsKey, sessionId);\n  }\n\n  static async forgotPassword(email: string) {\n    const user = await User.findOne({\n      email\n    });\n\n    if (!user) {\n      throw ApiError.badRequest(\n        \"If an account exists, a reset code has been sent.\"\n      );\n    }\n\n    const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n    await OtpService.checkOtpRestrictions(email);\n    await OtpService.trackOtpRequests(email);\n\n    const redisKey = `reset_password:${email}:${hashCode}`;\n\n    await redisClient.set(redisKey, hashCode, {\n      expiration: {\n        type: \"PX\",\n        value: RESET_PASSWORD_TOKEN_EXPIRY\n      }\n    });\n\n    await OtpService.sendOtp({\n      email,\n      subject: \"Password Reset\",\n      templateName: \"forgot-password\",\n      name: user.name,\n      code,\n      hashCode\n    });\n  }\n\n  static async verifyResetPasswordOtp(otpCode: string, email: string) {\n    const hashedCode = generateHashedToken(otpCode);\n\n    const redisKey = `reset_password:${email}:${hashedCode}`;\n    const storedHashCode = await redisClient.get(redisKey);\n    if (!storedHashCode) {\n      throw ApiError.unauthorized(\"Invalid or expired otp\");\n    }\n    await OtpService.verifyOtp(storedHashCode, email);\n\n    await redisClient.del(`reset_password:${email}:${hashedCode}`);\n    await redisClient.set(`reset_password:status:${email}`, \"pending\", {\n      expiration: {\n        type: \"PX\",\n        value: RESET_PASSWORD_TOKEN_EXPIRY\n      }\n    });\n  }\n\n  static async resetPassword(\n    next: NextFunction,\n    email: string,\n    newPassword: string\n  ) {\n    const user = await User.findOne({\n      email\n    }).select(\"+password\");\n\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${\n            getRemainingTime(user.lockUntil).minutes\n          } minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        )\n      );\n    }\n\n    if (user.failedLoginAttempts >= LOGIN_MAX_ATTEMPTS && user.lockUntil) {\n      return next(\n        ApiError.forbidden(\n          `You have exceeded the maximum number of login attempts. Please try again after ${\n            getRemainingTime(user.lockUntil).minutes\n          } minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        )\n      );\n    }\n\n    if (!user.isEmailVerified) {\n      return next(ApiError.unauthorized(\"Please verify your email first.\"));\n    }\n\n    const redisKey = `reset_password:status:${email}`;\n    const status = await redisClient.get(redisKey);\n    if (status !== \"pending\") {\n      return next(\n        ApiError.unauthorized(\n          \"Please request a password reset before attempting to set a new password.\"\n        )\n      );\n    }\n\n    const oldPassword = user.password;\n\n    const isOldPassword = await verifyPassword(\n      newPassword,\n      oldPassword as string\n    );\n\n    if (isOldPassword) {\n      return next(ApiError.badRequest(\"New password should be different!\"));\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n    await User.updateOne(\n      {\n        email\n      },\n      {\n        $set: {\n          password: hashedPassword\n        }\n      }\n    );\n    await redisClient.del(`reset_password:status:${email}`);\n\n    //? Delete all user sessions\n    await this.deleteAllUserSessions(user._id.toString());\n\n    return {\n      message: \"Password reset successfully. Please login!\"\n    };\n  }\n\n  static async changePassword(\n    next: NextFunction,\n    {\n      newPassword,\n      oldPassword,\n      userId\n    }: {\n      userId: string;\n      newPassword: string;\n      oldPassword: string;\n    }\n  ) {\n    const user = await User.findById(userId).select(\"+password\");\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    if (!user.isEmailVerified) {\n      return next(ApiError.unauthorized(\"Please verify your email first.\"));\n    }\n\n    const isOldPassword = await verifyPassword(\n      oldPassword,\n      user.password || \"\"\n    );\n\n    if (!isOldPassword) {\n      return next(ApiError.unauthorized(\"Invalid credentials\"));\n    }\n\n    if (newPassword === oldPassword) {\n      return next(ApiError.badRequest(\"New password should be different!\"));\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n    await User.updateOne(\n      {\n        _id: userId\n      },\n      {\n        $set: {\n          password: hashedPassword\n        }\n      }\n    );\n\n    await this.deleteAllUserSessions(userId);\n\n    return {\n      message: \"Password changed successfully. Please login again!\"\n    };\n  }\n\n  static async requestDeleteAccount(userId: string, password: string) {\n    const user = await User.findById(userId).select(\"+password\");\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const isPasswordValid = await verifyPassword(password, user.password || \"\");\n\n    if (!isPasswordValid) {\n      let lockUntil = null;\n\n      let newAttempts = user.failedLoginAttempts + 1;\n\n      if (newAttempts >= LOGIN_MAX_ATTEMPTS) {\n        lockUntil = new Date(Date.now() + LOCK_TIME_MS);\n      }\n\n      await User.updateOne(\n        {\n          _id: user._id\n        },\n        {\n          failedLoginAttempts: newAttempts,\n          lockUntil\n        }\n      );\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    const token = generateSecureToken();\n    const hashedToken = generateHashedToken(token);\n\n    const redisKey = `delete_account:token:${userId}`;\n\n    if (await redisClient.get(redisKey)) {\n      throw ApiError.badRequest(\"Delete account token already requested!\");\n    }\n\n    await redisClient.set(redisKey, hashedToken, {\n      expiration: {\n        type: \"PX\",\n        value: DELETE_ACCOUNT_TOKEN_EXPIRY\n      }\n    });\n\n    const deleteAccountUrl = `${env.CLIENT_URL}/account/delete?token=${token}`;\n    logger.warn(`Delete account token: ${token}`);\n    await sendEmail({\n      email: user.email,\n      subject: \"Delete Account Request\",\n      templateName: \"delete-account\",\n      data: {\n        name: user.name,\n        deleteAccountUrl\n      }\n    });\n  }\n\n  static async deleteOrDeactiveAccount({\n    userId,\n    type,\n    token\n  }: {\n    userId: string;\n    type: \"soft\" | \"hard\";\n    token: string;\n  }) {\n    const user = await User.findById(userId);\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const redisKey = `delete_account:token:${userId}`;\n    const storedToken = await redisClient.get(redisKey);\n    if (!storedToken) {\n      throw ApiError.badRequest(\"Invalid or expired token!\");\n    }\n\n    const isTokenValid = generateHashedToken(token) === storedToken;\n    if (!isTokenValid) {\n      throw ApiError.badRequest(\"Invalid or expired token!\");\n    }\n\n    await redisClient.del(redisKey);\n\n    if (type === \"soft\") {\n      user.isDeleted = true;\n      user.deletedAt = new Date();\n      user.reActivateAvailableAt = new Date(\n        Date.now() + REACTIVATION_AVAILABLE_AT\n      );\n      await user.save();\n      await this.deleteAllUserSessions(userId);\n    } else if (type === \"hard\") {\n      if (user?.avatar?.public_id) {\n        await deleteFileFromCloudinary([user.avatar.public_id]);\n      }\n      await User.findOneAndDelete({\n        _id: userId\n      });\n      await this.deleteAllUserSessions(userId);\n      await user.save();\n    }\n  }\n\n  static async reactivateAccount(userId: string) {\n    const user = await User.findById(userId);\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n      const remainingTime = getRemainingTime(user.lockUntil);\n      throw ApiError.badRequest(\n        `Your account has been locked. Please try again after ${remainingTime.minutes} minutes and ${remainingTime.seconds} seconds.`\n      );\n    }\n\n    if (!user?.isDeleted || !user?.deletedAt) {\n      throw ApiError.badRequest(\"Your account is already active!\");\n    }\n\n    if (\n      user?.reActivateAvailableAt &&\n      new Date(user?.reActivateAvailableAt) > new Date()\n    ) {\n      throw ApiError.forbidden(\n        `Your account has been locked. Please try again after ${\n          getRemainingTime(user.reActivateAvailableAt).minutes\n        } minutes and ${getRemainingTime(user.reActivateAvailableAt).seconds} seconds.`\n      );\n    }\n\n    await User.findOneAndUpdate(\n      {\n        _id: userId\n      },\n      {\n        $set: {\n          isDeleted: false,\n          deletedAt: null,\n          reActivateAvailableAt: null\n        }\n      },\n      {\n        new: true\n      }\n    );\n\n    await user.save();\n  }\n\n  static async getUserSessions(userId: string, currentSid: string) {\n    const sessionIds = await redisClient.sMembers(`user_sessions:${userId}`);\n\n    const sessions = await Promise.all(\n      sessionIds.map(async id => {\n        const data = await redisClient.get(`session:${id}`);\n        return data ? JSON.parse(data) : null;\n      })\n    );\n\n    const filteredData = sessions\n      .filter(Boolean)\n      .map((session: SessionData) => {\n        return {\n          sessionId: session.sessionId,\n          userAgent: session.userAgent,\n          ip: session.ip,\n          createdAt: session.createdAt,\n          expiresAt: session.expiresAt,\n          current: session.sessionId === currentSid\n        };\n      });\n\n    return filteredData;\n  }\n\n  static async deleteUserSession(userId: string, sessionId: string) {\n    const sessionKey = `session:${sessionId}`;\n    const userSessionsKey = `user_sessions:${userId}`;\n    const sessionData = await redisClient.get(sessionKey);\n\n    if (!sessionData) {\n      throw ApiError.unauthorized(\"Session not found.\");\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.userId !== userId) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n    const refreshTokenKey = `refreshToken:${session.refreshTokenHash}`;\n    await redisClient.del(sessionKey);\n    await redisClient.del(refreshTokenKey);\n    await redisClient.sRem(userSessionsKey, sessionId);\n  }\n\n  static async deleteAllUserSessions(userId: string) {\n    const userSessionsKey = `user_sessions:${userId}`;\n    const sessionIds = await redisClient.sMembers(userSessionsKey);\n    if (sessionIds.length) {\n      const keys = sessionIds.map(id => `session:${id}`);\n      await redisClient.del(keys);\n    }\n    await redisClient.del(userSessionsKey);\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/oauth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport passport from \"passport\";\nimport { facebookOAuth, githubOAuth, googleOAuth } from \"../controllers/oauth.controller\";\n\nconst router = Router();\n\nrouter.get(\n  \"/github\",\n  passport.authenticate(\"github\", { scope: [\"user:email\"] })\n);\n\nrouter.get(\n  \"/github/callback\",\n  passport.authenticate(\"github\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed,\n    session: false\n  }),\n  githubOAuth\n);\n\nrouter.get(\n  \"/facebook\",\n  passport.authenticate(\"facebook\", { scope: [\"email\", \"user_location\"] })\n);\n\nrouter.get(\n  \"/facebook/callback\",\n  passport.authenticate(\"facebook\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed,\n    session: false,\n    failureMessage: true\n  }),\n  facebookOAuth\n);\n\nrouter.get(\n  \"/google\",\n  passport.authenticate(\"google\", {\n    scope: [\"email\", \"profile\", \"openid\"],\n    prompt: \"consent\"\n  })\n);\n\nrouter.get(\n  \"/google/callback\",\n  passport.authenticate(\"google\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed\n    session: false\n  }),\n  googleOAuth\n);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/index.ts",
                          "content": "import { Router } from \"express\";\nimport healthRoutes from \"./health.routes\";\nimport authRoutes from \"./auth.routes\";\nimport oauthRoutes from \"./oauth.routes\";\n\nconst router = Router();\n\nrouter.use(\"/v1/health\", healthRoutes);\nrouter.use(\"/v1/auth\", authRoutes);\nrouter.use(\"/auth\", oauthRoutes); //* Here versioning is not given because, in google and github callback routes, we are not using versioning. process.env.GOOGLE_REDIRECT_URI\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/health.routes.ts",
                          "content": "import { Router } from \"express\";\nimport {\n  healthCheck,\n  detailedHealthCheck\n} from \"../controllers/health.controller\";\n\nconst router = Router();\n\nrouter.get(\"/\", healthCheck);\nrouter.get(\"/detailed\", detailedHealthCheck);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/auth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport { validateRequest } from \"../middlewares/validate-request\";\nimport {\n  ChangePasswordSchema,\n  DeleteAccountSchema,\n  RequestOtpSchema,\n  ResetPasswordSchema,\n  SigninSchema,\n  SignupSchema,\n  UpdateProfileSchema,\n  VerifyOtpSchema\n} from \"../validators/auth\";\nimport {\n  changePassword,\n  deleteAccount,\n  deleteAllUserSessions,\n  deleteUserSession,\n  forgotPassword,\n  getUserProfile,\n  getUserSessions,\n  logoutUser,\n  reactivateAccount,\n  refreshToken,\n  requestDeleteAccount,\n  resetPassword,\n  signinUser,\n  signupUser,\n  updateProfile,\n  verifyResetPasswordOtp,\n  verifyUser\n} from \"../controllers/auth.controller\";\nimport { verifyAuthentication } from \"../middlewares/verify-auth\";\nimport { checkUserAccountRestriction } from \"../middlewares/user-account-restriction\";\nimport {\n  changePasswordLimiter,\n  deleteAccountLimiter,\n  otpRequestLimiter,\n  resetPasswordLimiter,\n  signinRateLimiter,\n  signupRateLimiter\n} from \"../middlewares/rate-limiter\";\nimport upload from \"../middlewares/upload-file\";\n\nconst router = Router();\n\nrouter.post(\n  \"/signup\",\n  validateRequest(SignupSchema),\n  signupRateLimiter,\n  signupUser\n);\n\nrouter.post(\"/verify-user\", validateRequest(VerifyOtpSchema), verifyUser);\n\nrouter.post(\n  \"/signin\",\n  validateRequest(SigninSchema),\n  signinRateLimiter,\n  signinUser\n);\n\nrouter.get(\"/profile\", verifyAuthentication, getUserProfile);\n\nrouter.patch(\n  \"/profile\",\n  upload.single(\"avatar\"),\n  validateRequest(UpdateProfileSchema),\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  updateProfile\n);\n\nrouter.get(\"/sessions\", verifyAuthentication, getUserSessions);\n\nrouter.delete(\n  \"/sessions\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  deleteAllUserSessions\n);\n\nrouter.delete(\n  \"/sessions/:sessionId\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  deleteUserSession\n);\n\nrouter.post(\"/refresh-token\", refreshToken);\n\nrouter.post(\n  \"/logout\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  logoutUser\n);\n\nrouter.post(\n  \"/forgot-password\",\n  validateRequest(RequestOtpSchema.pick({ email: true })),\n  otpRequestLimiter,\n  forgotPassword\n);\n\nrouter.post(\n  \"/verify-reset-otp\",\n  validateRequest(VerifyOtpSchema),\n  otpRequestLimiter,\n  verifyResetPasswordOtp\n);\n\nrouter.post(\n  \"/reset-password\",\n  validateRequest(ResetPasswordSchema),\n  resetPasswordLimiter,\n  resetPassword\n);\n\nrouter.post(\n  \"/change-password\",\n  verifyAuthentication,\n  validateRequest(ChangePasswordSchema),\n  checkUserAccountRestriction,\n  changePasswordLimiter,\n  changePassword\n);\n\nrouter.post(\n  \"/account/request-delete\",\n  verifyAuthentication,\n  validateRequest(SigninSchema.pick({ password: true })),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  requestDeleteAccount\n);\n\nrouter.delete(\n  \"/account/delete\",\n  verifyAuthentication,\n  validateRequest(DeleteAccountSchema),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  deleteAccount\n);\n\nrouter.put(\"/account/reactivate\", verifyAuthentication, reactivateAccount);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/models/user.model.ts",
                          "content": "import mongoose, { Document, Model, Schema } from \"mongoose\";\n\nexport interface IAvatar {\n  public_id: string;\n  url: string;\n  size: number;\n}\n\nexport interface IUser extends Document {\n  _id: mongoose.Types.ObjectId;\n  name: string;\n  email: string;\n  password?: string;\n  role: \"user\" | \"admin\";\n  isEmailVerified: boolean;\n  lastLoginAt?: Date;\n  failedLoginAttempts: number;\n  lockUntil?: Date;\n  avatar?: IAvatar;\n\n  provider: \"local\" | \"google\" | \"github\";\n  providerId?: string;\n\n  isDeleted: boolean;\n  deletedAt?: Date | null;\n  reActivateAvailableAt?: Date | null;\n\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nconst userSchema = new Schema<IUser>(\n  {\n    name: {\n      type: String,\n      required: [true, \"Name is required\"],\n      trim: true\n    },\n    email: {\n      type: String,\n      required: [true, \"Email is required\"],\n      unique: true,\n      lowercase: true,\n      trim: true\n    },\n    password: {\n      type: String,\n      select: false,\n      default: null\n    },\n    provider: {\n      type: String,\n      enum: [\"local\", \"google\", \"github\"],\n      default: \"local\"\n    },\n    providerId: {\n      type: String,\n      default: null\n    },\n    role: {\n      type: String,\n      enum: [\"user\", \"admin\"],\n      default: \"user\"\n    },\n    avatar: {\n      public_id: String,\n      url: String,\n      size: Number\n    },\n    isEmailVerified: {\n      type: Boolean,\n      default: false\n    },\n    lastLoginAt: {\n      type: Date\n    },\n    failedLoginAttempts: {\n      type: Number,\n      required: true,\n      default: 0\n    },\n    lockUntil: {\n      type: Date\n    },\n    isDeleted: {\n      type: Boolean,\n      default: false\n    },\n    deletedAt: {\n      type: Date,\n      default: null\n    },\n    reActivateAvailableAt: {\n      type: Date,\n      default: null\n    }\n  },\n  {\n    timestamps: true\n  }\n);\n\n// Performance Indexes\nuserSchema.index({ provider: 1, providerId: 1 }); // Quick lookup for OAuth\nuserSchema.index({ role: 1 });\nuserSchema.index({ isDeleted: 1 }); // Optimized for soft-delete queries\n\nconst User: Model<IUser> =\n  mongoose.models.User || mongoose.model<IUser>(\"User\", userSchema);\n\nexport default User;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/verify-auth.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Response } from \"express\";\nimport { verifyAccessToken } from \"../utils/jwt\";\nimport { ApiError } from \"../utils/api-error\";\nimport { SessionData, UserRequest } from \"../types/user\";\nimport redisClient from \"@/configs/redis\";\n\nexport async function verifyAuthentication(\n  req: UserRequest,\n  _res: Response,\n  next: NextFunction\n): Promise<void> {\n  const authHeader = req.headers.authorization || \"\";\n  const token = authHeader.startsWith(\"Bearer \")\n    ? authHeader.split(\" \")[1]\n    : null;\n\n  const accessToken = req.cookies?.accessToken || token;\n  if (!accessToken) {\n    return next(ApiError.unauthorized(\"Missing access token\"));\n  }\n\n  try {\n    const decoded = verifyAccessToken(accessToken);\n\n    const sessionKey = `session:${decoded.sessionId}`;\n    const sessionData = await redisClient.get(sessionKey);\n    if (!sessionData) {\n      return next(ApiError.unauthorized(\"Session not found\"));\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.ip !== req.ip) {\n      return next(ApiError.unauthorized(\"Suspicious session\"));\n    }\n\n    if (session.userAgent !== req.headers[\"user-agent\"]) {\n      return next(ApiError.unauthorized(\"Suspicious session\"));\n    }\n\n    if (session.expiresAt < new Date()) {\n      return next(ApiError.unauthorized(\"Session expired\"));\n    }\n\n    req.user = decoded;\n    return next();\n  } catch (err: any) {\n    if (err.name === \"TokenExpiredError\") {\n      return next(ApiError.unauthorized(\"Access token expired\"));\n    }\n    return next(ApiError.unauthorized(\"Invalid access token\"));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/validate-request.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport z, { ZodError, type ZodObject } from \"zod\";\n\nimport { ApiError } from \"../utils/api-error\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const validateRequest = (schema: ZodObject<any>) => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    try {\n      schema.parse(req.body);\n\n      next();\n    } catch (error) {\n      if (!(error instanceof ZodError)) {\n        return next(error);\n      }\n\n      return next(\n        ApiError.badRequest(\n          \"Invalid request data\",\n          z.flattenError(error).fieldErrors || z.flattenError(error)\n        )\n      );\n    }\n  };\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/validate-id.ts",
                          "content": "import { isValidObjectId } from \"mongoose\";\nimport { ApiError } from \"../utils/api-error\";\nimport { NextFunction, Request, Response } from \"express\";\n\nexport const validateObjectId = (paramName: string = \"id\") => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const value =\n      req?.params[paramName] || req?.body[paramName] || req?.query[paramName];\n    if (!value) {\n      throw ApiError.badRequest(`${paramName} is required`);\n    }\n\n    if (!isValidObjectId(value)) {\n      throw ApiError.badRequest(`Invalid ${paramName}`);\n    }\n\n    next();\n  };\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/user-account-restriction.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Response } from \"express\";\nimport { UserRequest } from \"../types/user\";\nimport User from \"../models/user.model\";\nimport { ApiError } from \"../utils/api-error\";\nimport { logger } from \"../utils/logger\";\nimport { getRemainingTime } from \"@/utils/date\";\n\nexport async function checkUserAccountRestriction(\n  req: UserRequest,\n  _res: Response,\n  next: NextFunction\n): Promise<void> {\n  try {\n    if (!req.user?._id) {\n      return next(ApiError.unauthorized(\"Unauthorized\"));\n    }\n\n    const user = await User.findById(req.user._id);\n\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n    }\n\n    if (user.isDeleted || user.deletedAt) {\n      return next(ApiError.forbidden(\"Your account has been deactivated.\"));\n    }\n\n    if (user.lockUntil && user.lockUntil.getTime() > Date.now()) {\n      const remainingTime = getRemainingTime(user.lockUntil);\n\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${remainingTime.minutes} minutes and ${remainingTime.seconds} seconds.`\n        )\n      );\n    }\n\n    if (!user.isEmailVerified) {\n      return next(\n        ApiError.forbidden(\"Email not verified. Please verify your email.\")\n      );\n    }\n\n    return next();\n  } catch (err: any) {\n    logger.error(err?.message || err);\n    return next(ApiError.server(\"Something went wrong\"));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/upload-file.ts",
                          "content": "import multer from \"multer\";\n\nexport const ALLOWED_FILE_TYPES = [\n  \"image/jpeg\",\n  \"image/png\",\n  \"image/webp\",\n  \"video/mp4\",\n  \"video/mpeg\",\n  \"video/quicktime\",\n  \"application/pdf\"\n];\n\nexport const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB\n\nconst storage = multer.memoryStorage();\n\nconst fileFilter: multer.Options[\"fileFilter\"] = (_req, file, cb) => {\n  if (!ALLOWED_FILE_TYPES.includes(file.mimetype)) {\n    return cb(null, false);\n  }\n  cb(null, true);\n};\n\nconst upload = multer({\n  storage,\n  limits: { fileSize: MAX_FILE_SIZE },\n  fileFilter\n});\n\nexport default upload;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/security-header.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport cors from \"cors\";\nimport { Express } from \"express\";\nimport helmet from \"helmet\";\nimport env from \"../configs/env\";\n\nexport const configureSecurityHeaders = (app: Express) => {\n  // Use Helmet to set various security-related HTTP headers\n  app.use(helmet());\n\n  // Configure CORS\n  app.use(\n    cors({\n      origin: env.CORS_ORIGIN || \"*\",\n      credentials: true,\n      methods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\"],\n      allowedHeaders: [\"Content-Type\", \"Authorization\", \"X-Requested-With\"]\n    })\n  );\n\n  // Additional custom security headers\n  app.use((req: Request, res: Response, next: NextFunction) => {\n    res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n    res.setHeader(\"X-Frame-Options\", \"DENY\");\n    res.setHeader(\"X-XSS-Protection\", \"1; mode=block\");\n    next();\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/rate-limiter.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Request, Response } from \"express\";\nimport { rateLimit } from \"express-rate-limit\";\nimport { STATUS_CODES } from \"../constants/status-codes\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const rateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000, // 15 minutes\n  max: 100, // Limit each IP to 100 requests per window\n  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers\n  legacyHeaders: false, // Disable the `X-RateLimit-*` headers\n  message: {\n    success: false,\n    message:\n      \"Too many requests from this IP, please try again after 15 minutes\",\n    status: 429\n  },\n  handler: (req: Request, res: Response, next: NextFunction, options: any) => {\n    next(new ApiError(STATUS_CODES.TOO_MANY_REQUESTS, options.message.message));\n  }\n});\n\n/**\n * Stricter rate limiter for sensitive routes (e.g., auth, login)\n */\nexport const authRateLimiter = rateLimit({\n  windowMs: 60 * 60 * 1000, // 1 hour\n  max: 5, // Limit each IP to 5 failed attempts per hour\n  handler: (req, res, next, options) => {\n    next(\n      ApiError.tooManyRequests(\n        \"Too many login attempts, please try again after an hour\"\n      )\n    );\n  }\n});\n\n/**\n * Rate limiter for login route\n */\nexport const signinRateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many login attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\n/**\n * Rate limiter for registration route\n */\nexport const signupRateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many registration attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const otpRequestLimiter = rateLimit({\n  windowMs: 10 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many OTP requests. Please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const otpVerificationLimiter = rateLimit({\n  windowMs: 10 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many OTP verification attempts. Please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const resetPasswordLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many password reset attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const deleteAccountLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many account deletion attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const changePasswordLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many password change attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/not-found-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const notFoundHandler = (\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => {\n  throw ApiError.notFound(`Route ${req.method} ${req.originalUrl} not found`);\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/error-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport env from \"../configs/env\";\n\nimport { logger } from \"../utils/logger\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const errorHandler = (\n  err: Error,\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => {\n  if (res.headersSent) {\n    return next(err);\n  }\n  let statusCode = 500;\n  let message = \"Internal server error\";\n  let errors: unknown;\n\n  if (err instanceof ApiError) {\n    statusCode = err.statusCode;\n    message = err.message;\n    errors = err.errors;\n  }\n\n  logger.error(\n    err,\n    `Error: ${message} | Status: ${statusCode} | Path: ${req.method} ${req.originalUrl}`\n  );\n\n  const response = {\n    success: false,\n    message,\n    statusCode,\n    ...(errors !== undefined && { errors }),\n    ...(env.NODE_ENV === \"development\" && { stack: err.stack })\n  };\n\n  res.status(statusCode).json(response);\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/helpers/token.helpers.ts",
                          "content": "import crypto from \"node:crypto\";\n\nexport function generateOTP(length: number = 6, ttlMinutes: number = 5) {\n  const code = crypto\n    .randomInt(0, Math.pow(10, length))\n    .toString()\n    .padStart(length, \"0\");\n\n  const hashCode = crypto\n    .createHash(\"sha256\")\n    .update(String(code))\n    .digest(\"hex\");\n\n  const expiresAt = new Date(Date.now() + ttlMinutes * 60 * 1000).toISOString();\n\n  return { code, hashCode, expiresAt };\n}\n\nexport function generateHashedToken(token: string): string {\n  return crypto.createHash(\"sha256\").update(String(token)).digest(\"hex\");\n}\n\nexport function generateSecureToken(length: number = 32): string {\n  return crypto.randomBytes(length).toString(\"hex\");\n}\n\nexport function verifyHashedToken(token: string, hashedToken: string): boolean {\n  return (\n    crypto.createHash(\"sha256\").update(String(token)).digest(\"hex\") ===\n    hashedToken\n  );\n}\n\nexport function generateTokenAndHashedToken(id: string) {\n  const cryptoSecret = process.env.CRYPTO_SECRET! || \"secret\";\n  const token = crypto\n    .createHmac(\"sha256\", cryptoSecret)\n    .update(String(id))\n    .digest(\"hex\");\n\n  const hashedToken = crypto\n    .createHash(\"sha256\")\n    .update(String(token))\n    .digest(\"hex\");\n  return { token, hashedToken };\n}\n\nexport function generateUUID(): string {\n  return crypto.randomUUID();\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/helpers/cookie.helper.ts",
                          "content": "import { Response } from \"express\";\nimport {\n  ACCESS_TOKEN_EXPIRY,\n  REFRESH_TOKEN_EXPIRY,\n  SESSION_EXPIRY\n} from \"../constants/auth\";\nimport env from \"../configs/env\";\n\nconst isProduction = env.NODE_ENV === \"production\";\n\nexport const COOKIE_OPTIONS = {\n  httpOnly: true,\n  secure: isProduction,\n  sameSite: isProduction ? (\"none\" as const) : (\"lax\" as const),\n  path: \"/\"\n};\n\nexport function setAuthCookies(\n  res: Response,\n  accessToken: string,\n  refreshToken: string,\n  sessionId: string\n) {\n  setCookies(res, [\n    {\n      cookie: \"accessToken\",\n      value: accessToken,\n      maxAge: ACCESS_TOKEN_EXPIRY\n    },\n    {\n      cookie: \"refreshToken\",\n      value: refreshToken,\n      maxAge: REFRESH_TOKEN_EXPIRY,\n      path: \"/api/v1/auth/refresh-token\"\n    },\n    {\n      cookie: \"sid\",\n      value: sessionId,\n      maxAge: SESSION_EXPIRY\n    }\n  ]);\n}\n\nexport function clearAuthCookies(res: Response) {\n  clearCookie(res, \"accessToken\");\n  clearCookie(res, \"refreshToken\");\n  clearCookie(res, \"sid\");\n}\n\nexport function clearCookie(res: Response, cookie: string = \"sid\") {\n  res.clearCookie(cookie, COOKIE_OPTIONS);\n}\n\ntype Cookie = {\n  cookie: string;\n  value: string;\n  maxAge: number;\n  path?: string;\n};\n\nexport function setCookies(res: Response, cookies: Cookie[]) {\n  cookies.forEach(({ cookie, value, maxAge, path = \"/\" }) => {\n    res.cookie(cookie, value, {\n      ...COOKIE_OPTIONS,\n      path,\n      maxAge\n    });\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/helpers/auth.helpers.ts",
                          "content": "import { OTPType } from \"@/types/user\";\nimport argon2 from \"argon2\";\n\nexport async function hashPassword(password: string): Promise<string> {\n  return argon2.hash(password);\n}\n\nexport async function verifyPassword(\n  password: string,\n  hash: string\n): Promise<boolean> {\n  return argon2.verify(hash, password);\n}\n\nexport const buildRedisKey = (\n  email: string,\n  otpType: OTPType,\n  suffix: string\n) => `otp:${suffix}:${email}:${otpType}`;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/forgot-password.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Verify your OTP</title>\r\n</head>\r\n\r\n<body\r\n  style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f7; margin: 0; padding: 0; color: #51545e;\">\r\n  <div style=\"background-color: #f4f4f7; padding: 40px 0;\">\r\n    <div\r\n      style=\"margin: 0 auto; background-color: #ffffff; padding: 40px 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px;\">\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <h1 style=\"color: #333333; font-size: 24px; font-weight: 700; margin: 0;\">\r\n          Forgot Password - Verify OTP\r\n        </h1>\r\n      </div>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 24px; color: #51545e;\">Hello <strong>\r\n          <%= name %>\r\n        </strong>,</p>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 30px; color: #51545e;\">\r\n        Thank you for using our service. Please use the following One-Time Password (OTP) to verify your password reset\r\n        request. This\r\n        code is valid for 5 minutes.\r\n      </p>\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <div\r\n          style=\"display: inline-block; padding: 16px 40px; background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;\">\r\n          <span\r\n            style=\"font-family: 'Courier New', Courier, monospace; font-size: 32px; font-weight: 700; letter-spacing: 6px; color: #1e293b;\">\r\n            <%= code %>\r\n          </span>\r\n        </div>\r\n      </div>\r\n\r\n      <p style=\"font-size: 14px; line-height: 1.6; color: #64748b; margin-top: 24px;\">\r\n        If you didn't request this request, you can safely ignore this email.\r\n      </p>\r\n\r\n      <div style=\"margin-top: 40px; padding-top: 24px; border-top: 1px solid #e2e8f0; text-align: center;\">\r\n        <p style=\"font-size: 12px; color: #94a3b8; margin: 0;\">\r\n          &copy; <%= new Date().getFullYear() %>. All rights reserved.\r\n        </p>\r\n      </div>\r\n\r\n    </div>\r\n  </div>\r\n</body>\r\n\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/email-verification.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Verify your email</title>\r\n</head>\r\n\r\n<body\r\n  style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f7; margin: 0; padding: 0; color: #51545e;\">\r\n  <div style=\"background-color: #f4f4f7; padding: 40px 0;\">\r\n    <div\r\n      style=\"margin: 0 auto; background-color: #ffffff; padding: 40px 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px;\">\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <h1 style=\"color: #333333; font-size: 24px; font-weight: 700; margin: 0;\">Verify your email</h1>\r\n      </div>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 24px; color: #51545e;\">Hello <strong>\r\n          <%= name %>\r\n        </strong>,</p>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 30px; color: #51545e;\">\r\n        Thank you for registering. Please use the following One-Time Password (OTP) to verify your email address. This\r\n        code is valid for 5 minutes.\r\n      </p>\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <div\r\n          style=\"display: inline-block; padding: 16px 40px; background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;\">\r\n          <span\r\n            style=\"font-family: 'Courier New', Courier, monospace; font-size: 32px; font-weight: 700; letter-spacing: 6px; color: #1e293b;\">\r\n            <%= code %>\r\n          </span>\r\n        </div>\r\n      </div>\r\n\r\n      <p style=\"font-size: 14px; line-height: 1.6; color: #64748b; margin-top: 24px;\">\r\n        If you didn't request this verification, you can safely ignore this email.\r\n      </p>\r\n\r\n      <div style=\"margin-top: 40px; padding-top: 24px; border-top: 1px solid #e2e8f0; text-align: center;\">\r\n        <p style=\"font-size: 12px; color: #94a3b8; margin: 0;\">\r\n          &copy; <%= new Date().getFullYear() %>. All rights reserved.\r\n        </p>\r\n      </div>\r\n\r\n    </div>\r\n  </div>\r\n</body>\r\n\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/delete-account.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Delete Account Request</title>\r\n  <style>\r\n    body {\r\n      font-family: Arial, sans-serif;\r\n      line-height: 1.6;\r\n      margin: 0;\r\n      padding: 20px;\r\n      color: #333;\r\n    }\r\n    p {\r\n      margin-bottom: 10px;\r\n    }\r\n    a {\r\n      color: #007bff;\r\n      text-decoration: none;\r\n    }\r\n    a:hover {\r\n      text-decoration: underline;\r\n    }\r\n  </style>\r\n</head>\r\n<body>\r\n  <p>Hello <%= name %>,</p>\r\n  <p>We received a request to delete your account. If you confirm this action, your account will be permanently deleted.</p>\r\n  <p>To confirm, please click the link below:</p>\r\n  <a href=\"<%= deleteAccountUrl %>\">Confirm Delete Account</a>\r\n  <p>If you did not request this action, please ignore this email or reply to let us know. Your account is still secure.</p>\r\n  <p>Thank you,</p>\r\n</body>\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/docs/swagger.json",
                          "content": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"Hybrid Auth API\",\n    \"description\": \"Hybrid Auth API\",\n    \"version\": \"1.0.0\"\n  },\n  \"host\": \"localhost:9000/api\",\n  \"basePath\": \"/\",\n  \"schemes\": [\"http\"],\n  \"paths\": {\n    \"/verify-otp\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"example\": \"any\"\n                },\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                },\n                \"role\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/signin\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/profile\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      },\n      \"patch\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/refresh-token\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/logout\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/reset-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/change-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/delete-account\": {\n      \"delete\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/reactivate-account\": {\n      \"put\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/detailed\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/health/\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/health/detailed\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/verify-otp\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"example\": \"any\"\n                },\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                },\n                \"role\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/signin\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/profile\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      },\n      \"patch\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/refresh-token\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/logout\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/reset-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/change-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/delete-account\": {\n      \"delete\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/reactivate-account\": {\n      \"put\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/github\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/github/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/google\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/google/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/github\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/github/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/google\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/google/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    }\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/controllers/oauth.controller.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport { Profile as GithubProfile } from \"passport-github2\";\nimport { Profile as GoogleProfile } from \"passport-google-oauth20\";\nimport { Profile as FacebookProfile } from \"passport-facebook\";\n\nimport { ApiResponse } from \"../utils/api-response\";\nimport { AsyncHandler } from \"../utils/async-handler\";\nimport { ApiError } from \"../utils/api-error\";\nimport { OAuthService } from \"../services/oauth.service\";\nimport { setAuthCookies } from \"../helpers/cookie.helper\";\n\n//? LOGIN WITH GITHUB\nexport const githubOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as GithubProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authenticated failed!\"));\n    }\n\n    const user = {\n      provider: data?.provider,\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails && data?.emails[0]?.value,\n      isEmailVerified: true,\n      avatar: data.photos && data.photos[0].value,\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(user, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    //? save the data into your databases\n\n    ApiResponse.ok(res, \"Signin Successfull\", {\n      user: {\n        _id: existingUser._id.toString(),\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n\n//? LOGIN WITH GOOGLE\nexport const googleOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as GoogleProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authenticated failed!\"));\n    }\n\n    const userInfo = {\n      provider: data?.provider,\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails && data?.emails[0]?.value,\n      isEmailVerified:\n        (data?.emails && data?.emails[0]?.verified === true) || true,\n      avatar: data.profileUrl || (data.photos && data.photos[0].value),\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    ApiResponse.ok(res, \"Signin Successfull\", {\n      user: {\n        _id: existingUser._id.toString(),\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n\n//? LOGIN WITH FACEBOOK\nexport const facebookOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as FacebookProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authenticated failed!\"));\n    }\n\n    const userInfo = {\n      provider: data?.provider,\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails && data?.emails[0]?.value,\n      isEmailVerified: true,\n      avatar: data.profileUrl || (data.photos && data.photos[0].value),\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    ApiResponse.ok(res, \"Signin Successfull\", {\n      user: {\n        _id: existingUser._id.toString(),\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/controllers/health.controller.ts",
                          "content": "import { Request, Response } from \"express\";\nimport { ApiResponse } from \"../utils/api-response\";\nimport { AsyncHandler } from \"../utils/async-handler\";\n\n/**\n * Basic health check endpoint\n * GET /api/health\n */\nexport const healthCheck = AsyncHandler(\n  async (_req: Request, res: Response) => {\n    return ApiResponse.Success(res, \"Service is healthy\", {\n      status: \"healthy\",\n      timestamp: new Date().toISOString(),\n      uptime: process.uptime()\n    });\n  }\n);\n\n/**\n * Detailed health check with system information\n * GET /api/health/detailed\n */\nexport const detailedHealthCheck = AsyncHandler(\n  async (_req: Request, res: Response) => {\n    const healthData = {\n      status: \"healthy\",\n      timestamp: new Date().toISOString(),\n      uptime: process.uptime(),\n      environment: process.env.NODE_ENV || \"development\",\n      version: process.env.npm_package_version || \"1.0.0\",\n      memory: {\n        used:\n          Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) /\n          100,\n        total:\n          Math.round((process.memoryUsage().heapTotal / 1024 / 1024) * 100) /\n          100,\n        unit: \"MB\"\n      },\n      cpu: {\n        usage: process.cpuUsage()\n      }\n    };\n\n    return ApiResponse.Success(res, \"Service is healthy\", healthData);\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/controllers/auth.controller.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport { ApiResponse } from \"../utils/api-response\";\nimport { AsyncHandler } from \"../utils/async-handler\";\n\nimport { ApiError } from \"../utils/api-error\";\nimport { AuthService } from \"../services/auth.service\";\nimport {\n  clearAuthCookies,\n  clearCookie,\n  setAuthCookies\n} from \"../helpers/cookie.helper\";\nimport { UserRequest } from \"../types/user\";\nimport {\n  deleteFileFromCloudinary,\n  uploadToCloudinary\n} from \"../services/cloudinary.service\";\nimport { DeleteAccountType, VerifyOtpType } from \"../validators/auth\";\n\n//? SIGNUP USER\nexport const signupUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { name, email, password, role } = req.body;\n    if (!name || !email || !password) {\n      return next(ApiError.badRequest(\"Name, email and password are required\"));\n    }\n\n    await AuthService.registerUser({\n      name,\n      email,\n      password,\n      role\n    });\n\n    return ApiResponse.Success(\n      res,\n      \"User registered successfully. Please check your email for verification.\"\n    );\n  }\n);\n\n//? VERIFY USER\nexport const verifyUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, otpCode }: VerifyOtpType = req.body;\n\n    if (!email || !otpCode) {\n      return next(ApiError.badRequest(\"Email and code are required\"));\n    }\n\n    await AuthService.verifyUser({ email, otpCode });\n\n    return ApiResponse.ok(res, \"User verified successfully\");\n  }\n);\n\n//? SIGNIN USER\nexport const signinUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, password } = req.body;\n    if (!email || !password) {\n      return next(ApiError.badRequest(\"Email and password are required\"));\n    }\n\n    const ip = req.ip || \"Unknown\";\n    const userAgent = req.headers[\"user-agent\"] || \"Unknown\";\n\n    await AuthService.signinUser(\n      { email, password, ip, userAgent },\n      {\n        setAuthCookie: (\n          accessToken: string,\n          refreshToken: string,\n          sessionId: string\n        ) => {\n          setAuthCookies(res, accessToken, refreshToken, sessionId);\n        }\n      }\n    );\n\n    return ApiResponse.ok(res, \"User signed in successfully!\");\n  }\n);\n\n//? GET USER PROFILE\nexport const getUserProfile = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(userId.toString());\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    if (user.isDeleted) {\n      return next(ApiError.notFound(\"This account has been deactivated.\"));\n    }\n\n    const result = await AuthService.getUserSessions(\n      userId.toString(),\n      currentSessionId\n    );\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to get user sessions!\"));\n    }\n\n    return ApiResponse.ok(res, \"User profile fetched successfully\", {\n      user: {\n        _id: user._id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        avatar: user.avatar,\n        isEmailVerified: user.isEmailVerified,\n        lastLoginAt: user.lastLoginAt,\n        sessions: result\n      }\n    });\n  }\n);\n\n//? UPDATE PROFILE\nexport const updateProfile = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const data = req.body;\n    const { name } = data;\n\n    if (!req.user?._id) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(req.user?._id.toString());\n\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    if (req?.file && user?.avatar?.public_id) {\n      await deleteFileFromCloudinary([user.avatar.public_id]);\n    }\n\n    if (req?.file && user?.avatar) {\n      const file = await uploadToCloudinary(req.file.buffer, {\n        folder: \"uploads/files\",\n        resource_type: \"auto\"\n      });\n      user.avatar = {\n        public_id: req.file\n          ? file.public_id\n          : (user?.avatar?.public_id as string),\n        url: req.file ? file.url : (user.avatar.url as string),\n        size: req.file ? file.size : (user.avatar.size as number)\n      };\n    }\n\n    if (name) {\n      user.name = name;\n    }\n\n    await user.save();\n\n    return ApiResponse.Success(res, \"Profile updated successfully!\", {\n      user: {\n        _id: user._id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        avatar: user.avatar,\n        isEmailVerified: user.isEmailVerified,\n        lastLoginAt: user.lastLoginAt\n      }\n    });\n  }\n);\n\n//? REFRESH TOKENS\nexport const refreshToken = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const accessToken = req.cookies?.accessToken;\n    const refreshToken = req.cookies?.refreshToken;\n\n    const token = await AuthService.refreshTokens(accessToken, refreshToken);\n\n    if (!token) {\n      return next(ApiError.server(\"Failed to refresh tokens!\"));\n    }\n\n    const newAccessToken = token.accessToken;\n    const newRefreshToken = token.refreshToken;\n    setAuthCookies(res, newAccessToken, newRefreshToken, token.sessionId);\n    clearCookie(res, \"refreshToken\");\n\n    return ApiResponse.Success(res, \"Tokens refreshed successfully!\");\n  }\n);\n\n//? LOGOUT\nexport const logoutUser = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req.user?._id;\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const currentSessionId = req.user?.sessionId;\n    if (!currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.logoutUser(userId.toString(), currentSessionId);\n\n    clearAuthCookies(res);\n\n    return ApiResponse.Success(res, \"Logged out successfully!\");\n  }\n);\n\n//? FORGOT PASSWORD\nexport const forgotPassword = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email } = req.body;\n    if (!email) {\n      return next(ApiError.badRequest(\"Email is required!\"));\n    }\n\n    await AuthService.forgotPassword(email);\n\n    return ApiResponse.ok(\n      res,\n      \"If an account exists, a reset code has been sent to your email.\"\n    );\n  }\n);\n\n//? VERIFY RESET PASSWORD TOKEN\nexport const verifyResetPasswordOtp = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { otpCode, email } = req.body;\n    if (!otpCode || !email) {\n      return next(ApiError.badRequest(\"OtpCode and email are required!\"));\n    }\n\n    await AuthService.verifyResetPasswordOtp(otpCode, email);\n\n    return ApiResponse.ok(\n      res,\n      \"Password reset otp verified successfully. You can now reset your password.\"\n    );\n  }\n);\n\n//? RESET PASSWORD\nexport const resetPassword = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { newPassword, email } = req.body;\n    if (!email || !newPassword) {\n      return next(ApiError.badRequest(\"Newpassword and email are required!\"));\n    }\n\n    const result = await AuthService.resetPassword(next, email, newPassword);\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to reset password!\"));\n    }\n\n    clearAuthCookies(res);\n\n    return ApiResponse.ok(\n      res,\n      result.message || \"Password reset successfully!\"\n    );\n  }\n);\n\n//? CHANGE PASSWORD\nexport const changePassword = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const { oldPassword, newPassword } = req.body;\n\n    if (!oldPassword || !newPassword) {\n      return next(\n        ApiError.badRequest(\"Old password and new password are required\")\n      );\n    }\n\n    const result = await AuthService.changePassword(next, {\n      userId: userId.toString(),\n      oldPassword,\n      newPassword\n    });\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to change password!\"));\n    }\n\n    clearAuthCookies(res);\n\n    return ApiResponse.ok(\n      res,\n      result.message || \"Password changed successfully!\"\n    );\n  }\n);\n\n//? REQUEST DELETE ACCOUNT\nexport const requestDeleteAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n    const { password } = req.body;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    if (!password) {\n      return next(ApiError.badRequest(\"Password is required!\"));\n    }\n\n    await AuthService.requestDeleteAccount(userId, password);\n\n    return ApiResponse.ok(\n      res,\n      \"Account deletion request sent successfully. Please check your email to confirm.\"\n    );\n  }\n);\n\n//? DELETE/DEACTIVATE ACCOUNT\nexport const deleteAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { userId, type }: DeleteAccountType = req.body;\n\n    if (!userId || !type) {\n      return next(ApiError.badRequest(\"User id and type are required!\"));\n    }\n\n    const reqUserId = req?.user?._id;\n\n    if (!reqUserId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n    const token = req.query.token as string;\n    if (!token) {\n      return next(\n        ApiError.badRequest(\n          `${type === \"hard\" ? \"Delete\" : \"Deactivate\"} account token is required!`\n        )\n      );\n    }\n\n    if (userId !== reqUserId) {\n      return next(\n        ApiError.unauthorized(\"You are not authorized to perform this action\")\n      );\n    }\n\n    await AuthService.deleteOrDeactiveAccount({ userId, type, token });\n\n    clearAuthCookies(res);\n\n    return ApiResponse.Success(\n      res,\n      `Account ${type === \"soft\" ? \"deactivated\" : \"deleted\"} successfully!`\n    );\n  }\n);\n\n//? REACTIVATE ACCOUNT\nexport const reactivateAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.reactivateAccount(userId);\n\n    return ApiResponse.Success(res, \"Account reactivated successfully!\");\n  }\n);\n\n//? GET USER SESSIONS\nexport const getUserSessions = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const result = await AuthService.getUserSessions(\n      userId.toString(),\n      currentSessionId\n    );\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to get user sessions!\"));\n    }\n\n    return ApiResponse.ok(res, \"User sessions fetched successfully\", result);\n  }\n);\n\n//? DELETE SESSION\nexport const deleteUserSession = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n    const { sessionId } = req.params;\n\n    if (!userId || !sessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.deleteUserSession(userId, sessionId as string);\n\n    const reqSId = req.cookies?.sid;\n\n    const isCurrentSession = sessionId === reqSId;\n    if (isCurrentSession) {\n      clearAuthCookies(res);\n    }\n\n    return ApiResponse.Success(res, \"User session deleted successfully!\");\n  }\n);\n\n//? DELETE ALL SESSIONS\nexport const deleteAllUserSessions = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.deleteAllUserSessions(userId);\n\n    clearAuthCookies(res);\n    // clearCookie(res, \"sid\");\n\n    return ApiResponse.Success(res, \"User sessions deleted successfully!\");\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/constants/status-codes.ts",
                          "content": "export const STATUS_CODES = {\n  // 2xx Success\n  OK: 200,\n  CREATED: 201,\n  ACCEPTED: 202,\n  NO_CONTENT: 204,\n\n  // 3xx Redirection\n  MOVED_PERMANENTLY: 301,\n  FOUND: 302,\n  NOT_MODIFIED: 304,\n\n  // 4xx Client Errors\n  BAD_REQUEST: 400,\n  UNAUTHORIZED: 401,\n  FORBIDDEN: 403,\n  NOT_FOUND: 404,\n  CONFLICT: 409,\n  UNPROCESSABLE_ENTITY: 422,\n  TOO_MANY_REQUESTS: 429,\n\n  // 5xx Server Errors\n  INTERNAL_SERVER_ERROR: 500,\n  NOT_IMPLEMENTED: 501,\n  BAD_GATEWAY: 502,\n  SERVICE_UNAVAILABLE: 503,\n  GATEWAY_TIMEOUT: 504\n} as const;\n\nexport type StatusCode = (typeof STATUS_CODES)[keyof typeof STATUS_CODES];\n"
                        },
                        {
                          "type": "file",
                          "path": "src/constants/auth.ts",
                          "content": "export const OTP_MAX_ATTEMPTS = 5;\n\nexport const OTP_TYPES = [\n  \"signin\",\n  \"email-verification\",\n  \"password-reset\",\n  \"password-change\"\n] as const;\n\nexport const NEXT_OTP_DELAY = 1 * 60 * 1000; // 1 minute\n\nexport const LOGIN_MAX_ATTEMPTS = 5 as const;\n\nexport const OTP_CODE_LENGTH = 6 as const;\n\nexport const OTP_COOL_DOWN = 60;\n\nexport const OTP_EXPIRES_IN = 5 * 60 * 1000; // 5 minutes\n\nexport const OTP_SPAM_LOCK_TIME = 3600; // 1 hour\n\nexport const LOCK_TIME_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport const ACCESS_TOKEN_EXPIRY = 15 * 60 * 1000; // 15 minutes\n\nexport const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days\n\nexport const SESSION_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days\n\nexport const RESET_PASSWORD_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n\nexport const REACTIVATION_AVAILABLE_AT = 24 * 60 * 60 * 1000; // 24 hours\n\nexport const DELETE_ACCOUNT_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/swagger.ts",
                          "content": "import swaggerUi from \"swagger-ui-express\";\nimport { Express } from \"express\";\nimport env from \"../configs/env\";\n\nimport swaggerDocument from \"../docs/swagger.json\";\n\nexport const setupSwagger = (app: Express) => {\n  if (env.NODE_ENV !== \"development\") return;\n\n  app.use(\"/api/docs\", swaggerUi.serve, swaggerUi.setup(swaggerDocument));\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/resend.ts",
                          "content": "import { Resend } from \"resend\";\nimport env from \"./env\";\n\nexport const resend = new Resend(env.RESEND_API_KEY);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/redis.ts",
                          "content": "import { createClient } from \"redis\";\nimport { env } from \"./env\";\n\nconst redisClient = createClient({\n  url: env.REDIS_URL\n});\n\nexport default redisClient;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/passport.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport passport from \"passport\";\nimport {\n  Strategy as GitHubStrategy,\n  Profile as GithubProfile\n} from \"passport-github2\";\n\nimport {\n  Strategy as GoogleStrategy,\n  Profile as GoogleProfile\n} from \"passport-google-oauth20\";\n\nimport {\n  Strategy as FacebookStrategy,\n  Profile as FacebookProfile\n} from \"passport-facebook\"; // npm i --save-dev @types/passport-facebook\n\nimport env from \"./env\";\n\n//? GITHUB STRATEGY\npassport.use(\n  new GitHubStrategy(\n    {\n      clientID: env.GITHUB_CLIENT_ID,\n      clientSecret: env.GITHUB_CLIENT_SECRET,\n      callbackURL: env.GITHUB_REDIRECT_URI\n    },\n    function (\n      accessToken: string,\n      refreshToken: string,\n      profile: GithubProfile,\n      cb: (error: Error | null, user?: any) => void\n    ) {\n      // console.log({ profile });\n      return cb(null, profile);\n    }\n  )\n);\n\n//? GOOGLE STRATEGY\npassport.use(\n  new GoogleStrategy(\n    {\n      clientID: env.GOOGLE_CLIENT_ID,\n      clientSecret: env.GOOGLE_CLIENT_SECRET,\n      callbackURL: env.GOOGLE_REDIRECT_URI\n    },\n    function (accessToken, refreshToken, profile: GoogleProfile, cb) {\n      return cb(null, profile);\n    }\n  )\n);\n\n//? FACEBOOK STRATEGY\npassport.use(\n  new FacebookStrategy(\n    {\n      clientID: env.FACEBOOK_APP_ID,\n      clientSecret: env.FACEBOOK_APP_SECRET,\n      callbackURL: env.FACEBOOK_REDIRECT_URI\n    },\n    function (accessToken, refreshToken, profile: FacebookProfile, cb) {\n      // console.log({ profile });\n      return cb(null, profile);\n    }\n  )\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/env.ts",
                          "content": "/* eslint-disable no-console */\nimport \"dotenv-flow/config\";\nimport { z } from \"zod\";\n\nexport const envSchema = z.object({\n  NODE_ENV: z\n    .enum([\"development\", \"test\", \"production\"])\n    .default(\"development\"),\n\n  PORT: z.string().regex(/^\\d+$/, \"PORT must be a number\").transform(Number),\n\n  DATABASE_URL: z.url(),\n\n  CORS_ORIGIN: z.string(),\n  CLIENT_URL: z.url(),\n\n  LOG_LEVEL: z\n    .enum([\"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\"])\n    .default(\"info\"),\n\n  JWT_ACCESS_SECRET: z.string().min(32),\n  JWT_REFRESH_SECRET: z.string().min(32),\n\n  CRYPTO_SECRET: z.string().min(32),\n\n  RESEND_API_KEY: z.string(),\n  EMAIL_FROM: z.email(),\n\n  CLOUDINARY_CLOUD_NAME: z.string(),\n  CLOUDINARY_API_KEY: z.string(),\n  CLOUDINARY_API_SECRET: z.string(),\n\n  GOOGLE_CLIENT_ID: z.string(),\n  GOOGLE_CLIENT_SECRET: z.string(),\n  GOOGLE_REDIRECT_URI: z.url(),\n\n  GITHUB_CLIENT_ID: z.string(),\n  GITHUB_CLIENT_SECRET: z.string(),\n  GITHUB_REDIRECT_URI: z.url(),\n\n  FACEBOOK_APP_ID: z.string(),\n  FACEBOOK_APP_SECRET: z.string(),\n  FACEBOOK_REDIRECT_URI: z.url(),\n\n  REDIS_URL: z.url()\n});\n\nexport type Env = z.infer<typeof envSchema>;\n\nconst result = envSchema.safeParse(process.env);\n\nif (!result.success) {\n  console.error(\"❌ Invalid environment configuration\");\n  console.error(z.treeifyError(result.error));\n  process.exit(1);\n}\n\nexport const env: Readonly<Env> = Object.freeze(result.data);\n\nexport default env;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/db.ts",
                          "content": "import mongoose from \"mongoose\";\nimport env from \"./env\";\nimport { logger } from \"../utils/logger\";\n\nexport const connectDB = async (): Promise<void> => {\n  try {\n    const conn = await mongoose.connect(env.DATABASE_URL as string);\n    logger.info(`MongoDB Connected: ${conn.connection.host}`);\n  } catch (error) {\n    logger.error(error, \"MongoDB Connection Failed\");\n    process.exit(1);\n  }\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/cloudinary.ts",
                          "content": "import { v2 as cloudinary } from \"cloudinary\";\nimport env from \"./env\";\n\ncloudinary.config({\n  cloud_name: env.CLOUDINARY_CLOUD_NAME,\n  api_key: env.CLOUDINARY_API_KEY,\n  api_secret: env.CLOUDINARY_API_SECRET\n});\n\nexport default cloudinary;\n"
                        }
                      ]
                    },
                    "feature": {
                      "files": [
                        {
                          "type": "file",
                          "path": "swagger.config.ts",
                          "content": "import swaggerAutoGen from \"swagger-autogen\";\n\nconst doc = {\n  info: {\n    title: \"Hybrid Auth API\",\n    description: \"Hybrid Auth API\",\n    version: \"1.0.0\"\n  },\n  host: \"localhost:9000/api\",\n  schemes: [\"http\"]\n};\n\nconst outputFile = \"./src/docs/swagger.json\";\nconst endpointsFiles = [\"./src/routes/*.ts\"];\n\nswaggerAutoGen(outputFile, endpointsFiles, doc);\n"
                        },
                        {
                          "type": "file",
                          "path": "README.md",
                          "content": "# Hybrid Auth MongoDB Feature\n\nMinimal Node.js + Express + TypeScript Feature starter using MongoDB and hybrid authentication (local and OAuth via Passport or similar).\n\n## Features\n\n- Express + TypeScript Feature structure\n- MongoDB integration\n- Hybrid auth: local credentials and OAuth providers (e.g., Google, GitHub, Facebook)\n- Session-based authentication compatible with production\n- Environment-driven configuration with `.env`\n- Dev and production scripts\n\n## What This Provides\n\n- A clean starting point for credential and OAuth login\n- Prewired Express app with routing and session middleware\n- MongoDB connection wiring ready for your data models\n- TypeScript configuration and scripts for iterative dev and production builds\n- Example environment keys you can enable as needed\n\n## Quick Start\n\n1. Install dependencies:\n   - `npm install`\n2. Configure environment:\n   - Create `.env` (copy from `.env.example` if present).\n   - Set variables shown below.\n3. Run in development:\n   - `npm run dev`\n4. Build and run in production:\n   - `npm run build`\n   - `npm start`\n\n## Requirements\n\n- Node.js 18+\n- MongoDB (local or Atlas)\n\n## Environment Variables\n\n- `MONGO_URI` — MongoDB connection string\n- `SESSION_SECRET` — random strong string\n- `PORT` — server port (e.g., 3000)\n- `NODE_ENV` — `development` or `production`\n- Optional OAuth (enable what you use):\n  - `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_CALLBACK_URL`\n  - `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GITHUB_CALLBACK_URL`\n  - `FACEBOOK_APP_ID`, `FACEBOOK_APP_SECRET`, `FACEBOOK_CALLBACK_URL`\n\n## Scripts\n\n- `npm run dev` — start development server\n- `npm run build` — compile TypeScript\n- `npm start` — start compiled app\n\n## Notes\n\n- Never commit `.env` or secrets.\n- Ensure indexes and users collections exist if your auth flow relies on them.\n"
                        },
                        {
                          "type": "file",
                          "path": "package.json",
                          "content": "{\n  \"name\": \"servercn-hybrid-auth\",\n  \"version\": \"1.0.0\",\n  \"main\": \"dist/server.js\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development npx tsx watch src/server.ts\",\n    \"build\": \"rm -rf dist && tsc && tsc-alias\",\n    \"start\": \"cross-env NODE_ENV=production node dist/server.js\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"docs\": \"npx tsx swagger.config.ts\",\n    \"prepare\": \"husky\",\n    \"lint:check\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"format:check\": \"npx prettier . --check\",\n    \"format:fix\": \"npx prettier . --write\"\n  },\n  \"lint-staged\": {\n    \"src/**/*.ts\": [\n      \"eslint --fix\",\n      \"prettier --write\",\n      \"tsc --noEmit\"\n    ]\n  },\n  \"dependencies\": {},\n  \"devDependencies\": {}\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/server.ts",
                          "content": "import app from \"./app\";\nimport { connectDB } from \"./shared/configs/db\";\nimport env from \"./shared/configs/env\";\nimport redisClient from \"./shared/configs/redis\";\nimport { logger } from \"./shared/utils/logger\";\nimport { configureGracefulShutdown } from \"./shared/utils/shutdown\";\n\nconst port = env.PORT || 9000;\n\nconnectDB().then(() => {\n  redisClient\n    .connect()\n    .then(() => {\n      logger.info(\"Redis Connection Success\");\n      const server = app.listen(port, () => {\n        logger.info(`[server]: Server is running at http://localhost:${port}`);\n        logger.info(`[server]: Environment: ${env.NODE_ENV}`);\n        logger.info(\n          `[server]: Swagger docs are available at http://localhost:${port}/api/docs`\n        );\n      });\n\n      configureGracefulShutdown(server);\n    })\n    .catch(error => {\n      logger.error(error, \"Redis Connection Failed\");\n      process.exit(1);\n    });\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/app.ts",
                          "content": "import express, { Express, Request, Response } from \"express\";\nimport cookieParser from \"cookie-parser\";\nimport morgan from \"morgan\";\nimport { notFoundHandler } from \"./shared/middlewares/not-found-handler\";\nimport { errorHandler } from \"./shared/middlewares/error-handler\";\nimport env from \"./shared/configs/env\";\nimport { configureSecurityHeaders } from \"./shared/middlewares/security-header\";\n\nimport Routes from \"./routes/index\";\n\nimport \"./shared/configs/passport\";\n\nimport sourceMapSupport from \"source-map-support\";\nimport { setupSwagger } from \"./shared/configs/swagger\";\nsourceMapSupport.install();\n\nconst app: Express = express();\n\n//? Apply security headers before other middlewares and routes\nconfigureSecurityHeaders(app);\n\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\napp.use(cookieParser());\napp.use(morgan(env.NODE_ENV === \"development\" ? \"dev\" : \"combined\"));\n\n//? Swagger Setup\nsetupSwagger(app);\n\n//? Routes\napp.get(\"/\", (req: Request, res: Response) => {\n  res.redirect(\"/api/v1/health\");\n});\n\napp.use(\"/api\", Routes);\n\n//? Not-found-handler (should be after routes)\napp.use(notFoundHandler);\n\n//? Global error handler (should be last)\napp.use(errorHandler);\n\nexport default app;\n"
                        },
                        {
                          "type": "file",
                          "path": ".husky/pre-commit",
                          "content": "npx lint-staged\n"
                        },
                        {
                          "type": "file",
                          "path": "src/types/global.d.ts",
                          "content": "import { Request } from \"express\";\n\nexport interface UserRequest extends Request {\n  user?: {\n    _id?: string | undefined;\n    role?: \"user\" | \"admin\" | undefined;\n    sessionId?: string | undefined;\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/forgot-password.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Verify your OTP</title>\r\n</head>\r\n\r\n<body\r\n  style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f7; margin: 0; padding: 0; color: #51545e;\">\r\n  <div style=\"background-color: #f4f4f7; padding: 40px 0;\">\r\n    <div\r\n      style=\"margin: 0 auto; background-color: #ffffff; padding: 40px 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px;\">\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <h1 style=\"color: #333333; font-size: 24px; font-weight: 700; margin: 0;\">\r\n          Forgot Password - Verify OTP\r\n        </h1>\r\n      </div>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 24px; color: #51545e;\">Hello <strong>\r\n          <%= name %>\r\n        </strong>,</p>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 30px; color: #51545e;\">\r\n        Thank you for using our service. Please use the following One-Time Password (OTP) to verify your password reset\r\n        request. This\r\n        code is valid for 5 minutes.\r\n      </p>\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <div\r\n          style=\"display: inline-block; padding: 16px 40px; background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;\">\r\n          <span\r\n            style=\"font-family: 'Courier New', Courier, monospace; font-size: 32px; font-weight: 700; letter-spacing: 6px; color: #1e293b;\">\r\n            <%= code %>\r\n          </span>\r\n        </div>\r\n      </div>\r\n\r\n      <p style=\"font-size: 14px; line-height: 1.6; color: #64748b; margin-top: 24px;\">\r\n        If you didn't request this request, you can safely ignore this email.\r\n      </p>\r\n\r\n      <div style=\"margin-top: 40px; padding-top: 24px; border-top: 1px solid #e2e8f0; text-align: center;\">\r\n        <p style=\"font-size: 12px; color: #94a3b8; margin: 0;\">\r\n          &copy; <%= new Date().getFullYear() %>. All rights reserved.\r\n        </p>\r\n      </div>\r\n\r\n    </div>\r\n  </div>\r\n</body>\r\n\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/email-verification.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Verify your email</title>\r\n</head>\r\n\r\n<body\r\n  style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f7; margin: 0; padding: 0; color: #51545e;\">\r\n  <div style=\"background-color: #f4f4f7; padding: 40px 0;\">\r\n    <div\r\n      style=\"margin: 0 auto; background-color: #ffffff; padding: 40px 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px;\">\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <h1 style=\"color: #333333; font-size: 24px; font-weight: 700; margin: 0;\">Verify your email</h1>\r\n      </div>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 24px; color: #51545e;\">Hello <strong>\r\n          <%= name %>\r\n        </strong>,</p>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 30px; color: #51545e;\">\r\n        Thank you for registering. Please use the following One-Time Password (OTP) to verify your email address. This\r\n        code is valid for 5 minutes.\r\n      </p>\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <div\r\n          style=\"display: inline-block; padding: 16px 40px; background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;\">\r\n          <span\r\n            style=\"font-family: 'Courier New', Courier, monospace; font-size: 32px; font-weight: 700; letter-spacing: 6px; color: #1e293b;\">\r\n            <%= code %>\r\n          </span>\r\n        </div>\r\n      </div>\r\n\r\n      <p style=\"font-size: 14px; line-height: 1.6; color: #64748b; margin-top: 24px;\">\r\n        If you didn't request this verification, you can safely ignore this email.\r\n      </p>\r\n\r\n      <div style=\"margin-top: 40px; padding-top: 24px; border-top: 1px solid #e2e8f0; text-align: center;\">\r\n        <p style=\"font-size: 12px; color: #94a3b8; margin: 0;\">\r\n          &copy; <%= new Date().getFullYear() %>. All rights reserved.\r\n        </p>\r\n      </div>\r\n\r\n    </div>\r\n  </div>\r\n</body>\r\n\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/delete-account.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Delete Account Request</title>\r\n  <style>\r\n    body {\r\n      font-family: Arial, sans-serif;\r\n      line-height: 1.6;\r\n      margin: 0;\r\n      padding: 20px;\r\n      color: #333;\r\n    }\r\n    p {\r\n      margin-bottom: 10px;\r\n    }\r\n    a {\r\n      color: #007bff;\r\n      text-decoration: none;\r\n    }\r\n    a:hover {\r\n      text-decoration: underline;\r\n    }\r\n  </style>\r\n</head>\r\n<body>\r\n  <p>Hello <%= name %>,</p>\r\n  <p>We received a request to delete your account. If you confirm this action, your account will be permanently deleted.</p>\r\n  <p>To confirm, please click the link below:</p>\r\n  <a href=\"<%= deleteAccountUrl %>\">Confirm Delete Account</a>\r\n  <p>If you did not request this action, please ignore this email or reply to let us know. Your account is still secure.</p>\r\n  <p>Thank you,</p>\r\n</body>\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/index.ts",
                          "content": "import { Router } from \"express\";\nimport healthRoutes from \"../modules/health/health.routes\";\nimport authRoutes from \"../modules/auth/auth.routes\";\nimport oauthRoutes from \"../modules/oauth/oauth.routes\";\n\nconst router = Router();\n\nrouter.use(\"/v1/health\", healthRoutes);\nrouter.use(\"/v1/auth\", authRoutes);\nrouter.use(\"/auth\", oauthRoutes); //* Here versioning is not given because, in google and github callback routes, we are not using versioning. process.env.GOOGLE_REDIRECT_URI\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/docs/swagger.json",
                          "content": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"Hybrid Auth API\",\n    \"description\": \"Hybrid Auth API\",\n    \"version\": \"1.0.0\"\n  },\n  \"host\": \"localhost:9000/api\",\n  \"basePath\": \"/\",\n  \"schemes\": [\"http\"],\n  \"paths\": {\n    \"/verify-otp\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"example\": \"any\"\n                },\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                },\n                \"role\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/signin\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/profile\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      },\n      \"patch\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/refresh-token\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/logout\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/reset-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/change-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/delete-account\": {\n      \"delete\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/reactivate-account\": {\n      \"put\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/detailed\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/health/\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/health/detailed\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/verify-otp\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"example\": \"any\"\n                },\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                },\n                \"role\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/signin\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/profile\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      },\n      \"patch\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/refresh-token\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/logout\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/reset-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/change-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/delete-account\": {\n      \"delete\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/reactivate-account\": {\n      \"put\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/github\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/github/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/google\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/google/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/github\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/github/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/google\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/google/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    }\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/verify-auth.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Response } from \"express\";\nimport { verifyAccessToken } from \"../utils/jwt\";\nimport { ApiError } from \"../utils/api-error\";\nimport redisClient from \"../configs/redis\";\nimport { UserRequest } from \"../../types/global\";\nimport { SessionData } from \"../../modules/auth/auth.types\";\n\nexport async function verifyAuthentication(\n  req: UserRequest,\n  _res: Response,\n  next: NextFunction\n): Promise<void> {\n  const authHeader = req.headers.authorization || \"\";\n  const token = authHeader.startsWith(\"Bearer \")\n    ? authHeader.split(\" \")[1]\n    : null;\n\n  const accessToken = req.cookies?.accessToken || token;\n  if (!accessToken) {\n    return next(ApiError.unauthorized(\"Missing access token\"));\n  }\n\n  try {\n    const decoded = verifyAccessToken(accessToken);\n\n    const sessionKey = `session:${decoded.sessionId}`;\n    const sessionData = await redisClient.get(sessionKey);\n    if (!sessionData) {\n      return next(ApiError.unauthorized(\"Session not found\"));\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.ip !== req.ip) {\n      return next(ApiError.unauthorized(\"Suspicious session\"));\n    }\n\n    if (session.userAgent !== req.headers[\"user-agent\"]) {\n      return next(ApiError.unauthorized(\"Suspicious session\"));\n    }\n\n    if (session.expiresAt < new Date()) {\n      return next(ApiError.unauthorized(\"Session expired\"));\n    }\n\n    req.user = decoded;\n    return next();\n  } catch (err: any) {\n    if (err.name === \"TokenExpiredError\") {\n      return next(ApiError.unauthorized(\"Access token expired\"));\n    }\n    return next(ApiError.unauthorized(\"Invalid access token\"));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/validate-request.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { Request, Response, NextFunction } from \"express\";\nimport z, { ZodError, type ZodObject } from \"zod\";\n\nimport { ApiError } from \"../utils/api-error\";\n\nexport const validateRequest = (schema: ZodObject<any>) => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    try {\n      schema.parse(req.body);\n\n      next();\n    } catch (error) {\n      if (!(error instanceof ZodError)) {\n        return next(error);\n      }\n\n      return next(\n        ApiError.badRequest(\n          \"Invalid request data\",\n          z.flattenError(error).fieldErrors || z.flattenError(error)\n        )\n      );\n    }\n  };\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/validate-id.ts",
                          "content": "import { isValidObjectId } from \"mongoose\";\nimport { ApiError } from \"../utils/api-error\";\nimport { NextFunction, Request, Response } from \"express\";\n\nexport const validateObjectId = (paramName: string = \"id\") => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const value =\n      req?.params[paramName] || req?.body[paramName] || req?.query[paramName];\n    if (!value) {\n      throw ApiError.badRequest(`${paramName} is required`);\n    }\n\n    if (!isValidObjectId(value)) {\n      throw ApiError.badRequest(`Invalid ${paramName}`);\n    }\n\n    next();\n  };\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/user-account-restriction.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Response } from \"express\";\nimport { ApiError } from \"../utils/api-error\";\nimport { logger } from \"../utils/logger\";\nimport { getRemainingTime } from \"../utils/date\";\nimport { UserRequest } from \"../../types/global\";\nimport User from \"../../modules/auth/user.model\";\n\nexport async function checkUserAccountRestriction(\n  req: UserRequest,\n  _res: Response,\n  next: NextFunction\n): Promise<void> {\n  try {\n    if (!req.user?._id) {\n      return next(ApiError.unauthorized(\"Unauthorized\"));\n    }\n\n    const user = await User.findById(req.user._id);\n\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n    }\n\n    if (user.isDeleted || user.deletedAt) {\n      return next(ApiError.forbidden(\"Your account has been deactivated.\"));\n    }\n\n    if (user.lockUntil && user.lockUntil.getTime() > Date.now()) {\n      const remainingTime = getRemainingTime(user.lockUntil);\n\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${remainingTime.minutes} minutes and ${remainingTime.seconds} seconds.`\n        )\n      );\n    }\n\n    if (!user.isEmailVerified) {\n      return next(\n        ApiError.forbidden(\"Email not verified. Please verify your email.\")\n      );\n    }\n\n    return next();\n  } catch (err: any) {\n    logger.error(err?.message || err);\n    return next(ApiError.server(\"Something went wrong\"));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/upload-file.ts",
                          "content": "import multer from \"multer\";\n\nexport const ALLOWED_FILE_TYPES = [\n  \"image/jpeg\",\n  \"image/png\",\n  \"image/webp\",\n  \"video/mp4\",\n  \"video/mpeg\",\n  \"video/quicktime\",\n  \"application/pdf\"\n];\n\nexport const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB\n\nconst storage = multer.memoryStorage();\n\nconst fileFilter: multer.Options[\"fileFilter\"] = (_req, file, cb) => {\n  if (!ALLOWED_FILE_TYPES.includes(file.mimetype)) {\n    return cb(null, false);\n  }\n  cb(null, true);\n};\n\nconst upload = multer({\n  storage,\n  limits: { fileSize: MAX_FILE_SIZE },\n  fileFilter\n});\n\nexport default upload;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/security-header.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport cors from \"cors\";\nimport { Express } from \"express\";\nimport helmet from \"helmet\";\nimport env from \"../configs/env\";\n\nexport const configureSecurityHeaders = (app: Express) => {\n  // Use Helmet to set various security-related HTTP headers\n  app.use(helmet());\n\n  // Configure CORS\n  app.use(\n    cors({\n      origin: env.CORS_ORIGIN || \"*\",\n      credentials: true,\n      methods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\"],\n      allowedHeaders: [\"Content-Type\", \"Authorization\", \"X-Requested-With\"]\n    })\n  );\n\n  // Additional custom security headers\n  app.use((req: Request, res: Response, next: NextFunction) => {\n    res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n    res.setHeader(\"X-Frame-Options\", \"DENY\");\n    res.setHeader(\"X-XSS-Protection\", \"1; mode=block\");\n    next();\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/rate-limiter.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Request, Response } from \"express\";\nimport { rateLimit } from \"express-rate-limit\";\nimport { STATUS_CODES } from \"../constants/status-codes\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const rateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000, // 15 minutes\n  max: 100, // Limit each IP to 100 requests per window\n  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers\n  legacyHeaders: false, // Disable the `X-RateLimit-*` headers\n  message: {\n    success: false,\n    message:\n      \"Too many requests from this IP, please try again after 15 minutes\",\n    status: 429\n  },\n  handler: (req: Request, res: Response, next: NextFunction, options: any) => {\n    next(new ApiError(STATUS_CODES.TOO_MANY_REQUESTS, options.message.message));\n  }\n});\n\n/**\n * Stricter rate limiter for sensitive routes (e.g., auth, login)\n */\nexport const authRateLimiter = rateLimit({\n  windowMs: 60 * 60 * 1000, // 1 hour\n  max: 5, // Limit each IP to 5 failed attempts per hour\n  handler: (req, res, next, options) => {\n    next(\n      ApiError.tooManyRequests(\n        \"Too many login attempts, please try again after an hour\"\n      )\n    );\n  }\n});\n\n/**\n * Rate limiter for login route\n */\nexport const signinRateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many login attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\n/**\n * Rate limiter for registration route\n */\nexport const signupRateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many registration attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const otpRequestLimiter = rateLimit({\n  windowMs: 10 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many OTP requests. Please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const otpVerificationLimiter = rateLimit({\n  windowMs: 10 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many OTP verification attempts. Please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const resetPasswordLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many password reset attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const deleteAccountLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many account deletion attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const changePasswordLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many password change attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/not-found-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const notFoundHandler = (\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => {\n  throw ApiError.notFound(`Route ${req.method} ${req.originalUrl} not found`);\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/error-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport env from \"../configs/env\";\n\nimport { logger } from \"../utils/logger\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const errorHandler = (\n  err: Error,\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => {\n  if (res.headersSent) {\n    return next(err);\n  }\n  let statusCode = 500;\n  let message = \"Internal server error\";\n  let errors: unknown;\n\n  if (err instanceof ApiError) {\n    statusCode = err.statusCode;\n    message = err.message;\n    errors = err.errors;\n  }\n\n  logger.error(\n    err,\n    `Error: ${message} | Status: ${statusCode} | Path: ${req.method} ${req.originalUrl}`\n  );\n\n  const response = {\n    success: false,\n    message,\n    statusCode,\n    ...(errors !== undefined && { errors }),\n    ...(env.NODE_ENV === \"development\" && { stack: err.stack })\n  };\n\n  res.status(statusCode).json(response);\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/shutdown.ts",
                          "content": "import { Server } from \"http\";\nimport { logger } from \"./logger\";\n\nexport const configureGracefulShutdown = (server: Server) => {\n  const signals = [\"SIGTERM\", \"SIGINT\"];\n\n  signals.forEach(signal => {\n    process.on(signal, () => {\n      logger.info(`\\n${signal} signal received. Shutting down gracefully...`);\n\n      server.close(err => {\n        if (err) {\n          logger.error(err, \"Error during server close\");\n          process.exit(1);\n        }\n\n        logger.info(\"HTTP server closed.\");\n        process.exit(0);\n      });\n\n      // Force shutdown after 10 seconds\n      setTimeout(() => {\n        logger.error(\n          \"Could not close connections in time, forcefully shutting down\"\n        );\n        process.exit(1);\n      }, 10000);\n    });\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/send-mail.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport env from \"../configs/env\";\nimport { resend } from \"../configs/resend\";\nimport { renderEmailTemplates } from \"./render-email-template\";\n\nexport type SendMailType = {\n  from?: string;\n  subject: string;\n  data: Record<string, any>;\n  email: string;\n  html?: string;\n  templateName: string;\n};\n\nexport async function sendEmail({\n  from,\n  email,\n  subject,\n  data,\n  html,\n  templateName\n}: SendMailType) {\n  const htmlContent =\n    (await renderEmailTemplates(templateName, data)) || html || \"\";\n\n  return await resend.emails.send({\n    from: from || env.EMAIL_FROM,\n    to: email,\n    subject,\n    replyTo: email,\n    html: htmlContent\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/render-email-template.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport ejs from \"ejs\"; // npm i --save-dev @types/ejs\nimport path from \"node:path\";\n\nexport async function renderEmailTemplates(\n  templateName: string,\n  data: Record<string, any>\n) {\n  const templatePath = path.join(\n    process.cwd(),\n    \"src\",\n    \"email-templates\",\n    `${templateName}.ejs`\n  );\n  return ejs.renderFile(templatePath, data);\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/logger.ts",
                          "content": "import pino from \"pino\";\nimport env from \"../configs/env\";\n\nexport const logger = pino({\n  level: env.LOG_LEVEL,\n  transport:\n    env.NODE_ENV !== \"production\"\n      ? {\n          target: \"pino-pretty\",\n          options: {\n            colorize: true,\n            translateTime: \"yyyy-mm-dd HH:MM:ss\",\n            ignore: \"pid,hostname\"\n          }\n        }\n      : undefined\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/jwt.ts",
                          "content": "import jwt from \"jsonwebtoken\";\nimport env from \"../configs/env\";\n\nconst JWT_ACCESS_TOKEN_EXPIRY = \"15m\";\nconst JWT_REFRESH_TOKEN_EXPIRY = \"7d\";\n\nexport function generateAccessToken(user: {\n  _id: string;\n  role: \"user\" | \"admin\";\n  sessionId: string;\n}) {\n  return jwt.sign(\n    { _id: user._id, role: user.role, sessionId: user.sessionId },\n    env.JWT_ACCESS_SECRET!,\n    {\n      expiresIn: JWT_ACCESS_TOKEN_EXPIRY\n    }\n  );\n}\n\nexport function generateRefreshToken(user: { _id: string; sessionId: string }) {\n  return jwt.sign(\n    { _id: user._id, sessionId: user.sessionId },\n    env.JWT_REFRESH_SECRET!,\n    {\n      expiresIn: JWT_REFRESH_TOKEN_EXPIRY\n    }\n  );\n}\n\nexport function verifyAccessToken(token: string) {\n  return jwt.verify(token, env.JWT_ACCESS_SECRET!) as {\n    _id: string;\n    role: \"user\" | \"admin\";\n    sessionId: string;\n  };\n}\n\nexport function verifyRefreshToken(token: string) {\n  return jwt.verify(token, env.JWT_REFRESH_SECRET!) as {\n    _id: string;\n    sessionId: string;\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/date.ts",
                          "content": "export function getRemainingTime(date: Date) {\n  const now = new Date();\n  let diff = date.getTime() - now.getTime();\n\n  if (diff <= 0) {\n    return {\n      days: 0,\n      minutes: 0,\n      seconds: 0\n    };\n  }\n\n  const seconds = Math.floor((diff / 1000) % 60);\n  const minutes = Math.floor((diff / (1000 * 60)) % 60);\n  const days = Math.floor(diff / (1000 * 60 * 60 * 24));\n\n  return {\n    days,\n    minutes,\n    seconds\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/async-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\n\nexport type AsyncRouteHandler = (\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => Promise<unknown>;\n\nexport function AsyncHandler(fn: AsyncRouteHandler) {\n  return function (req: Request, res: Response, next: NextFunction) {\n    Promise.resolve()\n      .then(() => fn(req, res, next))\n      .catch(next);\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/api-response.ts",
                          "content": "import { STATUS_CODES, StatusCode } from \"../constants/status-codes\";\nimport type { Response } from \"express\";\n\ntype ApiResponseParams<T> = {\n  success: boolean;\n  message: string;\n  statusCode: StatusCode;\n  data?: T | null;\n  errors?: unknown;\n};\n\nexport class ApiResponse<T = unknown> {\n  public readonly success: boolean;\n  public readonly message: string;\n  public readonly statusCode: StatusCode;\n  public readonly data?: T | null;\n  public readonly errors?: unknown;\n\n  constructor({\n    success,\n    message,\n    statusCode,\n    data,\n    errors\n  }: ApiResponseParams<T>) {\n    this.success = success;\n    this.message = message;\n    this.statusCode = statusCode;\n    this.data = data;\n    this.errors = errors;\n  }\n\n  send(res: Response): Response {\n    return res.status(this.statusCode).json({\n      success: this.success,\n      message: this.message,\n      statusCode: this.statusCode,\n      ...(this.data !== undefined && { data: this.data }),\n      ...(this.errors !== undefined && { errors: this.errors })\n    });\n  }\n\n  static Success<T>(\n    res: Response,\n    message: string,\n    data?: T,\n    statusCode: StatusCode = STATUS_CODES.OK\n  ): Response {\n    return new ApiResponse<T>({\n      success: true,\n      message,\n      data,\n      statusCode\n    }).send(res);\n  }\n\n  static ok<T>(res: Response, message = \"OK\", data?: T) {\n    return ApiResponse.Success(res, message, data, STATUS_CODES.OK);\n  }\n\n  static created<T>(res: Response, message = \"Created\", data?: T) {\n    return ApiResponse.Success(res, message, data, STATUS_CODES.CREATED);\n  }\n}\n\n/*\n * Usage:\n * ApiResponse.ok(res, \"OK\", data);\n * ApiResponse.created(res, \"Created\", data);\n */\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/api-error.ts",
                          "content": "import { STATUS_CODES, StatusCode } from \"../constants/status-codes\";\n\nexport class ApiError extends Error {\n  public readonly statusCode: StatusCode;\n  public readonly isOperational: boolean;\n  public readonly errors?: unknown;\n\n  constructor(\n    statusCode: StatusCode,\n    message: string,\n    errors?: unknown,\n    isOperational = true\n  ) {\n    super(message);\n    this.name = \"ApiError\";\n    this.statusCode = statusCode;\n    this.errors = errors;\n    this.isOperational = isOperational;\n\n    Error.captureStackTrace(this, this.constructor);\n  }\n\n  static badRequest(message = \"Bad Request\", errors?: unknown) {\n    return new ApiError(STATUS_CODES.BAD_REQUEST, message, errors);\n  }\n\n  static unauthorized(message = \"Unauthorized\") {\n    return new ApiError(STATUS_CODES.UNAUTHORIZED, message);\n  }\n\n  static forbidden(message = \"Forbidden\") {\n    return new ApiError(STATUS_CODES.FORBIDDEN, message);\n  }\n\n  static notFound(message = \"Not Found\") {\n    return new ApiError(STATUS_CODES.NOT_FOUND, message);\n  }\n\n  static conflict(message = \"Conflict\") {\n    return new ApiError(STATUS_CODES.CONFLICT, message);\n  }\n\n  static server(message = \"Internal Server Error\") {\n    return new ApiError(STATUS_CODES.INTERNAL_SERVER_ERROR, message);\n  }\n\n  static unprocessableEntity(message = \"Unprocessable Entity\") {\n    return new ApiError(STATUS_CODES.UNPROCESSABLE_ENTITY, message);\n  }\n\n  static tooManyRequests(message = \"Too Many Requests\") {\n    return new ApiError(STATUS_CODES.TOO_MANY_REQUESTS, message);\n  }\n}\n\n/*\n  ? Usage:\n  * throw new ApiError(404, \"Not found\");\n  * throw ApiError.badRequest(\"Bad request\");\n */\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/helpers/token.helpers.ts",
                          "content": "import crypto from \"node:crypto\";\n\nexport function generateOTP(length: number = 6, ttlMinutes: number = 5) {\n  const code = crypto\n    .randomInt(0, Math.pow(10, length))\n    .toString()\n    .padStart(length, \"0\");\n\n  const hashCode = crypto\n    .createHash(\"sha256\")\n    .update(String(code))\n    .digest(\"hex\");\n\n  const expiresAt = new Date(Date.now() + ttlMinutes * 60 * 1000).toISOString();\n\n  return { code, hashCode, expiresAt };\n}\n\nexport function generateHashedToken(token: string): string {\n  return crypto.createHash(\"sha256\").update(String(token)).digest(\"hex\");\n}\n\nexport function generateSecureToken(length: number = 32): string {\n  return crypto.randomBytes(length).toString(\"hex\");\n}\n\nexport function verifyHashedToken(token: string, hashedToken: string): boolean {\n  return (\n    crypto.createHash(\"sha256\").update(String(token)).digest(\"hex\") ===\n    hashedToken\n  );\n}\n\nexport function generateTokenAndHashedToken(id: string) {\n  const cryptoSecret = process.env.CRYPTO_SECRET! || \"secret\";\n  const token = crypto\n    .createHmac(\"sha256\", cryptoSecret)\n    .update(String(id))\n    .digest(\"hex\");\n\n  const hashedToken = crypto\n    .createHash(\"sha256\")\n    .update(String(token))\n    .digest(\"hex\");\n  return { token, hashedToken };\n}\n\nexport function generateUUID(): string {\n  return crypto.randomUUID();\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/helpers/cookie.helper.ts",
                          "content": "import { Response } from \"express\";\nimport {\n  ACCESS_TOKEN_EXPIRY,\n  REFRESH_TOKEN_EXPIRY,\n  SESSION_EXPIRY\n} from \"../../modules/auth/auth.constants\";\nimport env from \"../configs/env\";\n\nconst isProduction = env.NODE_ENV === \"production\";\n\nexport const COOKIE_OPTIONS = {\n  httpOnly: true,\n  secure: isProduction,\n  sameSite: isProduction ? (\"none\" as const) : (\"lax\" as const),\n  path: \"/\"\n};\n\nexport function setAuthCookies(\n  res: Response,\n  accessToken: string,\n  refreshToken: string,\n  sessionId: string\n) {\n  setCookies(res, [\n    {\n      cookie: \"accessToken\",\n      value: accessToken,\n      maxAge: ACCESS_TOKEN_EXPIRY\n    },\n    {\n      cookie: \"refreshToken\",\n      value: refreshToken,\n      maxAge: REFRESH_TOKEN_EXPIRY,\n      path: \"/api/v1/auth/refresh-token\"\n    },\n    {\n      cookie: \"sid\",\n      value: sessionId,\n      maxAge: SESSION_EXPIRY\n    }\n  ]);\n}\n\nexport function clearAuthCookies(res: Response) {\n  clearCookie(res, \"accessToken\");\n  clearCookie(res, \"refreshToken\");\n  clearCookie(res, \"sid\");\n}\n\nexport function clearCookie(res: Response, cookie: string = \"sid\") {\n  res.clearCookie(cookie, COOKIE_OPTIONS);\n}\n\ntype Cookie = {\n  cookie: string;\n  value: string;\n  maxAge: number;\n  path?: string;\n};\n\nexport function setCookies(res: Response, cookies: Cookie[]) {\n  cookies.forEach(({ cookie, value, maxAge, path = \"/\" }) => {\n    res.cookie(cookie, value, {\n      ...COOKIE_OPTIONS,\n      path,\n      maxAge\n    });\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/constants/status-codes.ts",
                          "content": "export const STATUS_CODES = {\n  // 2xx Success\n  OK: 200,\n  CREATED: 201,\n  ACCEPTED: 202,\n  NO_CONTENT: 204,\n\n  // 3xx Redirection\n  MOVED_PERMANENTLY: 301,\n  FOUND: 302,\n  NOT_MODIFIED: 304,\n\n  // 4xx Client Errors\n  BAD_REQUEST: 400,\n  UNAUTHORIZED: 401,\n  FORBIDDEN: 403,\n  NOT_FOUND: 404,\n  CONFLICT: 409,\n  UNPROCESSABLE_ENTITY: 422,\n  TOO_MANY_REQUESTS: 429,\n\n  // 5xx Server Errors\n  INTERNAL_SERVER_ERROR: 500,\n  NOT_IMPLEMENTED: 501,\n  BAD_GATEWAY: 502,\n  SERVICE_UNAVAILABLE: 503,\n  GATEWAY_TIMEOUT: 504\n} as const;\n\nexport type StatusCode = (typeof STATUS_CODES)[keyof typeof STATUS_CODES];\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/swagger.ts",
                          "content": "import swaggerUi from \"swagger-ui-express\";\nimport { Express } from \"express\";\nimport env from \"../configs/env\";\n\nimport swaggerDocument from \"../../docs/swagger.json\";\n\nexport const setupSwagger = (app: Express) => {\n  if (env.NODE_ENV !== \"development\") return;\n\n  app.use(\"/api/docs\", swaggerUi.serve, swaggerUi.setup(swaggerDocument));\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/resend.ts",
                          "content": "import { Resend } from \"resend\";\nimport env from \"./env\";\n\nexport const resend = new Resend(env.RESEND_API_KEY);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/redis.ts",
                          "content": "import { createClient } from \"redis\";\nimport { env } from \"./env\";\n\nconst redisClient = createClient({\n  url: env.REDIS_URL\n});\n\nexport default redisClient;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/passport.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport passport from \"passport\";\nimport {\n  Strategy as GitHubStrategy,\n  Profile as GithubProfile\n} from \"passport-github2\";\n\nimport {\n  Strategy as GoogleStrategy,\n  Profile as GoogleProfile\n} from \"passport-google-oauth20\";\n\nimport {\n  Strategy as FacebookStrategy,\n  Profile as FacebookProfile\n} from \"passport-facebook\"; // npm i --save-dev @types/passport-facebook\n\nimport env from \"./env\";\n\n//? GITHUB STRATEGY\npassport.use(\n  new GitHubStrategy(\n    {\n      clientID: env.GITHUB_CLIENT_ID,\n      clientSecret: env.GITHUB_CLIENT_SECRET,\n      callbackURL: env.GITHUB_REDIRECT_URI\n    },\n    function (\n      accessToken: string,\n      refreshToken: string,\n      profile: GithubProfile,\n      cb: (error: Error | null, user?: any) => void\n    ) {\n      // console.log({ profile });\n      return cb(null, profile);\n    }\n  )\n);\n\n//? GOOGLE STRATEGY\npassport.use(\n  new GoogleStrategy(\n    {\n      clientID: env.GOOGLE_CLIENT_ID,\n      clientSecret: env.GOOGLE_CLIENT_SECRET,\n      callbackURL: env.GOOGLE_REDIRECT_URI\n    },\n    function (accessToken, refreshToken, profile: GoogleProfile, cb) {\n      return cb(null, profile);\n    }\n  )\n);\n\n//? FACEBOOK STRATEGY\npassport.use(\n  new FacebookStrategy(\n    {\n      clientID: env.FACEBOOK_APP_ID,\n      clientSecret: env.FACEBOOK_APP_SECRET,\n      callbackURL: env.FACEBOOK_REDIRECT_URI\n    },\n    function (accessToken, refreshToken, profile: FacebookProfile, cb) {\n      // console.log({ profile });\n      return cb(null, profile);\n    }\n  )\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/env.ts",
                          "content": "/* eslint-disable no-console */\nimport \"dotenv-flow/config\";\nimport { z } from \"zod\";\n\nexport const envSchema = z.object({\n  NODE_ENV: z\n    .enum([\"development\", \"test\", \"production\"])\n    .default(\"development\"),\n\n  PORT: z.string().regex(/^\\d+$/, \"PORT must be a number\").transform(Number),\n\n  DATABASE_URL: z.url(),\n\n  CORS_ORIGIN: z.string(),\n  CLIENT_URL: z.url(),\n\n  LOG_LEVEL: z\n    .enum([\"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\"])\n    .default(\"info\"),\n\n  JWT_ACCESS_SECRET: z.string().min(32),\n  JWT_REFRESH_SECRET: z.string().min(32),\n\n  CRYPTO_SECRET: z.string().min(32),\n\n  RESEND_API_KEY: z.string(),\n  EMAIL_FROM: z.email(),\n\n  CLOUDINARY_CLOUD_NAME: z.string(),\n  CLOUDINARY_API_KEY: z.string(),\n  CLOUDINARY_API_SECRET: z.string(),\n\n  GOOGLE_CLIENT_ID: z.string(),\n  GOOGLE_CLIENT_SECRET: z.string(),\n  GOOGLE_REDIRECT_URI: z.url(),\n\n  GITHUB_CLIENT_ID: z.string(),\n  GITHUB_CLIENT_SECRET: z.string(),\n  GITHUB_REDIRECT_URI: z.url(),\n\n  FACEBOOK_APP_ID: z.string(),\n  FACEBOOK_APP_SECRET: z.string(),\n  FACEBOOK_REDIRECT_URI: z.url(),\n\n  REDIS_URL: z.url()\n});\n\nexport type Env = z.infer<typeof envSchema>;\n\nconst result = envSchema.safeParse(process.env);\n\nif (!result.success) {\n  console.error(\"❌ Invalid environment configuration\");\n  console.error(z.treeifyError(result.error));\n  process.exit(1);\n}\n\nexport const env: Readonly<Env> = Object.freeze(result.data);\n\nexport default env;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/db.ts",
                          "content": "import mongoose from \"mongoose\";\nimport env from \"./env\";\nimport { logger } from \"../utils/logger\";\n\nexport const connectDB = async (): Promise<void> => {\n  try {\n    const conn = await mongoose.connect(env.DATABASE_URL as string);\n    logger.info(`MongoDB Connected: ${conn.connection.host}`);\n  } catch (error) {\n    logger.error(error, \"MongoDB Connection Failed\");\n    process.exit(1);\n  }\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/cloudinary.ts",
                          "content": "import { v2 as cloudinary } from \"cloudinary\";\nimport env from \"./env\";\n\ncloudinary.config({\n  cloud_name: env.CLOUDINARY_CLOUD_NAME,\n  api_key: env.CLOUDINARY_API_KEY,\n  api_secret: env.CLOUDINARY_API_SECRET\n});\n\nexport default cloudinary;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/upload/upload.service.ts",
                          "content": "import { DeleteApiResponse } from \"cloudinary\";\nimport cloudinary from \"../../shared/configs/cloudinary\";\n\nexport interface UploadOptions {\n  folder: string;\n  resource_type?: \"image\" | \"video\" | \"raw\" | \"auto\";\n}\n\nexport interface CloudinaryUploadResult {\n  url: string;\n  public_id: string;\n  size: number;\n}\n\nexport const uploadToCloudinary = (\n  buffer: Buffer,\n  options: UploadOptions\n): Promise<CloudinaryUploadResult> => {\n  return new Promise((resolve, reject) => {\n    const stream = cloudinary.uploader.upload_stream(\n      {\n        folder: options.folder || \"uploads\",\n        resource_type: options.resource_type || \"auto\"\n      },\n      (error, result) => {\n        if (error || !result) {\n          return reject(error);\n        }\n        resolve({\n          url: result.secure_url,\n          public_id: result.public_id,\n          size: result.bytes\n        });\n      }\n    );\n\n    stream.end(buffer);\n  });\n};\n\nexport const deleteFileFromCloudinary = (\n  publicIds: string[]\n): Promise<DeleteApiResponse> => {\n  return new Promise((resolve, reject) => {\n    cloudinary.api.delete_resources(publicIds, (error, result) => {\n      if (error || !result) {\n        return reject(error);\n      }\n      resolve(result);\n    });\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/otp/otp.service.ts",
                          "content": "import { logger } from \"../../shared/utils/logger\";\nimport redis from \"../../shared/configs/redis\";\nimport {\n  OTP_CODE_LENGTH,\n  OTP_EXPIRES_IN,\n  OTP_MAX_ATTEMPTS,\n  OTP_SPAM_LOCK_TIME,\n  OTP_COOL_DOWN\n} from \"../auth/auth.constants\";\nimport { generateOTP } from \"../../shared/helpers/token.helpers\";\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { sendEmail } from \"../../shared/utils/send-mail\";\n\ntype SendOtpBase = {\n  name: string;\n  email: string;\n  templateName: string;\n  subject: string;\n};\n\ntype SendOtpWithCode = SendOtpBase & {\n  code: string;\n  hashCode: string;\n};\n\ntype SendOtpWithoutCode = SendOtpBase & {\n  code?: never;\n  hashCode?: never;\n};\n\nexport type SendOtpType = SendOtpWithCode | SendOtpWithoutCode;\n\nexport class OtpService {\n  static async checkOtpRestrictions(email: string) {\n    const otpLock = await redis.get(`otp_lock:${email}`);\n    if (otpLock) {\n      throw ApiError.badRequest(\n        \"Your Account is locked due to multiple failed attempts. Please try again after 30 minutes.\"\n      );\n    }\n\n    if (await redis.get(`otp_spam_lock:${email}`)) {\n      throw ApiError.tooManyRequests(\n        \"Too many otp requests. Please try again after 1 hour before requesting again.\"\n      );\n    }\n\n    if (await redis.get(`otp_cooldown:${email}`)) {\n      throw ApiError.tooManyRequests(\n        \"Too many otp requests. Please try again after 1 minute before requesting new otp.\"\n      );\n    }\n  }\n\n  static async trackOtpRequests(email: string) {\n    try {\n      const otpRequestKey = `otp_request_count:${email}`;\n      let otpRequestsCount = parseInt((await redis.get(otpRequestKey)) || \"0\");\n      if (otpRequestsCount >= OTP_MAX_ATTEMPTS) {\n        await redis.set(`otp_spam_lock:${email}`, \"locked\", {\n          expiration: {\n            type: \"EX\",\n            value: 3600\n          }\n        });\n        throw ApiError.tooManyRequests(\n          \"Too many otp requests. Please try again after 1 hour before requesting again.\"\n        );\n      }\n\n      await redis.set(otpRequestKey, otpRequestsCount + 1, {\n        expiration: {\n          type: \"EX\",\n          value: 3600\n        }\n      });\n    } catch (error) {\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to track otp requests!\");\n    }\n  }\n\n  static async sendOtp({\n    name,\n    email,\n    templateName,\n    code,\n    hashCode,\n    subject\n  }: SendOtpType) {\n    try {\n      const newOtp = generateOTP(OTP_CODE_LENGTH);\n\n      logger.info(`OTP generated successfully: ${code ? code : newOtp.code}`);\n\n      await sendEmail({\n        email,\n        subject,\n        data: {\n          code: code ? code : newOtp.code,\n          name\n        },\n        templateName\n      });\n\n      await redis.set(`otp:${email}`, hashCode ? hashCode : newOtp.hashCode, {\n        expiration: {\n          type: \"EX\",\n          value: OTP_EXPIRES_IN / 1000\n        }\n      });\n\n      await redis.set(`otp_cooldown:${email}`, OTP_COOL_DOWN, {\n        expiration: {\n          type: \"EX\",\n          value: OTP_COOL_DOWN\n        }\n      });\n    } catch (error) {\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to send otp!\");\n    }\n  }\n\n  static async verifyOtp(hashCode: string, email: string) {\n    const hashOtpCodeKey = await redis.get(`otp:${email}`);\n\n    if (!hashOtpCodeKey) {\n      throw ApiError.badRequest(\"Invalid or expired otp\");\n    }\n\n    const failedAttemptsKey = `otp_attempts:${email}`;\n    const failedAttempts = parseInt(\n      (await redis.get(failedAttemptsKey)) || \"0\"\n    );\n\n    if (hashOtpCodeKey !== hashCode) {\n      if (failedAttempts >= OTP_MAX_ATTEMPTS) {\n        await redis.set(`otp_lock:${email}`, \"locked\", {\n          EX: OTP_SPAM_LOCK_TIME / 1000\n        });\n        throw ApiError.tooManyRequests(\n          \"Too many failed attempts. Please try again after 1 hour.\"\n        );\n      }\n      await redis.set(failedAttemptsKey, failedAttempts + 1, {\n        EX: OTP_EXPIRES_IN / 1000\n      });\n      throw ApiError.badRequest(\n        `Incorrect OTP. ${OTP_MAX_ATTEMPTS - failedAttempts} attempts left.`\n      );\n    }\n\n    await redis.del([`otp:${email}`, failedAttemptsKey]);\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/health/health.routes.ts",
                          "content": "import { Router } from \"express\";\nimport { healthCheck, detailedHealthCheck } from \"./health.controller\";\n\nconst router = Router();\n\nrouter.get(\"/\", healthCheck);\nrouter.get(\"/detailed\", detailedHealthCheck);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/health/health.controller.ts",
                          "content": "import { Request, Response } from \"express\";\nimport { ApiResponse } from \"../../shared/utils/api-response\";\nimport { AsyncHandler } from \"../../shared/utils/async-handler\";\n\n/**\n * Basic health check endpoint\n * GET /api/health\n */\nexport const healthCheck = AsyncHandler(\n  async (_req: Request, res: Response) => {\n    return ApiResponse.Success(res, \"Service is healthy\", {\n      status: \"healthy\",\n      timestamp: new Date().toISOString(),\n      uptime: process.uptime()\n    });\n  }\n);\n\n/**\n * Detailed health check with system information\n * GET /api/health/detailed\n */\nexport const detailedHealthCheck = AsyncHandler(\n  async (_req: Request, res: Response) => {\n    const healthData = {\n      status: \"healthy\",\n      timestamp: new Date().toISOString(),\n      uptime: process.uptime(),\n      environment: process.env.NODE_ENV || \"development\",\n      version: process.env.npm_package_version || \"1.0.0\",\n      memory: {\n        used:\n          Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) /\n          100,\n        total:\n          Math.round((process.memoryUsage().heapTotal / 1024 / 1024) * 100) /\n          100,\n        unit: \"MB\"\n      },\n      cpu: {\n        usage: process.cpuUsage()\n      }\n    };\n\n    return ApiResponse.Success(res, \"Service is healthy\", healthData);\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/oauth/oauth.service.ts",
                          "content": "import { AuthService, CookieOptionsType } from \"../auth/auth.service\";\nimport User from \"../auth/user.model\";\n\ntype OAuthProfile = {\n  provider: string;\n  providerId: string;\n  name: string;\n  email: string | undefined;\n  isEmailVerified: boolean;\n  avatar: string | undefined;\n  ip: string;\n  userAgent: string;\n};\n\nexport class OAuthService {\n  static async handleOAuthLogin(\n    user: OAuthProfile,\n    context: CookieOptionsType\n  ) {\n    const existingUser = await User.findOne({ email: user.email });\n\n    if (existingUser) {\n      await User.findByIdAndUpdate(existingUser._id, {\n        provider: user.provider,\n        providerId: user.providerId,\n        isEmailVerified: user.isEmailVerified,\n        avatar: {\n          url: user.avatar\n        }\n      });\n      await AuthService.handleToken(\n        {\n          _id: existingUser._id.toString(),\n          role: existingUser.role,\n          ip: user.ip,\n          userAgent: user.userAgent\n        },\n        context\n      );\n      return existingUser;\n    }\n\n    const newUser = await User.create({\n      name: user.name,\n      email: user.email,\n      isEmailVerified: user.isEmailVerified,\n\n      provider: user.provider,\n      providerId: user.providerId,\n\n      avatar: {\n        url: user.avatar\n      }\n    });\n\n    await AuthService.handleToken(\n      {\n        _id: newUser._id.toString(),\n        role: newUser.role,\n        ip: user.ip,\n        userAgent: user.userAgent\n      },\n      context\n    );\n\n    return newUser;\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/oauth/oauth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport passport from \"passport\";\nimport { facebookOAuth, githubOAuth, googleOAuth } from \"./oauth.controller\";\n\nconst router = Router();\n\nrouter.get(\n  \"/github\",\n  passport.authenticate(\"github\", { scope: [\"user:email\"] })\n);\n\nrouter.get(\n  \"/github/callback\",\n  passport.authenticate(\"github\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed,\n    session: false\n  }),\n  githubOAuth\n);\n\nrouter.get(\n  \"/facebook\",\n  passport.authenticate(\"facebook\", { scope: [\"email\", \"user_location\"] })\n);\n\nrouter.get(\n  \"/facebook/callback\",\n  passport.authenticate(\"facebook\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed,\n    session: false,\n    failureMessage: true\n  }),\n  facebookOAuth\n);\n\nrouter.get(\n  \"/google\",\n  passport.authenticate(\"google\", {\n    scope: [\"email\", \"profile\", \"openid\"],\n    prompt: \"consent\"\n  })\n);\n\nrouter.get(\n  \"/google/callback\",\n  passport.authenticate(\"google\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed\n    session: false\n  }),\n  googleOAuth\n);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/oauth/oauth.controller.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport { Profile as GithubProfile } from \"passport-github2\";\nimport { Profile as GoogleProfile } from \"passport-google-oauth20\";\nimport { Profile as FacebookProfile } from \"passport-facebook\";\n\nimport { ApiResponse } from \"../../shared/utils/api-response\";\nimport { AsyncHandler } from \"../../shared/utils/async-handler\";\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { OAuthService } from \"./oauth.service\";\nimport { setAuthCookies } from \"../../shared/helpers/cookie.helper\";\n\n//? LOGIN WITH GITHUB\nexport const githubOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as GithubProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authenticated failed!\"));\n    }\n\n    const user = {\n      provider: data?.provider,\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails && data?.emails[0]?.value,\n      isEmailVerified: true,\n      avatar: data.photos && data.photos[0].value,\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(user, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    //? save the data into your databases\n\n    ApiResponse.ok(res, \"Signin Successfull\", {\n      user: {\n        _id: existingUser._id.toString(),\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n\n//? LOGIN WITH GOOGLE\nexport const googleOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as GoogleProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authenticated failed!\"));\n    }\n\n    const userInfo = {\n      provider: data?.provider,\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails && data?.emails[0]?.value,\n      isEmailVerified:\n        (data?.emails && data?.emails[0]?.verified === true) || true,\n      avatar: data.profileUrl || (data.photos && data.photos[0].value),\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    ApiResponse.ok(res, \"Signin Successfull\", {\n      user: {\n        _id: existingUser._id.toString(),\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n\n//? LOGIN WITH FACEBOOK\nexport const facebookOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as FacebookProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authenticated failed!\"));\n    }\n\n    const userInfo = {\n      provider: data?.provider,\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails && data?.emails[0]?.value,\n      isEmailVerified: true,\n      avatar: data.profileUrl || (data.photos && data.photos[0].value),\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    ApiResponse.ok(res, \"Signin Successfull\", {\n      user: {\n        _id: existingUser._id.toString(),\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/user.model.ts",
                          "content": "import mongoose, { Document, Model, Schema } from \"mongoose\";\n\nexport interface IAvatar {\n  public_id: string;\n  url: string;\n  size: number;\n}\n\nexport interface IUser extends Document {\n  _id: mongoose.Types.ObjectId;\n  name: string;\n  email: string;\n  password?: string;\n  role: \"user\" | \"admin\";\n  isEmailVerified: boolean;\n  lastLoginAt?: Date;\n  failedLoginAttempts: number;\n  lockUntil?: Date;\n  avatar?: IAvatar;\n\n  provider: \"local\" | \"google\" | \"github\";\n  providerId?: string;\n\n  isDeleted: boolean;\n  deletedAt?: Date | null;\n  reActivateAvailableAt?: Date | null;\n\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nconst userSchema = new Schema<IUser>(\n  {\n    name: {\n      type: String,\n      required: [true, \"Name is required\"],\n      trim: true\n    },\n    email: {\n      type: String,\n      required: [true, \"Email is required\"],\n      unique: true,\n      lowercase: true,\n      trim: true\n    },\n    password: {\n      type: String,\n      select: false,\n      default: null\n    },\n    provider: {\n      type: String,\n      enum: [\"local\", \"google\", \"github\"],\n      default: \"local\"\n    },\n    providerId: {\n      type: String,\n      default: null\n    },\n    role: {\n      type: String,\n      enum: [\"user\", \"admin\"],\n      default: \"user\"\n    },\n    avatar: {\n      public_id: String,\n      url: String,\n      size: Number\n    },\n    isEmailVerified: {\n      type: Boolean,\n      default: false\n    },\n    lastLoginAt: {\n      type: Date\n    },\n    failedLoginAttempts: {\n      type: Number,\n      required: true,\n      default: 0\n    },\n    lockUntil: {\n      type: Date\n    },\n    isDeleted: {\n      type: Boolean,\n      default: false\n    },\n    deletedAt: {\n      type: Date,\n      default: null\n    },\n    reActivateAvailableAt: {\n      type: Date,\n      default: null\n    }\n  },\n  {\n    timestamps: true\n  }\n);\n\n// Performance Indexes\nuserSchema.index({ provider: 1, providerId: 1 }); // Quick lookup for OAuth\nuserSchema.index({ role: 1 });\nuserSchema.index({ isDeleted: 1 }); // Optimized for soft-delete queries\n\nconst User: Model<IUser> =\n  mongoose.models.User || mongoose.model<IUser>(\"User\", userSchema);\n\nexport default User;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.validator.ts",
                          "content": "import * as z from \"zod\";\nimport { OTP_TYPES } from \"./auth.constants\";\n\nexport const nameSchema = z\n  .string({ error: \"Name must be a string\" })\n  .trim()\n  .min(3, {\n    message: \"Name must be at least 3 characters long\"\n  })\n  .max(50, {\n    message: \"Name must be at most 50 characters long\"\n  });\n\nexport const passwordSchema = z\n  .string({ error: \"Password must be a string\" })\n  .trim()\n  .min(6, {\n    message: \"Password must be at least 6 characters long\"\n  })\n  .max(80, {\n    message: \"Password must be at most 80 characters long\"\n  });\n\nexport const emailSchema = z\n  .email({ message: \"Please enter a valid email address.\" })\n  .max(100, { message: \"Email must be no more than 100 characters.\" });\n\nexport const roleSchema = z\n  .enum([\"user\", \"admin\"], {\n    error: \"Role must be either applicant, recruiter, or admin\"\n  })\n  .default(\"user\");\n\nexport const SigninSchema = z.object({\n  email: emailSchema,\n  password: z.string({ error: \"Password must be a string\" }).trim().min(1, {\n    message: \"Password is required\"\n  })\n});\n\nexport const SignupSchema = z\n  .object({\n    name: nameSchema,\n    email: emailSchema,\n    password: passwordSchema,\n    confirmPassword: passwordSchema,\n    role: roleSchema\n  })\n  .refine(\n    data => {\n      return data.password === data.confirmPassword;\n    },\n    {\n      message: \"Passwords do not match\",\n      path: [\"confirmPassword\"]\n    }\n  );\n\nexport const RequestOtpSchema = z.object({\n  email: emailSchema,\n  otpType: z.enum(OTP_TYPES, { error: \"Invalid otp type\" })\n});\n\nexport const VerifyOtpSchema = z.object({\n  otpCode: z.string().min(6, \"Please enter a valid OTP\"),\n  email: emailSchema\n});\n\nexport const ResetPasswordSchema = z.object({\n  email: emailSchema,\n  newPassword: passwordSchema\n});\n\nexport const ChangePasswordSchema = z.object({\n  oldPassword: z.string({ error: \"Password must be a string\" }).min(1, {\n    message: \"Old password is required\"\n  }),\n  newPassword: passwordSchema\n});\n\nexport const UpdateProfileSchema = z.object({\n  name: nameSchema.optional(),\n  avatar: z.string().optional()\n});\n\nexport const GoogleSigninSchema = z.object({\n  name: nameSchema,\n  email: emailSchema,\n  provider: z.enum([\"google\", \"github\"]).default(\"google\"),\n  providerId: z.string({ error: \"Provider id must be a string\" }).min(1, {\n    message: \"Provider id is required\"\n  }),\n  avatar: z.string().optional(),\n  isEmailVerified: z.boolean().default(false)\n});\n\nexport const DeleteAccountSchema = z.object({\n  userId: z.string({ error: \"User id must be a string\" }).min(1, {\n    message: \"User id is required\"\n  }),\n  type: z\n    .enum([\"soft\", \"hard\"], { error: \"Type must be either soft or hard\" })\n    .default(\"soft\")\n});\n\nexport type SignupUserType = z.infer<typeof SignupSchema>;\nexport type SigninUserType = z.infer<typeof SigninSchema>;\nexport type RequestOtpType = z.infer<typeof RequestOtpSchema>;\nexport type VerifyOtpType = z.infer<typeof VerifyOtpSchema>;\nexport type ResetPasswordType = z.infer<typeof ResetPasswordSchema>;\nexport type ChangePasswordType = z.infer<typeof ChangePasswordSchema>;\nexport type UpdateProfileType = z.infer<typeof UpdateProfileSchema>;\nexport type GoogleSigninType = z.infer<typeof GoogleSigninSchema>;\nexport type DeleteAccountType = z.infer<typeof DeleteAccountSchema>;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.types.ts",
                          "content": "import { OTP_TYPES } from \"@/modules/auth/auth.constants\";\n\nexport type OTPType = (typeof OTP_TYPES)[number];\n\nexport interface IUser {\n  _id: string;\n  name: string;\n  email: string;\n  password?: string;\n  role: \"user\" | \"admin\";\n  isEmailVerified: boolean;\n  lastLoginAt?: Date;\n  failedLoginAttempts: number;\n  lockUntil?: Date;\n  avatar?: {\n    url: string;\n    publicId: string;\n    size: number;\n  };\n  provider: \"local\" | \"google\" | \"github\";\n  providerId?: string;\n  isDeleted: boolean;\n  deletedAt?: Date;\n  reActivateAvailableAt?: Date;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nexport type RefreshTokenData = {\n  userId: string;\n  tokenHash: string;\n  expiresAt: Date;\n};\n\nexport type SessionData = {\n  userId: string;\n  sessionId: string;\n  refreshTokenHash: string;\n  userAgent: string;\n  ip: string;\n  createdAt: Date;\n  expiresAt: Date;\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.service.ts",
                          "content": "import { NextFunction } from \"express\";\nimport User from \"./user.model\";\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { hashPassword, verifyPassword } from \"./auth.helpers\";\nimport { SignupUserType, VerifyOtpType } from \"./auth.validator\";\nimport {\n  DELETE_ACCOUNT_TOKEN_EXPIRY,\n  LOCK_TIME_MS,\n  LOGIN_MAX_ATTEMPTS,\n  OTP_CODE_LENGTH,\n  OTP_EXPIRES_IN,\n  REACTIVATION_AVAILABLE_AT,\n  REFRESH_TOKEN_EXPIRY,\n  RESET_PASSWORD_TOKEN_EXPIRY,\n  SESSION_EXPIRY\n} from \"./auth.constants\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../../shared/utils/jwt\";\nimport {\n  generateHashedToken,\n  generateOTP,\n  generateSecureToken,\n  generateUUID\n} from \"../../shared/helpers/token.helpers\";\nimport { IUser, RefreshTokenData, SessionData } from \"./auth.types\";\nimport { OtpService } from \"../otp/otp.service\";\nimport { deleteFileFromCloudinary } from \"../upload/upload.service\";\nimport redisClient from \"../../shared/configs/redis\";\nimport { logger } from \"../../shared/utils/logger\";\nimport env from \"../../shared/configs/env\";\nimport { sendEmail } from \"../../shared/utils/send-mail\";\nimport { getRemainingTime } from \"../../shared/utils/date\";\n\nexport type CookieOptionsType = {\n  setAuthCookie?: (\n    accessToken: string,\n    refreshToken: string,\n    sessionId: string\n  ) => void;\n};\n\nexport class AuthService {\n  static async registerUser(user: Omit<SignupUserType, \"confirmPassword\">) {\n    try {\n      const { name, email, password, role } = user;\n      const existingUser = await User.findOne({\n        email\n      }).select(\"+password\");\n\n      if (existingUser) {\n        throw ApiError.conflict(\"User with this email already exists\");\n      }\n\n      const pending = await redisClient.get(`user:pending:${email}`);\n\n      if (pending) {\n        throw ApiError.conflict(\n          \"Signup already in progress. Check your email for OTP.\"\n        );\n      }\n\n      const hashedPassword = await hashPassword(password);\n\n      await OtpService.checkOtpRestrictions(email);\n      await OtpService.trackOtpRequests(email);\n\n      const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n      const redisKey = `user:${email}:${hashCode}`;\n      const indexKey = `user:pending:${email}`;\n      await redisClient.set(indexKey, hashCode, {\n        expiration: {\n          type: \"PX\",\n          value: OTP_EXPIRES_IN\n        }\n      });\n      const userData = JSON.stringify({\n        name,\n        email,\n        role,\n        password: hashedPassword\n      });\n\n      await OtpService.sendOtp({\n        name,\n        email,\n        templateName: \"email-verification\",\n        code,\n        hashCode,\n        subject: \"Email Verification\"\n      });\n\n      await redisClient.set(redisKey, userData, {\n        expiration: {\n          type: \"PX\",\n          value: OTP_EXPIRES_IN\n        }\n      });\n    } catch (error) {\n      logger.error(error, \"Failed to register user\");\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to register user\");\n    }\n  }\n\n  static async verifyUser({ email, otpCode }: VerifyOtpType) {\n    const hashCode = generateHashedToken(otpCode);\n\n    await OtpService.verifyOtp(hashCode, email);\n\n    const userData = await redisClient.get(`user:${email}:${hashCode}`);\n\n    if (!userData) {\n      throw ApiError.badRequest(\"Invalid or expired otp\");\n    }\n\n    const { name, email: userEmail, role, password } = JSON.parse(userData);\n\n    const user = await User.create({\n      name,\n      email: userEmail,\n      role,\n      password,\n      isEmailVerified: true\n    });\n\n    await redisClient.del(`user:${email}:${hashCode}`);\n    await redisClient.del(`user:pending:${email}`);\n\n    return {\n      _id: user._id,\n      name,\n      email,\n      role: role,\n      isEmailVerified: true\n    };\n  }\n\n  static async signinUser(\n    {\n      email,\n      password,\n      ip,\n      userAgent\n    }: {\n      email: string;\n      password: string;\n      ip: string;\n      userAgent: string;\n    },\n    setCookie: CookieOptionsType\n  ) {\n    try {\n      const user = await User.findOne({\n        email\n      }).select(\"+password\");\n      if (!user) {\n        throw ApiError.unauthorized(\"Invalid credentials\");\n      }\n\n      if (!user.isEmailVerified) {\n        throw ApiError.unauthorized(\"Email not verified\");\n      }\n\n      if (user.lockUntil && new Date() < user.lockUntil) {\n        throw ApiError.forbidden(\n          `Your account has been locked. Please try again after ${getRemainingTime(user.lockUntil).minutes} minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        );\n      }\n\n      const isPasswordValid = await verifyPassword(\n        password,\n        user.password || \"\"\n      );\n      if (!isPasswordValid) {\n        let lockUntil = null;\n\n        let newAttempts = user.failedLoginAttempts + 1;\n\n        if (newAttempts >= LOGIN_MAX_ATTEMPTS) {\n          lockUntil = new Date(Date.now() + LOCK_TIME_MS);\n        }\n\n        await User.updateOne(\n          {\n            _id: user._id\n          },\n          {\n            failedLoginAttempts: newAttempts,\n            lockUntil\n          }\n        );\n\n        throw ApiError.unauthorized(\"Invalid credentials\");\n      }\n\n      await User.updateOne(\n        {\n          _id: user._id\n        },\n        {\n          $set: {\n            failedLoginAttempts: 0\n          },\n          $unset: {\n            lockUntil: 1\n          }\n        }\n      );\n\n      await AuthService.handleToken(\n        {\n          _id: user._id.toString(),\n          role: user.role,\n          ip,\n          userAgent\n        },\n        setCookie\n      );\n\n      return {\n        id: user.id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        isEmailVerified: user.isEmailVerified\n      };\n    } catch (err) {\n      if (err instanceof ApiError) {\n        throw err;\n      }\n      throw ApiError.server(\"Signin failed\");\n    }\n  }\n\n  static async handleToken(\n    user: Pick<IUser, \"_id\" | \"role\"> & {\n      ip: string;\n      userAgent: string;\n    },\n    context: CookieOptionsType\n  ) {\n    const sessionId = generateUUID();\n\n    const accessToken = generateAccessToken({\n      _id: user._id,\n      role: user.role,\n      sessionId\n    });\n\n    const refreshToken = generateRefreshToken({\n      _id: user._id.toString(),\n      sessionId\n    });\n\n    const hashedRefreshToken = generateHashedToken(refreshToken);\n\n    const refreshTokenData: RefreshTokenData = {\n      userId: user._id,\n      tokenHash: hashedRefreshToken,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    };\n\n    const sessionData: SessionData = {\n      userId: user._id,\n      sessionId,\n      refreshTokenHash: hashedRefreshToken,\n      userAgent: user.userAgent,\n      ip: user.ip,\n      createdAt: new Date(),\n      expiresAt: new Date(Date.now() + SESSION_EXPIRY)\n    };\n\n    const refreshTokenKey = `refreshToken:${hashedRefreshToken}`;\n\n    await redisClient.set(refreshTokenKey, JSON.stringify(refreshTokenData), {\n      expiration: {\n        type: \"PX\",\n        value: REFRESH_TOKEN_EXPIRY\n      }\n    });\n\n    const sessionKey = `session:${sessionId}`;\n\n    const userSessionsKey = `user_sessions:${user._id}`;\n\n    await redisClient.set(sessionKey, JSON.stringify(sessionData), {\n      expiration: {\n        type: \"PX\",\n        value: SESSION_EXPIRY\n      }\n    });\n\n    // add sessionId to user's set\n    await redisClient.sAdd(userSessionsKey, sessionId);\n\n    context.setAuthCookie &&\n      context.setAuthCookie(accessToken, refreshToken, sessionId);\n\n    await User.updateOne(\n      {\n        _id: user._id\n      },\n      {\n        $set: {\n          lastLogin: new Date(),\n          failedLoginAttempts: 0\n        },\n        $unset: {\n          lockUntil: 1\n        }\n      }\n    );\n  }\n\n  static async getUserProfile(userId: string) {\n    const user = await User.findById(userId);\n    return user;\n  }\n\n  static async refreshTokens(accessToken: string | null, refreshToken: string) {\n    if (!refreshToken) {\n      throw ApiError.unauthorized(\"Unauthorized, please login.\");\n    }\n\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n\n    if (!decodedRefresh?._id) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const refreshTokenKey = `refreshToken:${refreshTokenHash}`;\n\n    const storedToken = await redisClient.get(refreshTokenKey);\n    if (!storedToken) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    const { userId, tokenHash, expiresAt } = JSON.parse(\n      storedToken\n    ) as RefreshTokenData;\n\n    if (userId !== decodedRefresh._id) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    // Reuse detection\n    if (!storedToken) {\n      throw ApiError.unauthorized(\"Token reuse detected. Please login again.\");\n    }\n\n    if (expiresAt < new Date()) {\n      throw ApiError.unauthorized(\"Refresh token expired.\");\n    }\n\n    const session = await redisClient.get(\n      `session:${decodedRefresh.sessionId}`\n    );\n\n    if (!session) {\n      throw ApiError.unauthorized(\"Session not found.\");\n    }\n\n    const storedSessionData = JSON.parse(session) as SessionData;\n\n    if (\n      decodedRefresh.sessionId !== storedSessionData.sessionId ||\n      decodedRefresh._id !== storedSessionData.userId\n    ) {\n      throw ApiError.unauthorized(\"Token-session mismatch\");\n    }\n\n    if (accessToken) {\n      const decodedAccess = verifyAccessToken(accessToken);\n      if (decodedAccess._id !== decodedRefresh._id) {\n        throw ApiError.unauthorized(\"Token mismatch.\");\n      }\n    }\n\n    const user = await User.findById(decodedRefresh._id);\n    if (!user) {\n      throw ApiError.unauthorized(\"User not found.\");\n    }\n\n    const newAccessToken = generateAccessToken({\n      _id: user._id.toString(),\n      role: user.role,\n      sessionId: storedSessionData.sessionId\n    });\n\n    const newRefreshToken = generateRefreshToken({\n      _id: user._id.toString(),\n      sessionId: storedSessionData.sessionId\n    });\n    const newRefreshTokenHash = generateHashedToken(newRefreshToken);\n\n    //? Rotate token\n    await Promise.all([\n      redisClient.del(`refreshToken:${tokenHash}`),\n      redisClient.del(`session:${storedSessionData.sessionId}`)\n    ]);\n\n    const refreshTokenData: RefreshTokenData = {\n      userId: user._id.toString(),\n      tokenHash: newRefreshTokenHash,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    };\n    const sessionData: SessionData = {\n      userId: user._id.toString(),\n      sessionId: storedSessionData.sessionId,\n      refreshTokenHash: newRefreshTokenHash,\n      userAgent: storedSessionData.userAgent,\n      ip: storedSessionData.ip,\n      createdAt: new Date(),\n      expiresAt: new Date(Date.now() + SESSION_EXPIRY)\n    };\n\n    const newRefreshTokenKey = `refreshToken:${newRefreshTokenHash}`;\n    const newSessionKey = `session:${storedSessionData.sessionId}`;\n\n    await Promise.all([\n      redisClient.set(newRefreshTokenKey, JSON.stringify(refreshTokenData), {\n        expiration: {\n          type: \"PX\",\n          value: REFRESH_TOKEN_EXPIRY\n        }\n      }),\n      redisClient.set(newSessionKey, JSON.stringify(sessionData), {\n        expiration: {\n          type: \"PX\",\n          value: SESSION_EXPIRY\n        }\n      })\n    ]);\n\n    //? delete old refresh token\n    await redisClient.del(refreshTokenKey);\n\n    return {\n      accessToken: newAccessToken,\n      refreshToken: newRefreshToken,\n      sessionId: storedSessionData.sessionId\n    };\n  }\n\n  static async logoutUser(userId: string, sessionId: string) {\n    const sessionKey = `session:${sessionId}`;\n    const sessionData = await redisClient.get(sessionKey);\n    const userSessionsKey = `user_sessions:${userId}`;\n    if (!sessionData) {\n      throw ApiError.unauthorized(\"Session not found.\");\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.userId !== userId) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const refreshTokenKey = `refreshToken:${session.refreshTokenHash}`;\n\n    await redisClient.del(sessionKey);\n    await redisClient.del(refreshTokenKey);\n    await redisClient.sRem(userSessionsKey, sessionId);\n  }\n\n  static async forgotPassword(email: string) {\n    const user = await User.findOne({\n      email\n    });\n\n    if (!user) {\n      throw ApiError.badRequest(\n        \"If an account exists, a reset code has been sent.\"\n      );\n    }\n\n    const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n    await OtpService.checkOtpRestrictions(email);\n    await OtpService.trackOtpRequests(email);\n\n    const redisKey = `reset_password:${email}:${hashCode}`;\n\n    await redisClient.set(redisKey, hashCode, {\n      expiration: {\n        type: \"PX\",\n        value: RESET_PASSWORD_TOKEN_EXPIRY\n      }\n    });\n\n    await OtpService.sendOtp({\n      email,\n      subject: \"Password Reset\",\n      templateName: \"forgot-password\",\n      name: user.name,\n      code,\n      hashCode\n    });\n  }\n\n  static async verifyResetPasswordOtp(otpCode: string, email: string) {\n    const hashedCode = generateHashedToken(otpCode);\n\n    const redisKey = `reset_password:${email}:${hashedCode}`;\n    const storedHashCode = await redisClient.get(redisKey);\n    if (!storedHashCode) {\n      throw ApiError.unauthorized(\"Invalid or expired otp\");\n    }\n    await OtpService.verifyOtp(storedHashCode, email);\n\n    await redisClient.del(`reset_password:${email}:${hashedCode}`);\n    await redisClient.set(`reset_password:status:${email}`, \"pending\", {\n      expiration: {\n        type: \"PX\",\n        value: RESET_PASSWORD_TOKEN_EXPIRY\n      }\n    });\n  }\n\n  static async resetPassword(\n    next: NextFunction,\n    email: string,\n    newPassword: string\n  ) {\n    const user = await User.findOne({\n      email\n    }).select(\"+password\");\n\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${\n            getRemainingTime(user.lockUntil).minutes\n          } minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        )\n      );\n    }\n\n    if (user.failedLoginAttempts >= LOGIN_MAX_ATTEMPTS && user.lockUntil) {\n      return next(\n        ApiError.forbidden(\n          `You have exceeded the maximum number of login attempts. Please try again after ${\n            getRemainingTime(user.lockUntil).minutes\n          } minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        )\n      );\n    }\n\n    if (!user.isEmailVerified) {\n      return next(ApiError.unauthorized(\"Please verify your email first.\"));\n    }\n\n    const redisKey = `reset_password:status:${email}`;\n    const status = await redisClient.get(redisKey);\n    if (status !== \"pending\") {\n      return next(\n        ApiError.unauthorized(\n          \"Please request a password reset before attempting to set a new password.\"\n        )\n      );\n    }\n\n    const oldPassword = user.password;\n\n    const isOldPassword = await verifyPassword(\n      newPassword,\n      oldPassword as string\n    );\n\n    if (isOldPassword) {\n      return next(ApiError.badRequest(\"New password should be different!\"));\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n    await User.updateOne(\n      {\n        email\n      },\n      {\n        $set: {\n          password: hashedPassword\n        }\n      }\n    );\n    await redisClient.del(`reset_password:status:${email}`);\n\n    //? Delete all user sessions\n    await this.deleteAllUserSessions(user._id.toString());\n\n    return {\n      message: \"Password reset successfully. Please login!\"\n    };\n  }\n\n  static async changePassword(\n    next: NextFunction,\n    {\n      newPassword,\n      oldPassword,\n      userId\n    }: {\n      userId: string;\n      newPassword: string;\n      oldPassword: string;\n    }\n  ) {\n    const user = await User.findById(userId).select(\"+password\");\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    if (!user.isEmailVerified) {\n      return next(ApiError.unauthorized(\"Please verify your email first.\"));\n    }\n\n    const isOldPassword = await verifyPassword(\n      oldPassword,\n      user.password || \"\"\n    );\n\n    if (!isOldPassword) {\n      return next(ApiError.unauthorized(\"Invalid credentials\"));\n    }\n\n    if (newPassword === oldPassword) {\n      return next(ApiError.badRequest(\"New password should be different!\"));\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n    await User.updateOne(\n      {\n        _id: userId\n      },\n      {\n        $set: {\n          password: hashedPassword\n        }\n      }\n    );\n\n    await this.deleteAllUserSessions(userId);\n\n    return {\n      message: \"Password changed successfully. Please login again!\"\n    };\n  }\n\n  static async requestDeleteAccount(userId: string, password: string) {\n    const user = await User.findById(userId).select(\"+password\");\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const isPasswordValid = await verifyPassword(password, user.password || \"\");\n\n    if (!isPasswordValid) {\n      let lockUntil = null;\n\n      let newAttempts = user.failedLoginAttempts + 1;\n\n      if (newAttempts >= LOGIN_MAX_ATTEMPTS) {\n        lockUntil = new Date(Date.now() + LOCK_TIME_MS);\n      }\n\n      await User.updateOne(\n        {\n          _id: user._id\n        },\n        {\n          failedLoginAttempts: newAttempts,\n          lockUntil\n        }\n      );\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    const token = generateSecureToken();\n    const hashedToken = generateHashedToken(token);\n\n    const redisKey = `delete_account:token:${userId}`;\n\n    if (await redisClient.get(redisKey)) {\n      throw ApiError.badRequest(\"Delete account token already requested!\");\n    }\n\n    await redisClient.set(redisKey, hashedToken, {\n      expiration: {\n        type: \"PX\",\n        value: DELETE_ACCOUNT_TOKEN_EXPIRY\n      }\n    });\n\n    const deleteAccountUrl = `${env.CLIENT_URL}/account/delete?token=${token}`;\n    logger.warn(`Delete account token: ${token}`);\n    await sendEmail({\n      email: user.email,\n      subject: \"Delete Account Request\",\n      templateName: \"delete-account\",\n      data: {\n        name: user.name,\n        deleteAccountUrl\n      }\n    });\n  }\n\n  static async deleteOrDeactiveAccount({\n    userId,\n    type,\n    token\n  }: {\n    userId: string;\n    type: \"soft\" | \"hard\";\n    token: string;\n  }) {\n    const user = await User.findById(userId);\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const redisKey = `delete_account:token:${userId}`;\n    const storedToken = await redisClient.get(redisKey);\n    if (!storedToken) {\n      throw ApiError.badRequest(\"Invalid or expired token!\");\n    }\n\n    const isTokenValid = generateHashedToken(token) === storedToken;\n    if (!isTokenValid) {\n      throw ApiError.badRequest(\"Invalid or expired token!\");\n    }\n\n    await redisClient.del(redisKey);\n\n    if (type === \"soft\") {\n      user.isDeleted = true;\n      user.deletedAt = new Date();\n      user.reActivateAvailableAt = new Date(\n        Date.now() + REACTIVATION_AVAILABLE_AT\n      );\n      await user.save();\n      await this.deleteAllUserSessions(userId);\n    } else if (type === \"hard\") {\n      if (user?.avatar?.public_id) {\n        await deleteFileFromCloudinary([user.avatar.public_id]);\n      }\n      await User.findOneAndDelete({\n        _id: userId\n      });\n      await this.deleteAllUserSessions(userId);\n      await user.save();\n    }\n  }\n\n  static async reactivateAccount(userId: string) {\n    const user = await User.findById(userId);\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n      const remainingTime = getRemainingTime(user.lockUntil);\n      throw ApiError.badRequest(\n        `Your account has been locked. Please try again after ${remainingTime.minutes} minutes and ${remainingTime.seconds} seconds.`\n      );\n    }\n\n    if (!user?.isDeleted || !user?.deletedAt) {\n      throw ApiError.badRequest(\"Your account is already active!\");\n    }\n\n    if (\n      user?.reActivateAvailableAt &&\n      new Date(user?.reActivateAvailableAt) > new Date()\n    ) {\n      throw ApiError.forbidden(\n        `Your account has been locked. Please try again after ${\n          getRemainingTime(user.reActivateAvailableAt).minutes\n        } minutes and ${getRemainingTime(user.reActivateAvailableAt).seconds} seconds.`\n      );\n    }\n\n    await User.findOneAndUpdate(\n      {\n        _id: userId\n      },\n      {\n        $set: {\n          isDeleted: false,\n          deletedAt: null,\n          reActivateAvailableAt: null\n        }\n      },\n      {\n        new: true\n      }\n    );\n\n    await user.save();\n  }\n\n  static async getUserSessions(userId: string, currentSid: string) {\n    const sessionIds = await redisClient.sMembers(`user_sessions:${userId}`);\n\n    const sessions = await Promise.all(\n      sessionIds.map(async id => {\n        const data = await redisClient.get(`session:${id}`);\n        return data ? JSON.parse(data) : null;\n      })\n    );\n\n    const filteredData = sessions\n      .filter(Boolean)\n      .map((session: SessionData) => {\n        return {\n          sessionId: session.sessionId,\n          userAgent: session.userAgent,\n          ip: session.ip,\n          createdAt: session.createdAt,\n          expiresAt: session.expiresAt,\n          current: session.sessionId === currentSid\n        };\n      });\n\n    return filteredData;\n  }\n\n  static async deleteUserSession(userId: string, sessionId: string) {\n    const sessionKey = `session:${sessionId}`;\n    const userSessionsKey = `user_sessions:${userId}`;\n    const sessionData = await redisClient.get(sessionKey);\n\n    if (!sessionData) {\n      throw ApiError.unauthorized(\"Session not found.\");\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.userId !== userId) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n    const refreshTokenKey = `refreshToken:${session.refreshTokenHash}`;\n    await redisClient.del(sessionKey);\n    await redisClient.del(refreshTokenKey);\n    await redisClient.sRem(userSessionsKey, sessionId);\n  }\n\n  static async deleteAllUserSessions(userId: string) {\n    const userSessionsKey = `user_sessions:${userId}`;\n    const sessionIds = await redisClient.sMembers(userSessionsKey);\n    if (sessionIds.length) {\n      const keys = sessionIds.map(id => `session:${id}`);\n      await redisClient.del(keys);\n    }\n    await redisClient.del(userSessionsKey);\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport {\n  ChangePasswordSchema,\n  DeleteAccountSchema,\n  RequestOtpSchema,\n  ResetPasswordSchema,\n  SigninSchema,\n  SignupSchema,\n  UpdateProfileSchema,\n  VerifyOtpSchema\n} from \"./auth.validator\";\nimport {\n  changePassword,\n  deleteAccount,\n  deleteAllUserSessions,\n  deleteUserSession,\n  forgotPassword,\n  getUserProfile,\n  getUserSessions,\n  logoutUser,\n  reactivateAccount,\n  refreshToken,\n  requestDeleteAccount,\n  resetPassword,\n  signinUser,\n  signupUser,\n  updateProfile,\n  verifyResetPasswordOtp,\n  verifyUser\n} from \"./auth.controller\";\nimport { verifyAuthentication } from \"@/shared/middlewares/verify-auth\";\nimport { checkUserAccountRestriction } from \"@/shared/middlewares/user-account-restriction\";\nimport {\n  changePasswordLimiter,\n  deleteAccountLimiter,\n  otpRequestLimiter,\n  resetPasswordLimiter,\n  signinRateLimiter,\n  signupRateLimiter\n} from \"@/shared/middlewares/rate-limiter\";\nimport upload from \"@/shared/middlewares/upload-file\";\nimport { validateRequest } from \"@/shared/middlewares/validate-request\";\n\nconst router = Router();\n\nrouter.post(\n  \"/signup\",\n  validateRequest(SignupSchema),\n  signupRateLimiter,\n  signupUser\n);\n\nrouter.post(\"/verify-user\", validateRequest(VerifyOtpSchema), verifyUser);\n\nrouter.post(\n  \"/signin\",\n  validateRequest(SigninSchema),\n  signinRateLimiter,\n  signinUser\n);\n\nrouter.get(\"/profile\", verifyAuthentication, getUserProfile);\n\nrouter.patch(\n  \"/profile\",\n  upload.single(\"avatar\"),\n  validateRequest(UpdateProfileSchema),\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  updateProfile\n);\n\nrouter.get(\"/sessions\", verifyAuthentication, getUserSessions);\n\nrouter.delete(\n  \"/sessions\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  deleteAllUserSessions\n);\n\nrouter.delete(\n  \"/sessions/:sessionId\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  deleteUserSession\n);\n\nrouter.post(\"/refresh-token\", refreshToken);\n\nrouter.post(\n  \"/logout\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  logoutUser\n);\n\nrouter.post(\n  \"/forgot-password\",\n  validateRequest(RequestOtpSchema.pick({ email: true })),\n  otpRequestLimiter,\n  forgotPassword\n);\n\nrouter.post(\n  \"/verify-reset-otp\",\n  validateRequest(VerifyOtpSchema),\n  otpRequestLimiter,\n  verifyResetPasswordOtp\n);\n\nrouter.post(\n  \"/reset-password\",\n  validateRequest(ResetPasswordSchema),\n  resetPasswordLimiter,\n  resetPassword\n);\n\nrouter.post(\n  \"/change-password\",\n  verifyAuthentication,\n  validateRequest(ChangePasswordSchema),\n  checkUserAccountRestriction,\n  changePasswordLimiter,\n  changePassword\n);\n\nrouter.post(\n  \"/account/request-delete\",\n  verifyAuthentication,\n  validateRequest(SigninSchema.pick({ password: true })),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  requestDeleteAccount\n);\n\nrouter.delete(\n  \"/account/delete\",\n  verifyAuthentication,\n  validateRequest(DeleteAccountSchema),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  deleteAccount\n);\n\nrouter.put(\"/account/reactivate\", verifyAuthentication, reactivateAccount);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.helpers.ts",
                          "content": "import argon2 from \"argon2\";\n\nexport async function hashPassword(password: string): Promise<string> {\n  return argon2.hash(password);\n}\n\nexport async function verifyPassword(\n  password: string,\n  hash: string\n): Promise<boolean> {\n  return argon2.verify(hash, password);\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.controller.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport { ApiResponse } from \"../../shared/utils/api-response\";\nimport { AsyncHandler } from \"../../shared/utils/async-handler\";\n\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { AuthService } from \"./auth.service\";\nimport {\n  clearAuthCookies,\n  clearCookie,\n  setAuthCookies\n} from \"../../shared/helpers/cookie.helper\";\nimport { UserRequest } from \"../../types/global\";\nimport {\n  deleteFileFromCloudinary,\n  uploadToCloudinary\n} from \"../upload/upload.service\";\nimport { DeleteAccountType, VerifyOtpType } from \"./auth.validator\";\n\n//? SIGNUP USER\nexport const signupUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { name, email, password, role } = req.body;\n    if (!name || !email || !password) {\n      return next(ApiError.badRequest(\"Name, email and password are required\"));\n    }\n\n    await AuthService.registerUser({\n      name,\n      email,\n      password,\n      role\n    });\n\n    return ApiResponse.Success(\n      res,\n      \"User registered successfully. Please check your email for verification.\"\n    );\n  }\n);\n\n//? VERIFY USER\nexport const verifyUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, otpCode }: VerifyOtpType = req.body;\n\n    if (!email || !otpCode) {\n      return next(ApiError.badRequest(\"Email and code are required\"));\n    }\n\n    await AuthService.verifyUser({ email, otpCode });\n\n    return ApiResponse.ok(res, \"User verified successfully\");\n  }\n);\n\n//? SIGNIN USER\nexport const signinUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, password } = req.body;\n    if (!email || !password) {\n      return next(ApiError.badRequest(\"Email and password are required\"));\n    }\n\n    const ip = req.ip || \"Unknown\";\n    const userAgent = req.headers[\"user-agent\"] || \"Unknown\";\n\n    await AuthService.signinUser(\n      { email, password, ip, userAgent },\n      {\n        setAuthCookie: (\n          accessToken: string,\n          refreshToken: string,\n          sessionId: string\n        ) => {\n          setAuthCookies(res, accessToken, refreshToken, sessionId);\n        }\n      }\n    );\n\n    return ApiResponse.ok(res, \"User signed in successfully!\");\n  }\n);\n\n//? GET USER PROFILE\nexport const getUserProfile = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(userId.toString());\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    if (user.isDeleted) {\n      return next(ApiError.notFound(\"This account has been deactivated.\"));\n    }\n\n    const result = await AuthService.getUserSessions(\n      userId.toString(),\n      currentSessionId\n    );\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to get user sessions!\"));\n    }\n\n    return ApiResponse.ok(res, \"User profile fetched successfully\", {\n      user: {\n        _id: user._id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        avatar: user.avatar,\n        isEmailVerified: user.isEmailVerified,\n        lastLoginAt: user.lastLoginAt,\n        sessions: result\n      }\n    });\n  }\n);\n\n//? UPDATE PROFILE\nexport const updateProfile = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const data = req.body;\n    const { name } = data;\n\n    if (!req.user?._id) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(req.user?._id.toString());\n\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    if (req?.file && user?.avatar?.public_id) {\n      await deleteFileFromCloudinary([user.avatar.public_id]);\n    }\n\n    if (req?.file && user?.avatar) {\n      const file = await uploadToCloudinary(req.file.buffer, {\n        folder: \"uploads/files\",\n        resource_type: \"auto\"\n      });\n      user.avatar = {\n        public_id: req.file\n          ? file.public_id\n          : (user?.avatar?.public_id as string),\n        url: req.file ? file.url : (user.avatar.url as string),\n        size: req.file ? file.size : (user.avatar.size as number)\n      };\n    }\n\n    if (name) {\n      user.name = name;\n    }\n\n    await user.save();\n\n    return ApiResponse.Success(res, \"Profile updated successfully!\", {\n      user: {\n        _id: user._id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        avatar: user.avatar,\n        isEmailVerified: user.isEmailVerified,\n        lastLoginAt: user.lastLoginAt\n      }\n    });\n  }\n);\n\n//? REFRESH TOKENS\nexport const refreshToken = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const accessToken = req.cookies?.accessToken;\n    const refreshToken = req.cookies?.refreshToken;\n\n    const token = await AuthService.refreshTokens(accessToken, refreshToken);\n\n    if (!token) {\n      return next(ApiError.server(\"Failed to refresh tokens!\"));\n    }\n\n    const newAccessToken = token.accessToken;\n    const newRefreshToken = token.refreshToken;\n    setAuthCookies(res, newAccessToken, newRefreshToken, token.sessionId);\n    clearCookie(res, \"refreshToken\");\n\n    return ApiResponse.Success(res, \"Tokens refreshed successfully!\");\n  }\n);\n\n//? LOGOUT\nexport const logoutUser = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req.user?._id;\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const currentSessionId = req.user?.sessionId;\n    if (!currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.logoutUser(userId.toString(), currentSessionId);\n\n    clearAuthCookies(res);\n\n    return ApiResponse.Success(res, \"Logged out successfully!\");\n  }\n);\n\n//? FORGOT PASSWORD\nexport const forgotPassword = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email } = req.body;\n    if (!email) {\n      return next(ApiError.badRequest(\"Email is required!\"));\n    }\n\n    await AuthService.forgotPassword(email);\n\n    return ApiResponse.ok(\n      res,\n      \"If an account exists, a reset code has been sent to your email.\"\n    );\n  }\n);\n\n//? VERIFY RESET PASSWORD TOKEN\nexport const verifyResetPasswordOtp = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { otpCode, email } = req.body;\n    if (!otpCode || !email) {\n      return next(ApiError.badRequest(\"OtpCode and email are required!\"));\n    }\n\n    await AuthService.verifyResetPasswordOtp(otpCode, email);\n\n    return ApiResponse.ok(\n      res,\n      \"Password reset otp verified successfully. You can now reset your password.\"\n    );\n  }\n);\n\n//? RESET PASSWORD\nexport const resetPassword = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { newPassword, email } = req.body;\n    if (!email || !newPassword) {\n      return next(ApiError.badRequest(\"Newpassword and email are required!\"));\n    }\n\n    const result = await AuthService.resetPassword(next, email, newPassword);\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to reset password!\"));\n    }\n\n    clearAuthCookies(res);\n\n    return ApiResponse.ok(\n      res,\n      result.message || \"Password reset successfully!\"\n    );\n  }\n);\n\n//? CHANGE PASSWORD\nexport const changePassword = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const { oldPassword, newPassword } = req.body;\n\n    if (!oldPassword || !newPassword) {\n      return next(\n        ApiError.badRequest(\"Old password and new password are required\")\n      );\n    }\n\n    const result = await AuthService.changePassword(next, {\n      userId: userId.toString(),\n      oldPassword,\n      newPassword\n    });\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to change password!\"));\n    }\n\n    clearAuthCookies(res);\n\n    return ApiResponse.ok(\n      res,\n      result.message || \"Password changed successfully!\"\n    );\n  }\n);\n\n//? REQUEST DELETE ACCOUNT\nexport const requestDeleteAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n    const { password } = req.body;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    if (!password) {\n      return next(ApiError.badRequest(\"Password is required!\"));\n    }\n\n    await AuthService.requestDeleteAccount(userId, password);\n\n    return ApiResponse.ok(\n      res,\n      \"Account deletion request sent successfully. Please check your email to confirm.\"\n    );\n  }\n);\n\n//? DELETE/DEACTIVATE ACCOUNT\nexport const deleteAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { userId, type }: DeleteAccountType = req.body;\n\n    if (!userId || !type) {\n      return next(ApiError.badRequest(\"User id and type are required!\"));\n    }\n\n    const reqUserId = req?.user?._id;\n\n    if (!reqUserId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n    const token = req.query.token as string;\n    if (!token) {\n      return next(\n        ApiError.badRequest(\n          `${type === \"hard\" ? \"Delete\" : \"Deactivate\"} account token is required!`\n        )\n      );\n    }\n\n    if (userId !== reqUserId) {\n      return next(\n        ApiError.unauthorized(\"You are not authorized to perform this action\")\n      );\n    }\n\n    await AuthService.deleteOrDeactiveAccount({ userId, type, token });\n\n    clearAuthCookies(res);\n\n    return ApiResponse.Success(\n      res,\n      `Account ${type === \"soft\" ? \"deactivated\" : \"deleted\"} successfully!`\n    );\n  }\n);\n\n//? REACTIVATE ACCOUNT\nexport const reactivateAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.reactivateAccount(userId);\n\n    return ApiResponse.Success(res, \"Account reactivated successfully!\");\n  }\n);\n\n//? GET USER SESSIONS\nexport const getUserSessions = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const result = await AuthService.getUserSessions(\n      userId.toString(),\n      currentSessionId\n    );\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to get user sessions!\"));\n    }\n\n    return ApiResponse.ok(res, \"User sessions fetched successfully\", result);\n  }\n);\n\n//? DELETE SESSION\nexport const deleteUserSession = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n    const { sessionId } = req.params;\n\n    if (!userId || !sessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.deleteUserSession(userId, sessionId as string);\n\n    const reqSId = req.cookies?.sid;\n\n    const isCurrentSession = sessionId === reqSId;\n    if (isCurrentSession) {\n      clearAuthCookies(res);\n    }\n\n    return ApiResponse.Success(res, \"User session deleted successfully!\");\n  }\n);\n\n//? DELETE ALL SESSIONS\nexport const deleteAllUserSessions = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?._id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.deleteAllUserSessions(userId);\n\n    clearAuthCookies(res);\n    // clearCookie(res, \"sid\");\n\n    return ApiResponse.Success(res, \"User sessions deleted successfully!\");\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.constants.ts",
                          "content": "export const OTP_MAX_ATTEMPTS = 5;\nexport const OTP_TYPES = [\n  \"signin\",\n  \"email-verification\",\n  \"password-reset\",\n  \"password-change\"\n] as const;\n\nexport const NEXT_OTP_DELAY = 1 * 60 * 1000; // 1 minute\n\nexport const LOGIN_MAX_ATTEMPTS = 5 as const;\n\nexport const OTP_CODE_LENGTH = 6 as const;\n\nexport const OTP_COOL_DOWN = 60;\n\nexport const OTP_EXPIRES_IN = 5 * 60 * 1000; // 5 minutes\n\nexport const OTP_SPAM_LOCK_TIME = 3600; // 1 hour\n\nexport const LOCK_TIME_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport const ACCESS_TOKEN_EXPIRY = 15 * 60 * 1000; // 15 minutes\n\nexport const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days\n\nexport const SESSION_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days\n\nexport const RESET_PASSWORD_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n\nexport const REACTIVATION_AVAILABLE_AT = 24 * 60 * 60 * 1000; // 24 hours\n\nexport const DELETE_ACCOUNT_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n"
                        }
                      ]
                    }
                  }
                }
              }
            },
            "postgresql": {
              "orms": {
                "drizzle": {
                  "dependencies": {
                    "runtime": [
                      "express",
                      "drizzle-orm",
                      "pg",
                      "resend",
                      "redis",
                      "@paralleldrive/cuid2",
                      "argon2",
                      "cloudinary",
                      "cookie-parser",
                      "cors",
                      "jsonwebtoken",
                      "express-rate-limit",
                      "helmet",
                      "multer",
                      "nodemailer",
                      "passport",
                      "passport-github2",
                      "passport-facebook",
                      "passport-google-oauth20",
                      "pino",
                      "pino-pretty",
                      "zod",
                      "dotenv-flow",
                      "cross-env",
                      "source-map-support",
                      "swagger-autogen",
                      "swagger-ui-express",
                      "ejs"
                    ],
                    "dev": [
                      "@types/express",
                      "@types/cookie-parser",
                      "@types/cors",
                      "@types/morgan",
                      "@types/multer",
                      "@types/nodemailer",
                      "@types/jsonwebtoken",
                      "@types/passport",
                      "@types/passport-github2",
                      "@types/passport-facebook",
                      "@types/passport-google-oauth20",
                      "morgan",
                      "@types/source-map-support",
                      "@types/swagger-ui-express",
                      "@types/ejs",
                      "@types/pg",
                      "drizzle-kit"
                    ]
                  },
                  "env": [
                    "PORT",
                    "NODE_ENV",
                    "LOG_LEVEL",
                    "CORS_ORIGIN",
                    "CRYPTO_SECRET",
                    "DATABASE_URL",
                    "JWT_ACCESS_SECRET",
                    "JWT_REFRESH_SECRET",
                    "RESEND_API_KEY",
                    "REDIS_URL",
                    "EMAIL_FROM",
                    "CLOUDINARY_CLOUD_NAME",
                    "CLOUDINARY_API_KEY",
                    "CLOUDINARY_API_SECRET",
                    "GITHUB_CLIENT_ID",
                    "GITHUB_CLIENT_SECRET",
                    "GITHUB_REDIRECT_URI",
                    "GOOGLE_CLIENT_ID",
                    "GOOGLE_CLIENT_SECRET",
                    "GOOGLE_REDIRECT_URI",
                    "FACEBOOK_APP_ID",
                    "FACEBOOK_APP_SECRET",
                    "FACEBOOK_REDIRECT_URI"
                  ],
                  "architectures": {
                    "mvc": {
                      "files": [
                        {
                          "type": "file",
                          "path": "swagger.config.ts",
                          "content": "import swaggerAutoGen from \"swagger-autogen\";\n\nconst doc = {\n  info: {\n    title: \"Hybrid Auth API\",\n    description: \"Hybrid Auth API\",\n    version: \"1.0.0\"\n  },\n  host: \"localhost:9000/api\",\n  schemes: [\"http\"]\n};\n\nconst outputFile = \"./src/docs/swagger.json\";\nconst endpointsFiles = [\"./src/routes/*.ts\"];\n\nswaggerAutoGen(outputFile, endpointsFiles, doc);\n"
                        },
                        {
                          "type": "file",
                          "path": "README.md",
                          "content": "# Hybrid Auth PostgreSQL MVC\n\nMinimal Node.js + Express + TypeScript MVC starter using PostgreSQL, Drizzle ORM, and hybrid authentication (local and OAuth via Passport or similar).\n\n## Features\n\n- Express + TypeScript MVC structure\n- PostgreSQL + Drizzle integration\n- Hybrid auth: local credentials and OAuth providers (e.g., Google, GitHub, Facebook)\n- Session-based authentication compatible with production\n- Environment-driven configuration with `.env`\n- Dev and production scripts\n\n## What This Provides\n\n- A clean starting point for credential and OAuth login\n- Prewired Express app with routing and session middleware\n- PostgreSQL connection wiring ready for your schema and data models\n- TypeScript configuration and scripts for iterative dev and production builds\n- Example environment keys you can enable as needed\n\n## Quick Start\n\n1. Install dependencies:\n   - `npm install`\n2. Configure environment:\n   - Create `.env` (copy from `.env.example` if present).\n   - Set variables shown below.\n3. Run in development:\n   - `npm run dev`\n4. Build and run in production:\n   - `npm run build`\n   - `npm start`\n\n## Requirements\n\n- Node.js 18+\n- PostgreSQL\n\n## Environment Variables\n\n- `DATABASE_URL` — PostgreSQL connection string\n- `REDIS_URL` — Redis connection string for session storage\n- `PORT` — server port (e.g., 3000)\n- `NODE_ENV` — `development` or `production`\n- `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`, `CRYPTO_SECRET`\n- `EMAIL_FROM`, `RESEND_API_KEY`\n- Optional OAuth (enable what you use):\n  - `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI`\n  - `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GITHUB_REDIRECT_URI`\n  - `FACEBOOK_APP_ID`, `FACEBOOK_APP_SECRET`, `FACEBOOK_REDIRECT_URI`\n\n## Scripts\n\n- `npm run dev` — start development server\n- `npm run build` — compile TypeScript\n- `npm start` — start compiled app\n\n## Notes\n\n- Never commit `.env` or secrets.\n- Run your Drizzle migrations before starting the app.\n"
                        },
                        {
                          "type": "file",
                          "path": "package.json",
                          "content": "{\n  \"name\": \"servercn-hybrid-auth\",\n  \"version\": \"1.0.0\",\n  \"main\": \"dist/server.js\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development npx tsx watch src/server.ts\",\n    \"build\": \"rm -rf dist && tsc && tsc-alias\",\n    \"start\": \"cross-env NODE_ENV=production node dist/server.js\",\n    \"db:generate\": \"drizzle-kit generate\",\n    \"db:migrate\": \"drizzle-kit migrate\",\n    \"db:push\": \"drizzle-kit push\",\n    \"db:studio\": \"drizzle-kit studio\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"docs\": \"npx tsx swagger.config.ts\",\n    \"prepare\": \"husky\",\n    \"lint:check\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"format:check\": \"npx prettier . --check\",\n    \"format:fix\": \"npx prettier . --write\"\n  },\n  \"lint-staged\": {\n    \"src/**/*.ts\": [\n      \"eslint --fix\",\n      \"prettier --write\",\n      \"tsc --noEmit\"\n    ]\n  },\n  \"dependencies\": {},\n  \"devDependencies\": {}\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "drizzle.config.ts",
                          "content": "import { Config, defineConfig } from \"drizzle-kit\";\nimport env from \"./src/configs/env\";\n\nexport default defineConfig({\n  out: \"./src/drizzle/migrations\",\n  schema: \"./src/drizzle/schemas/*\",\n  dialect: \"postgresql\",\n  dbCredentials: {\n    url: env.DATABASE_URL!\n  },\n  verbose: true,\n  strict: true\n}) satisfies Config;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/server.ts",
                          "content": "import app from \"./app\";\nimport env from \"./configs/env\";\nimport redisClient from \"./configs/redis\";\nimport { logger } from \"./utils/logger\";\nimport { configureGracefulShutdown } from \"./utils/shutdown\";\n\nconst port = env.PORT || 9000;\n\nredisClient\n  .connect()\n  .then(() => {\n    logger.info(\"Redis Connection Success\");\n    const server = app.listen(port, () => {\n      logger.info(`[server]: Server is running at http://localhost:${port}`);\n      logger.info(`[server]: Environment: ${env.NODE_ENV}`);\n      logger.info(\n        `[server]: Swagger docs are available at http://localhost:${port}/api/docs`\n      );\n    });\n\n    configureGracefulShutdown(server);\n  })\n  .catch((error: Error) => {\n    logger.error(error, \"Redis Connection Failed\");\n    process.exit(1);\n  });\n"
                        },
                        {
                          "type": "file",
                          "path": "src/app.ts",
                          "content": "import express, { Express, Request, Response } from \"express\";\nimport cookieParser from \"cookie-parser\";\nimport morgan from \"morgan\";\nimport { notFoundHandler } from \"./middlewares/not-found-handler\";\nimport { errorHandler } from \"./middlewares/error-handler\";\nimport env from \"./configs/env\";\nimport { configureSecurityHeaders } from \"./middlewares/security-header\";\n\nimport Routes from \"./routes/index\";\n\nimport \"./configs/passport\";\nimport sourceMapSupport from \"source-map-support\";\nimport { setupSwagger } from \"./configs/swagger\";\nsourceMapSupport.install();\n\nconst app: Express = express();\n\n//? Apply security headers before other middlewares and routes\nconfigureSecurityHeaders(app);\n\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\napp.use(cookieParser());\napp.use(morgan(env.NODE_ENV === \"development\" ? \"dev\" : \"combined\"));\n\n//? Swagger Setup\nsetupSwagger(app);\n\n//? Routes\napp.get(\"/\", (req: Request, res: Response) => {\n  res.redirect(\"/api/v1/health\");\n});\n\napp.use(\"/api\", Routes);\n\n//? Not-found-handler (should be after routes)\napp.use(notFoundHandler);\n\n//? Global error handler (should be last)\napp.use(errorHandler);\n\nexport default app;\n"
                        },
                        {
                          "type": "file",
                          "path": ".husky/pre-commit",
                          "content": "npx lint-staged\n"
                        },
                        {
                          "type": "file",
                          "path": "src/validators/auth.ts",
                          "content": "import * as z from \"zod\";\nimport { OTP_TYPES } from \"../constants/auth\";\n\nexport const nameSchema = z\n  .string({ error: \"Name must be a string\" })\n  .trim()\n  .min(3, {\n    message: \"Name must be at least 3 characters long\"\n  })\n  .max(50, {\n    message: \"Name must be at most 50 characters long\"\n  });\n\nexport const passwordSchema = z\n  .string({ error: \"Password must be a string\" })\n  .min(6, {\n    message: \"Password must be at least 6 characters long\"\n  })\n  .max(80, {\n    message: \"Password must be at most 80 characters long\"\n  });\n\nexport const emailSchema = z\n  .email({ message: \"Please enter a valid email address.\" })\n  .max(100, { message: \"Email must be no more than 100 characters.\" });\n\nexport const roleSchema = z\n  .enum([\"user\", \"admin\"], {\n    error: \"Role must be either applicant, recruiter, or admin\"\n  })\n  .default(\"user\");\n\nexport const SigninSchema = z.object({\n  email: emailSchema,\n  password: z.string({ error: \"Password must be a string\" }).min(1, {\n    message: \"Password is required\"\n  })\n});\n\nexport const SignupSchema = z\n  .object({\n    name: nameSchema,\n    email: emailSchema,\n    password: passwordSchema,\n    confirmPassword: passwordSchema,\n    role: roleSchema\n  })\n  .refine(\n    data => {\n      return data.password === data.confirmPassword;\n    },\n    {\n      message: \"Passwords do not match\",\n      path: [\"confirmPassword\"]\n    }\n  );\n\nexport const RequestOtpSchema = z.object({\n  email: emailSchema,\n  otpType: z.enum(OTP_TYPES, { error: \"Invalid otp type\" })\n});\n\nexport const VerifyOtpSchema = z.object({\n  otpCode: z.string().min(6, \"Please enter a valid OTP\"),\n  email: emailSchema\n});\n\nexport const ResetPasswordSchema = z.object({\n  email: emailSchema,\n  newPassword: passwordSchema\n});\n\nexport const ChangePasswordSchema = z.object({\n  oldPassword: z.string({ error: \"Password must be a string\" }).min(1, {\n    message: \"Old password is required\"\n  }),\n  newPassword: passwordSchema\n});\n\nexport const UpdateProfileSchema = z.object({\n  name: nameSchema.optional(),\n  avatar: z.string().optional()\n});\n\nexport const GoogleSigninSchema = z.object({\n  name: nameSchema,\n  email: emailSchema,\n  provider: z.enum([\"google\", \"github\"]).default(\"google\"),\n  providerId: z.string({ error: \"Provider id must be a string\" }).min(1, {\n    message: \"Provider id is required\"\n  }),\n  avatar: z.string().optional(),\n  isEmailVerified: z.boolean().default(false)\n});\n\nexport const DeleteAccountSchema = z.object({\n  userId: z.string({ error: \"User id must be a string\" }).min(1, {\n    message: \"User id is required\"\n  }),\n  type: z\n    .enum([\"soft\", \"hard\"], { error: \"Type must be either soft or hard\" })\n    .default(\"soft\")\n});\n\nexport type SignupUserType = z.infer<typeof SignupSchema>;\nexport type SigninUserType = z.infer<typeof SigninSchema>;\nexport type RequestOtpType = z.infer<typeof RequestOtpSchema>;\nexport type VerifyOtpType = z.infer<typeof VerifyOtpSchema>;\nexport type ResetPasswordType = z.infer<typeof ResetPasswordSchema>;\nexport type ChangePasswordType = z.infer<typeof ChangePasswordSchema>;\nexport type UpdateProfileType = z.infer<typeof UpdateProfileSchema>;\nexport type GoogleSigninType = z.infer<typeof GoogleSigninSchema>;\nexport type DeleteAccountType = z.infer<typeof DeleteAccountSchema>;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/validators/auth.test.ts",
                          "content": "import assert from \"node:assert/strict\";\nimport { readFile } from \"node:fs/promises\";\nimport test from \"node:test\";\n\ntest(\"mvc auth validators do not trim password inputs\", async () => {\n  const source = await readFile(\n    new URL(\"./auth.ts\", import.meta.url),\n    \"utf8\"\n  );\n\n  assert.match(\n    source,\n    /export const passwordSchema = z\\s*\\.string\\(\\{ error: \"Password must be a string\" \\}\\)\\s*\\.min\\(6,/\n  );\n  assert.doesNotMatch(\n    source,\n    /export const passwordSchema\\s*=\\s*z[\\s\\S]*?\\.trim\\(\\)[\\s\\S]*?;\\s*(?=export const|$)/\n  );\n  assert.doesNotMatch(\n    source,\n    /password\\s*:\\s*z\\s*\\.string\\(\\s*\\{\\s*error:\\s*\"Password must be a string\"\\s*\\}\\s*\\)\\s*\\.trim\\(\\)\\s*\\.min\\(\\s*1\\s*,/\n  );\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/shutdown.ts",
                          "content": "import { Server } from \"http\";\nimport { logger } from \"./logger\";\n\nexport const configureGracefulShutdown = (server: Server) => {\n  const signals = [\"SIGTERM\", \"SIGINT\"];\n\n  signals.forEach(signal => {\n    process.on(signal, () => {\n      logger.info(`\\n${signal} signal received. Shutting down gracefully...`);\n\n      server.close(err => {\n        if (err) {\n          logger.error(err, \"Error during server close\");\n          process.exit(1);\n        }\n\n        logger.info(\"HTTP server closed.\");\n        process.exit(0);\n      });\n\n      // Force shutdown after 10 seconds\n      setTimeout(() => {\n        logger.error(\n          \"Could not close connections in time, forcefully shutting down\"\n        );\n        process.exit(1);\n      }, 10000);\n    });\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/send-mail.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport env from \"../configs/env\";\nimport { resend } from \"../configs/resend\";\nimport { renderEmailTemplates } from \"./render-email-template\";\n\nexport type SendMailType = {\n  from?: string;\n  subject: string;\n  data: Record<string, any>;\n  email: string;\n  html?: string;\n  templateName: string;\n};\n\nexport async function sendEmail({\n  from,\n  email,\n  subject,\n  data,\n  html,\n  templateName\n}: SendMailType) {\n  const htmlContent =\n    (await renderEmailTemplates(templateName, data)) || html || \"\";\n\n  return await resend.emails.send({\n    from: from || env.EMAIL_FROM,\n    to: email,\n    subject,\n    replyTo: email,\n    html: htmlContent\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/render-email-template.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport ejs from \"ejs\"; // npm i --save-dev @types/ejs\nimport path from \"node:path\";\n\nexport async function renderEmailTemplates(\n  templateName: string,\n  data: Record<string, any>\n) {\n  const templatePath = path.join(\n    process.cwd(),\n    \"src\",\n    \"email-templates\",\n    `${templateName}.ejs`\n  );\n  return ejs.renderFile(templatePath, data);\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/logger.ts",
                          "content": "import pino from \"pino\";\nimport env from \"../configs/env\";\n\nexport const logger = pino({\n  level: env.LOG_LEVEL,\n  transport:\n    env.NODE_ENV !== \"production\"\n      ? {\n          target: \"pino-pretty\",\n          options: {\n            colorize: true,\n            translateTime: \"yyyy-mm-dd HH:MM:ss\",\n            ignore: \"pid,hostname\"\n          }\n        }\n      : undefined\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/jwt.ts",
                          "content": "import jwt from \"jsonwebtoken\";\nimport env from \"../configs/env\";\n\nconst JWT_ACCESS_TOKEN_EXPIRY = \"15m\";\nconst JWT_REFRESH_TOKEN_EXPIRY = \"7d\";\n\nexport function generateAccessToken(user: {\n  _id: string;\n  role: \"user\" | \"admin\";\n  sessionId: string;\n}) {\n  return jwt.sign(\n    { _id: user._id, role: user.role, sessionId: user.sessionId },\n    env.JWT_ACCESS_SECRET!,\n    {\n      expiresIn: JWT_ACCESS_TOKEN_EXPIRY\n    }\n  );\n}\n\nexport function generateRefreshToken(user: { _id: string; sessionId: string }) {\n  return jwt.sign(\n    { _id: user._id, sessionId: user.sessionId },\n    env.JWT_REFRESH_SECRET!,\n    {\n      expiresIn: JWT_REFRESH_TOKEN_EXPIRY\n    }\n  );\n}\n\nexport function verifyAccessToken(token: string) {\n  return jwt.verify(token, env.JWT_ACCESS_SECRET!) as {\n    _id: string;\n    role: \"user\" | \"admin\";\n    sessionId: string;\n  };\n}\n\nexport function verifyRefreshToken(token: string) {\n  return jwt.verify(token, env.JWT_REFRESH_SECRET!) as {\n    _id: string;\n    sessionId: string;\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/date.ts",
                          "content": "export function getRemainingTime(date: Date) {\n  const now = new Date();\n  let diff = date.getTime() - now.getTime();\n\n  if (diff <= 0) {\n    return {\n      days: 0,\n      minutes: 0,\n      seconds: 0\n    };\n  }\n\n  const seconds = Math.floor((diff / 1000) % 60);\n  const minutes = Math.floor((diff / (1000 * 60)) % 60);\n  const days = Math.floor(diff / (1000 * 60 * 60 * 24));\n\n  return {\n    days,\n    minutes,\n    seconds\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/async-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\n\nexport type AsyncRouteHandler = (\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => Promise<unknown>;\n\nexport function AsyncHandler(fn: AsyncRouteHandler) {\n  return function (req: Request, res: Response, next: NextFunction) {\n    Promise.resolve()\n      .then(() => fn(req, res, next))\n      .catch(next);\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/api-response.ts",
                          "content": "import { STATUS_CODES, StatusCode } from \"../constants/status-codes\";\nimport type { Response } from \"express\";\n\ntype ApiResponseParams<T> = {\n  success: boolean;\n  message: string;\n  statusCode: StatusCode;\n  data?: T | null;\n  errors?: unknown;\n};\n\nexport class ApiResponse<T = unknown> {\n  public readonly success: boolean;\n  public readonly message: string;\n  public readonly statusCode: StatusCode;\n  public readonly data?: T | null;\n  public readonly errors?: unknown;\n\n  constructor({\n    success,\n    message,\n    statusCode,\n    data,\n    errors\n  }: ApiResponseParams<T>) {\n    this.success = success;\n    this.message = message;\n    this.statusCode = statusCode;\n    this.data = data;\n    this.errors = errors;\n  }\n\n  send(res: Response): Response {\n    return res.status(this.statusCode).json({\n      success: this.success,\n      message: this.message,\n      statusCode: this.statusCode,\n      ...(this.data !== undefined && { data: this.data }),\n      ...(this.errors !== undefined && { errors: this.errors })\n    });\n  }\n\n  static Success<T>(\n    res: Response,\n    message: string,\n    data?: T,\n    statusCode: StatusCode = STATUS_CODES.OK\n  ): Response {\n    return new ApiResponse<T>({\n      success: true,\n      message,\n      data,\n      statusCode\n    }).send(res);\n  }\n\n  static ok<T>(res: Response, message = \"OK\", data?: T) {\n    return ApiResponse.Success(res, message, data, STATUS_CODES.OK);\n  }\n\n  static created<T>(res: Response, message = \"Created\", data?: T) {\n    return ApiResponse.Success(res, message, data, STATUS_CODES.CREATED);\n  }\n}\n\n/*\n * Usage:\n * ApiResponse.ok(res, \"OK\", data);\n * ApiResponse.created(res, \"Created\", data);\n */\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/api-error.ts",
                          "content": "import { STATUS_CODES, StatusCode } from \"../constants/status-codes\";\n\nexport class ApiError extends Error {\n  public readonly statusCode: StatusCode;\n  public readonly isOperational: boolean;\n  public readonly errors?: unknown;\n\n  constructor(\n    statusCode: StatusCode,\n    message: string,\n    errors?: unknown,\n    isOperational = true\n  ) {\n    super(message);\n    this.name = \"ApiError\";\n    this.statusCode = statusCode;\n    this.errors = errors;\n    this.isOperational = isOperational;\n\n    Error.captureStackTrace(this, this.constructor);\n  }\n\n  static badRequest(message = \"Bad Request\", errors?: unknown) {\n    return new ApiError(STATUS_CODES.BAD_REQUEST, message, errors);\n  }\n\n  static unauthorized(message = \"Unauthorized\") {\n    return new ApiError(STATUS_CODES.UNAUTHORIZED, message);\n  }\n\n  static forbidden(message = \"Forbidden\") {\n    return new ApiError(STATUS_CODES.FORBIDDEN, message);\n  }\n\n  static notFound(message = \"Not Found\") {\n    return new ApiError(STATUS_CODES.NOT_FOUND, message);\n  }\n\n  static conflict(message = \"Conflict\") {\n    return new ApiError(STATUS_CODES.CONFLICT, message);\n  }\n\n  static server(message = \"Internal Server Error\") {\n    return new ApiError(STATUS_CODES.INTERNAL_SERVER_ERROR, message);\n  }\n\n  static unprocessableEntity(message = \"Unprocessable Entity\") {\n    return new ApiError(STATUS_CODES.UNPROCESSABLE_ENTITY, message);\n  }\n\n  static tooManyRequests(message = \"Too Many Requests\") {\n    return new ApiError(STATUS_CODES.TOO_MANY_REQUESTS, message);\n  }\n}\n\n/*\n  ? Usage:\n  * throw new ApiError(404, \"Not found\");\n  * throw ApiError.badRequest(\"Bad request\");\n */\n"
                        },
                        {
                          "type": "file",
                          "path": "src/types/user.d.ts",
                          "content": "import { Request } from \"express\";\nimport { OTP_TYPES } from \"../constants/auth\";\n\nexport type OTPType = (typeof OTP_TYPES)[number];\n\nexport interface AvatarData {\n  public_id: string;\n  url: string;\n  size: number;\n}\n\nexport interface UserRequest extends Request {\n  user?: {\n    _id?: string | undefined;\n    id?: string | undefined;\n    role?: \"user\" | \"admin\" | undefined;\n    sessionId?: string | undefined;\n  };\n}\n\nexport interface IUser {\n  id: string;\n  name: string;\n  email: string;\n  password?: string;\n  role: \"user\" | \"admin\";\n  isEmailVerified: boolean;\n  lastLoginAt?: Date;\n  failedLoginAttempts: number;\n  lockUntil?: Date;\n  avatar?: AvatarData | string | null;\n  provider: \"local\" | \"google\" | \"github\";\n  providerId?: string;\n  isDeleted: boolean;\n  deletedAt?: Date;\n  reActivateAvailableAt?: Date;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nexport type RefreshTokenData = {\n  userId: string;\n  tokenHash: string;\n  expiresAt: Date;\n};\n\nexport type SessionData = {\n  userId: string;\n  sessionId: string;\n  refreshTokenHash: string;\n  userAgent: string;\n  ip: string;\n  createdAt: Date;\n  expiresAt: Date;\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/otp.service.ts",
                          "content": "import { logger } from \"../utils/logger\";\nimport redis from \"../configs/redis\";\nimport {\n  OTP_CODE_LENGTH,\n  OTP_EXPIRES_IN,\n  OTP_MAX_ATTEMPTS,\n  OTP_SPAM_LOCK_TIME,\n  OTP_COOL_DOWN\n} from \"../constants/auth\";\nimport { generateOTP } from \"../helpers/token.helpers\";\nimport { ApiError } from \"../utils/api-error\";\nimport { sendEmail } from \"../utils/send-mail\";\n\ntype SendOtpBase = {\n  name: string;\n  email: string;\n  templateName: string;\n  subject: string;\n};\n\ntype SendOtpWithCode = SendOtpBase & {\n  code: string;\n  hashCode: string;\n};\n\ntype SendOtpWithoutCode = SendOtpBase & {\n  code?: never;\n  hashCode?: never;\n};\n\nexport type SendOtpType = SendOtpWithCode | SendOtpWithoutCode;\n\nexport class OtpService {\n  static async checkOtpRestrictions(email: string) {\n    const otpLock = await redis.get(`otp_lock:${email}`);\n    if (otpLock) {\n      throw ApiError.badRequest(\n        \"Your Account is locked due to multiple failed attempts. Please try again after 30 minutes.\"\n      );\n    }\n\n    if (await redis.get(`otp_spam_lock:${email}`)) {\n      throw ApiError.tooManyRequests(\n        \"Too many otp requests. Please try again after 1 hour before requesting again.\"\n      );\n    }\n\n    if (await redis.get(`otp_cooldown:${email}`)) {\n      throw ApiError.tooManyRequests(\n        \"Too many otp requests. Please try again after 1 minute before requesting new otp.\"\n      );\n    }\n  }\n\n  static async trackOtpRequests(email: string) {\n    try {\n      const otpRequestKey = `otp_request_count:${email}`;\n      let otpRequestsCount = parseInt((await redis.get(otpRequestKey)) || \"0\");\n      if (otpRequestsCount >= OTP_MAX_ATTEMPTS) {\n        await redis.set(`otp_spam_lock:${email}`, \"locked\", {\n          expiration: {\n            type: \"EX\",\n            value: 3600\n          }\n        });\n        throw ApiError.tooManyRequests(\n          \"Too many otp requests. Please try again after 1 hour before requesting again.\"\n        );\n      }\n\n      await redis.set(otpRequestKey, otpRequestsCount + 1, {\n        expiration: {\n          type: \"EX\",\n          value: 3600\n        }\n      });\n    } catch (error) {\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to track otp requests!\");\n    }\n  }\n\n  static async sendOtp({\n    name,\n    email,\n    templateName,\n    code,\n    hashCode,\n    subject\n  }: SendOtpType) {\n    try {\n      const newOtp = generateOTP(OTP_CODE_LENGTH);\n      const otpKey = `otp:${email}`;\n      const otpCooldownKey = `otp_cooldown:${email}`;\n      const otpHash = hashCode ? hashCode : newOtp.hashCode;\n\n      logger.info({ email }, \"OTP generated successfully\");\n\n      await redis.set(otpKey, otpHash, {\n        expiration: {\n          type: \"EX\",\n          value: OTP_EXPIRES_IN / 1000\n        }\n      });\n\n      await redis.set(otpCooldownKey, OTP_COOL_DOWN, {\n        expiration: {\n          type: \"EX\",\n          value: OTP_COOL_DOWN\n        }\n      });\n\n      try {\n        await sendEmail({\n          email,\n          subject,\n          data: {\n            code: code ? code : newOtp.code,\n            name\n          },\n          templateName\n        });\n      } catch (error) {\n        await Promise.allSettled([redis.del(otpKey), redis.del(otpCooldownKey)]);\n        throw error;\n      }\n    } catch (error) {\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to send otp!\");\n    }\n  }\n\n  static async verifyOtp(hashCode: string, email: string) {\n    const hashOtpCodeKey = await redis.get(`otp:${email}`);\n\n    if (!hashOtpCodeKey) {\n      throw ApiError.badRequest(\"Invalid or expired otp\");\n    }\n\n    const failedAttemptsKey = `otp_attempts:${email}`;\n    if (hashOtpCodeKey !== hashCode) {\n      const failedAttempts = await redis.incr(failedAttemptsKey);\n\n      if (failedAttempts === 1) {\n        await redis.expire(\n          failedAttemptsKey,\n          Math.floor(OTP_EXPIRES_IN / 1000)\n        );\n      }\n\n      if (failedAttempts >= OTP_MAX_ATTEMPTS) {\n        await redis.set(`otp_lock:${email}`, \"locked\", {\n          EX: OTP_SPAM_LOCK_TIME\n        });\n        throw ApiError.tooManyRequests(\n          \"Too many failed attempts. Please try again after 1 hour.\"\n        );\n      }\n      throw ApiError.badRequest(\n        `Incorrect OTP. ${OTP_MAX_ATTEMPTS - failedAttempts} attempts left.`\n      );\n    }\n\n    await redis.del([`otp:${email}`, failedAttemptsKey]);\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/otp.service.test.ts",
                          "content": "import assert from \"node:assert/strict\";\nimport { readFile } from \"node:fs/promises\";\nimport test from \"node:test\";\n\ntest(\"mvc otp service does not log raw OTP values\", async () => {\n  const source = await readFile(\n    new URL(\"./otp.service.ts\", import.meta.url),\n    \"utf8\"\n  );\n\n  assert.match(source, /logger\\.info\\(\\{ email \\}, \"OTP generated successfully\"\\)/);\n  assert.doesNotMatch(source, /OTP generated successfully: \\$\\{/);\n  assert.doesNotMatch(source, /logger\\.(info|warn|error|debug|trace)\\([^)]*(newOtp\\.code|code \\? code)/);\n});\n\ntest(\"mvc otp verification increments failures before lockout evaluation\", async () => {\n  const source = await readFile(\n    new URL(\"./otp.service.ts\", import.meta.url),\n    \"utf8\"\n  );\n\n  assert.match(source, /const failedAttempts = await redis\\.incr\\(failedAttemptsKey\\);/);\n  assert.match(\n    source,\n    /if \\(failedAttempts === 1\\) {\\s*await redis\\.expire\\(\\s*failedAttemptsKey,\\s*Math\\.floor\\(OTP_EXPIRES_IN \\/ 1000\\)\\s*\\);/\n  );\n  assert.match(source, /if \\(failedAttempts >= OTP_MAX_ATTEMPTS\\) {/);\n  assert.match(\n    source,\n    /Incorrect OTP\\. \\$\\{OTP_MAX_ATTEMPTS - failedAttempts\\} attempts left\\./\n  );\n});\n\ntest(\"mvc otp service persists otp state before email delivery and cleans up on send failure\", async () => {\n  const source = await readFile(\n    new URL(\"./otp.service.ts\", import.meta.url),\n    \"utf8\"\n  );\n\n  assert.match(source, /await redis\\.set\\(otpKey, otpHash,/);\n  assert.match(source, /await redis\\.set\\(otpCooldownKey, OTP_COOL_DOWN,/);\n  assert.match(source, /await sendEmail\\(\\{/);\n  assert.match(\n    source,\n    /await Promise\\.allSettled\\(\\[redis\\.del\\(otpKey\\), redis\\.del\\(otpCooldownKey\\)\\]\\);/\n  );\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/oauth.service.ts",
                          "content": "import { AuthService, CookieOptionsType } from \"./auth.service\";\nimport db from \"../configs/db\";\nimport { users } from \"../drizzle/schemas/user.schema\";\nimport { eq } from \"drizzle-orm\";\nimport { ApiError } from \"../utils/api-error\";\n\ntype OAuthProfile = {\n  provider: \"local\" | \"google\" | \"github\";\n  providerId: string;\n  name: string;\n  email: string | undefined;\n  isEmailVerified: boolean;\n  avatar: string | undefined;\n  ip: string;\n  userAgent: string;\n};\n\nexport class OAuthService {\n  static async handleOAuthLogin(\n    user: OAuthProfile,\n    context: CookieOptionsType\n  ) {\n    if (!user.email) {\n      throw new Error(\"Email is required for OAuth login\");\n    }\n\n    const existingUser = await db.query.users.findFirst({\n      where: eq(users.email, user.email)\n    });\n\n    if (existingUser) {\n      const canAutoLinkProvider =\n        user.isEmailVerified || existingUser.provider === user.provider;\n\n      if (!canAutoLinkProvider) {\n        throw ApiError.forbidden(\n          \"Please sign in with your existing provider to link this account.\"\n        );\n      }\n\n      const [updatedUser] = await db.update(users).set({\n        provider: user.provider,\n        providerId: user.providerId,\n        isEmailVerified: existingUser.isEmailVerified || user.isEmailVerified,\n        avatar: user.avatar ? { url: user.avatar } : null\n      }).where(eq(users.id, existingUser.id)).returning();\n\n      await AuthService.handleToken(\n        {\n          _id: updatedUser.id,\n          role: updatedUser.role,\n          ip: user.ip,\n          userAgent: user.userAgent\n        },\n        context\n      );\n      return updatedUser;\n    }\n\n    const [newUser] = await db.insert(users).values({\n      name: user.name,\n      email: user.email,\n      isEmailVerified: user.isEmailVerified,\n      provider: user.provider,\n      providerId: user.providerId,\n      avatar: user.avatar ? { url: user.avatar } : null\n    }).returning();\n\n    await AuthService.handleToken(\n      {\n        _id: newUser.id,\n        role: newUser.role,\n        ip: user.ip,\n        userAgent: user.userAgent\n      },\n      context\n    );\n\n    return newUser;\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/oauth.service.test.ts",
                          "content": "import assert from \"node:assert/strict\";\nimport test from \"node:test\";\n\nconst applyTestEnv = () => {\n  process.env.NODE_ENV = \"test\";\n  process.env.PORT = \"3000\";\n  process.env.DATABASE_URL = \"postgres://user:pass@localhost:5432/testdb\";\n  process.env.CORS_ORIGIN = \"http://localhost:3000\";\n  process.env.CLIENT_URL = \"https://example.com\";\n  process.env.LOG_LEVEL = \"info\";\n  process.env.JWT_ACCESS_SECRET = \"a\".repeat(32);\n  process.env.JWT_REFRESH_SECRET = \"b\".repeat(32);\n  process.env.CRYPTO_SECRET = \"c\".repeat(32);\n  process.env.RESEND_API_KEY = \"test-key\";\n  process.env.EMAIL_FROM = \"noreply@example.com\";\n  process.env.CLOUDINARY_CLOUD_NAME = \"cloud\";\n  process.env.CLOUDINARY_API_KEY = \"key\";\n  process.env.CLOUDINARY_API_SECRET = \"secret\";\n  process.env.GOOGLE_CLIENT_ID = \"google-client\";\n  process.env.GOOGLE_CLIENT_SECRET = \"google-secret\";\n  process.env.GOOGLE_REDIRECT_URI = \"https://example.com/google/callback\";\n  process.env.GITHUB_CLIENT_ID = \"github-client\";\n  process.env.GITHUB_CLIENT_SECRET = \"github-secret\";\n  process.env.GITHUB_REDIRECT_URI = \"https://example.com/github/callback\";\n  process.env.FACEBOOK_APP_ID = \"facebook-app\";\n  process.env.FACEBOOK_APP_SECRET = \"facebook-secret\";\n  process.env.FACEBOOK_REDIRECT_URI = \"https://example.com/facebook/callback\";\n  process.env.REDIS_URL = \"redis://localhost:6379\";\n};\n\nconst createReturningChain = <T>(result: T) => ({\n  where: () => ({\n    returning: async () => [result]\n  })\n});\n\ntest(\"oauth service auto-links verified users\", async t => {\n  applyTestEnv();\n\n  const [{ OAuthService }, { AuthService }, { default: db }] = await Promise.all([\n    import(\"./oauth.service.ts\"),\n    import(\"./auth.service.ts\"),\n    import(\"../configs/db.ts\")\n  ]);\n\n  const existingUser = {\n    id: \"user-1\",\n    role: \"user\",\n    provider: \"local\",\n    isEmailVerified: false\n  };\n  const updatedUser = {\n    ...existingUser,\n    provider: \"google\",\n    providerId: \"google-123\",\n    avatar: { url: \"https://example.com/avatar.png\" },\n    isEmailVerified: true\n  };\n\n  t.mock.method(db.query.users, \"findFirst\", async () => existingUser as never);\n  const updateMock = t.mock.method(db, \"update\", () => ({\n    set: () => createReturningChain(updatedUser)\n  }) as never);\n  const insertMock = t.mock.method(db, \"insert\", () => {\n    throw new Error(\"insert should not be called\");\n  });\n  const handleTokenMock = t.mock.method(\n    AuthService,\n    \"handleToken\",\n    async () => undefined\n  );\n\n  const result = await OAuthService.handleOAuthLogin(\n    {\n      provider: \"google\",\n      providerId: \"google-123\",\n      name: \"Verified User\",\n      email: \"verified@example.com\",\n      isEmailVerified: true,\n      avatar: \"https://example.com/avatar.png\",\n      ip: \"127.0.0.1\",\n      userAgent: \"test-agent\"\n    },\n    {}\n  );\n\n  assert.equal(updateMock.mock.callCount(), 1);\n  assert.equal(insertMock.mock.callCount(), 0);\n  assert.equal(handleTokenMock.mock.callCount(), 1);\n  assert.equal(result.provider, \"google\");\n});\n\ntest(\"oauth service allows same-provider linking even when the incoming email is unverified\", async t => {\n  applyTestEnv();\n\n  const [{ OAuthService }, { AuthService }, { default: db }] = await Promise.all([\n    import(\"./oauth.service.ts\"),\n    import(\"./auth.service.ts\"),\n    import(\"../configs/db.ts\")\n  ]);\n\n  const existingUser = {\n    id: \"user-2\",\n    role: \"user\",\n    provider: \"github\",\n    isEmailVerified: false\n  };\n  const updatedUser = {\n    ...existingUser,\n    providerId: \"github-123\",\n    avatar: { url: \"https://example.com/avatar.png\" }\n  };\n\n  t.mock.method(db.query.users, \"findFirst\", async () => existingUser as never);\n  const updateMock = t.mock.method(db, \"update\", () => ({\n    set: () => createReturningChain(updatedUser)\n  }) as never);\n  const insertMock = t.mock.method(db, \"insert\", () => {\n    throw new Error(\"insert should not be called\");\n  });\n  const handleTokenMock = t.mock.method(\n    AuthService,\n    \"handleToken\",\n    async () => undefined\n  );\n\n  const result = await OAuthService.handleOAuthLogin(\n    {\n      provider: \"github\",\n      providerId: \"github-123\",\n      name: \"Same Provider\",\n      email: \"same-provider@example.com\",\n      isEmailVerified: false,\n      avatar: \"https://example.com/avatar.png\",\n      ip: \"127.0.0.1\",\n      userAgent: \"test-agent\"\n    },\n    {}\n  );\n\n  assert.equal(updateMock.mock.callCount(), 1);\n  assert.equal(insertMock.mock.callCount(), 0);\n  assert.equal(handleTokenMock.mock.callCount(), 1);\n  assert.equal(result.provider, \"github\");\n});\n\ntest(\"oauth service blocks auto-linking for unverified users on a different provider\", async t => {\n  applyTestEnv();\n\n  const [{ OAuthService }, { AuthService }, { default: db }, { ApiError }] =\n    await Promise.all([\n      import(\"./oauth.service.ts\"),\n      import(\"./auth.service.ts\"),\n      import(\"../configs/db.ts\"),\n      import(\"../utils/api-error.ts\")\n    ]);\n\n  const existingUser = {\n    id: \"user-3\",\n    role: \"user\",\n    provider: \"google\",\n    isEmailVerified: false\n  };\n\n  t.mock.method(db.query.users, \"findFirst\", async () => existingUser as never);\n  const updateMock = t.mock.method(db, \"update\", () => {\n    throw new Error(\"update should not be called\");\n  });\n  const insertMock = t.mock.method(db, \"insert\", () => {\n    throw new Error(\"insert should not be called\");\n  });\n  const handleTokenMock = t.mock.method(\n    AuthService,\n    \"handleToken\",\n    async () => undefined\n  );\n\n  await assert.rejects(\n    () =>\n      OAuthService.handleOAuthLogin(\n        {\n          provider: \"github\",\n          providerId: \"github-123\",\n          name: \"Blocked User\",\n          email: \"blocked@example.com\",\n          isEmailVerified: false,\n          avatar: undefined,\n          ip: \"127.0.0.1\",\n          userAgent: \"test-agent\"\n        },\n        {}\n      ),\n    (error: unknown) =>\n      error instanceof ApiError &&\n      error.message ===\n        \"Please sign in with your existing provider to link this account.\"\n  );\n\n  assert.equal(updateMock.mock.callCount(), 0);\n  assert.equal(insertMock.mock.callCount(), 0);\n  assert.equal(handleTokenMock.mock.callCount(), 0);\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/cloudinary.service.ts",
                          "content": "import { DeleteApiResponse } from \"cloudinary\";\nimport cloudinary from \"../configs/cloudinary\";\n\nexport interface UploadOptions {\n  folder: string;\n  resource_type?: \"image\" | \"video\" | \"raw\" | \"auto\";\n}\n\nexport interface CloudinaryUploadResult {\n  url: string;\n  public_id: string;\n  size: number;\n}\n\nexport const uploadToCloudinary = (\n  buffer: Buffer,\n  options: UploadOptions\n): Promise<CloudinaryUploadResult> => {\n  return new Promise((resolve, reject) => {\n    const stream = cloudinary.uploader.upload_stream(\n      {\n        folder: options.folder || \"uploads\",\n        resource_type: options.resource_type || \"auto\"\n      },\n      (error, result) => {\n        if (error || !result) {\n          return reject(error);\n        }\n        resolve({\n          url: result.secure_url,\n          public_id: result.public_id,\n          size: result.bytes\n        });\n      }\n    );\n\n    stream.end(buffer);\n  });\n};\n\nexport const deleteFileFromCloudinary = (\n  publicIds: string[]\n): Promise<DeleteApiResponse> => {\n  return new Promise((resolve, reject) => {\n    cloudinary.api.delete_resources(publicIds, (error, result) => {\n      if (error || !result) {\n        return reject(error);\n      }\n      resolve(result);\n    });\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/auth.service.ts",
                          "content": "import { NextFunction } from \"express\";\nimport db from \"../configs/db\";\nimport { users } from \"../drizzle/schemas/user.schema\";\nimport { eq } from \"drizzle-orm\";\nimport { ApiError } from \"../utils/api-error\";\nimport { hashPassword, verifyPassword } from \"../helpers/auth.helpers\";\nimport { SignupUserType, VerifyOtpType } from \"../validators/auth\";\nimport {\n  DELETE_ACCOUNT_TOKEN_EXPIRY,\n  LOCK_TIME_MS,\n  LOGIN_MAX_ATTEMPTS,\n  OTP_CODE_LENGTH,\n  OTP_EXPIRES_IN,\n  REACTIVATION_AVAILABLE_AT,\n  REFRESH_TOKEN_EXPIRY,\n  RESET_PASSWORD_TOKEN_EXPIRY,\n  SESSION_EXPIRY\n} from \"../constants/auth\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../utils/jwt\";\nimport {\n  generateHashedToken,\n  generateOTP,\n  generateSecureToken,\n  generateUUID\n} from \"../helpers/token.helpers\";\nimport { AvatarData, RefreshTokenData, SessionData } from \"../types/user\";\nimport { OtpService } from \"./otp.service\";\nimport { deleteFileFromCloudinary } from \"./cloudinary.service\";\nimport redisClient from \"../configs/redis\";\nimport { logger } from \"../utils/logger\";\nimport env from \"../configs/env\";\nimport { sendEmail } from \"../utils/send-mail\";\nimport { getRemainingTime } from \"../utils/date\";\n\nexport type CookieOptionsType = {\n  setAuthCookie?: (\n    accessToken: string,\n    refreshToken: string,\n    sessionId: string\n  ) => void;\n};\n\nexport class AuthService {\n  static async registerUser(user: Omit<SignupUserType, \"confirmPassword\">) {\n    try {\n      const { name, email, password, role } = user;\n      const existingUser = await db.query.users.findFirst({\n        where: eq(users.email, email)\n      });\n\n      if (existingUser) {\n        throw ApiError.conflict(\"User with this email already exists\");\n      }\n\n      const pending = await redisClient.get(`user:pending:${email}`);\n\n      if (pending) {\n        throw ApiError.conflict(\n          \"Signup already in progress. Check your email for OTP.\"\n        );\n      }\n\n      const hashedPassword = await hashPassword(password);\n\n      await OtpService.checkOtpRestrictions(email);\n      await OtpService.trackOtpRequests(email);\n\n      const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n      const redisKey = `user:${email}:${hashCode}`;\n      const indexKey = `user:pending:${email}`;\n      const userData = JSON.stringify({\n        name,\n        email,\n        role,\n        password: hashedPassword\n      });\n\n      await OtpService.sendOtp({\n        name,\n        email,\n        templateName: \"email-verification\",\n        code,\n        hashCode,\n        subject: \"Email Verification\"\n      });\n\n      try {\n        await redisClient.set(redisKey, userData, {\n          expiration: {\n            type: \"PX\",\n            value: OTP_EXPIRES_IN\n          }\n        });\n\n        await redisClient.set(indexKey, hashCode, {\n          expiration: {\n            type: \"PX\",\n            value: OTP_EXPIRES_IN\n          }\n        });\n      } catch (error) {\n        await Promise.allSettled([\n          redisClient.del(redisKey),\n          redisClient.del(indexKey),\n          redisClient.del(`otp:${email}`),\n          redisClient.del(`otp_cooldown:${email}`)\n        ]);\n\n        throw error;\n      }\n    } catch (error) {\n      logger.error(error, \"Failed to register user\");\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to register user\");\n    }\n  }\n\n  static async verifyUser({ email, otpCode }: VerifyOtpType) {\n    const hashCode = generateHashedToken(otpCode);\n\n    await OtpService.verifyOtp(hashCode, email);\n\n    const userData = await redisClient.get(`user:${email}:${hashCode}`);\n\n    if (!userData) {\n      throw ApiError.badRequest(\"Invalid or expired otp\");\n    }\n\n    const { name, email: userEmail, role, password } = JSON.parse(userData);\n\n    const [user] = await db.insert(users).values({\n      name,\n      email: userEmail,\n      role,\n      password,\n      isEmailVerified: true\n    }).returning();\n\n    await redisClient.del(`user:${email}:${hashCode}`);\n    await redisClient.del(`user:pending:${email}`);\n\n    return {\n      _id: user.id,\n      name,\n      email,\n      role: role,\n      isEmailVerified: true\n    };\n  }\n\n  static async signinUser(\n    {\n      email,\n      password,\n      ip,\n      userAgent\n    }: {\n      email: string;\n      password: string;\n      ip: string;\n      userAgent: string;\n    },\n    setCookie: CookieOptionsType\n  ) {\n    try {\n      const user = await db.query.users.findFirst({\n        where: eq(users.email, email)\n      });\n      if (!user) {\n        throw ApiError.unauthorized(\"Invalid credentials\");\n      }\n\n      if (!user.isEmailVerified) {\n        throw ApiError.unauthorized(\"Email not verified\");\n      }\n\n      if (user.lockUntil && new Date() < user.lockUntil) {\n        throw ApiError.forbidden(\n          `Your account has been locked. Please try again after ${getRemainingTime(user.lockUntil).minutes} minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        );\n      }\n\n      const isPasswordValid = await verifyPassword(\n        password,\n        user.password || \"\"\n      );\n      if (!isPasswordValid) {\n        let lockUntil = null;\n\n        let newAttempts = user.failedLoginAttempts + 1;\n\n        if (newAttempts >= LOGIN_MAX_ATTEMPTS) {\n          lockUntil = new Date(Date.now() + LOCK_TIME_MS);\n        }\n\n        await db.update(users).set({\n          failedLoginAttempts: newAttempts,\n          lockUntil\n        }).where(eq(users.id, user.id));\n\n        throw ApiError.unauthorized(\"Invalid credentials\");\n      }\n\n      await db.update(users).set({\n        failedLoginAttempts: 0,\n        lockUntil: null\n      }).where(eq(users.id, user.id));\n\n      await AuthService.handleToken(\n        {\n          _id: user.id,\n          role: user.role as \"user\" | \"admin\",\n          ip,\n          userAgent\n        },\n        setCookie\n      );\n\n      return {\n        id: user.id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        isEmailVerified: user.isEmailVerified\n      };\n    } catch (err) {\n      if (err instanceof ApiError) {\n        throw err;\n      }\n      throw ApiError.server(\"Signin failed\");\n    }\n  }\n\n  static async handleToken(\n    user: { _id: string; role: \"user\" | \"admin\" } & {\n      ip: string;\n      userAgent: string;\n    },\n    context: CookieOptionsType\n  ) {\n    const sessionId = generateUUID();\n\n    const accessToken = generateAccessToken({\n      _id: user._id,\n      role: user.role,\n      sessionId\n    });\n\n    const refreshToken = generateRefreshToken({\n      _id: user._id,\n      sessionId\n    });\n\n    const hashedRefreshToken = generateHashedToken(refreshToken);\n\n    const refreshTokenData: RefreshTokenData = {\n      userId: user._id,\n      tokenHash: hashedRefreshToken,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    };\n\n    const sessionData: SessionData = {\n      userId: user._id,\n      sessionId,\n      refreshTokenHash: hashedRefreshToken,\n      userAgent: user.userAgent,\n      ip: user.ip,\n      createdAt: new Date(),\n      expiresAt: new Date(Date.now() + SESSION_EXPIRY)\n    };\n\n    const refreshTokenKey = `refreshToken:${hashedRefreshToken}`;\n\n    await redisClient.set(refreshTokenKey, JSON.stringify(refreshTokenData), {\n      expiration: {\n        type: \"PX\",\n        value: REFRESH_TOKEN_EXPIRY\n      }\n    });\n\n    const sessionKey = `session:${sessionId}`;\n\n    const userSessionsKey = `user_sessions:${user._id}`;\n\n    await redisClient.set(sessionKey, JSON.stringify(sessionData), {\n      expiration: {\n        type: \"PX\",\n        value: SESSION_EXPIRY\n      }\n    });\n\n    // add sessionId to user's set\n    await redisClient.sAdd(userSessionsKey, sessionId);\n\n    context.setAuthCookie &&\n      context.setAuthCookie(accessToken, refreshToken, sessionId);\n\n    await db.update(users).set({\n      lastLoginAt: new Date(),\n      failedLoginAttempts: 0,\n      lockUntil: null\n    }).where(eq(users.id, user._id));\n  }\n\n  static async getUserProfile(userId: string) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.id, userId)\n    });\n    return user;\n  }\n\n  static async refreshTokens(accessToken: string | null, refreshToken: string) {\n    if (!refreshToken) {\n      throw ApiError.unauthorized(\"Unauthorized, please login.\");\n    }\n\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n\n    if (!decodedRefresh?._id) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const refreshTokenKey = `refreshToken:${refreshTokenHash}`;\n    const sessionKey = `session:${decodedRefresh.sessionId}`;\n\n    await redisClient.watch(refreshTokenKey, sessionKey);\n\n    try {\n      const [storedToken, session] = await Promise.all([\n        redisClient.get(refreshTokenKey),\n        redisClient.get(sessionKey)\n      ]);\n\n      if (!storedToken) {\n        throw ApiError.unauthorized(\"Invalid refresh token.\");\n      }\n\n      const { userId, tokenHash, expiresAt } = JSON.parse(\n        storedToken\n      ) as RefreshTokenData;\n\n      if (\n        userId !== decodedRefresh._id ||\n        tokenHash !== refreshTokenHash\n      ) {\n        throw ApiError.unauthorized(\"Invalid refresh token.\");\n      }\n\n      if (new Date(expiresAt) < new Date()) {\n        throw ApiError.unauthorized(\"Refresh token expired.\");\n      }\n\n      if (!session) {\n        throw ApiError.unauthorized(\"Session not found.\");\n      }\n\n      const storedSessionData = JSON.parse(session) as SessionData;\n\n      if (\n        decodedRefresh.sessionId !== storedSessionData.sessionId ||\n        decodedRefresh._id !== storedSessionData.userId ||\n        storedSessionData.refreshTokenHash !== refreshTokenHash\n      ) {\n        throw ApiError.unauthorized(\"Token-session mismatch\");\n      }\n\n      if (accessToken) {\n        try {\n          const decodedAccess = verifyAccessToken(accessToken);\n          if (decodedAccess._id !== decodedRefresh._id) {\n            throw ApiError.unauthorized(\"Token mismatch.\");\n          }\n        } catch (e) {\n            // Access token might be expired, which is normal for a refresh flow\n        }\n      }\n\n      const user = await db.query.users.findFirst({\n        where: eq(users.id, decodedRefresh._id)\n      });\n      if (!user) {\n        throw ApiError.unauthorized(\"User not found.\");\n      }\n\n      const newAccessToken = generateAccessToken({\n        _id: user.id,\n        role: user.role as \"user\" | \"admin\",\n        sessionId: storedSessionData.sessionId\n      });\n\n      const newRefreshToken = generateRefreshToken({\n        _id: user.id,\n        sessionId: storedSessionData.sessionId\n      });\n      const newRefreshTokenHash = generateHashedToken(newRefreshToken);\n\n      const refreshTokenData: RefreshTokenData = {\n        userId: user.id,\n        tokenHash: newRefreshTokenHash,\n        expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n      };\n      const sessionData: SessionData = {\n        userId: user.id,\n        sessionId: storedSessionData.sessionId,\n        refreshTokenHash: newRefreshTokenHash,\n        userAgent: storedSessionData.userAgent,\n        ip: storedSessionData.ip,\n        createdAt: storedSessionData.createdAt,\n        expiresAt: new Date(Date.now() + SESSION_EXPIRY)\n      };\n\n      const newRefreshTokenKey = `refreshToken:${newRefreshTokenHash}`;\n      const transaction = redisClient.multi();\n\n      transaction.del(`refreshToken:${tokenHash}`);\n      transaction.set(newRefreshTokenKey, JSON.stringify(refreshTokenData), {\n        expiration: {\n          type: \"PX\",\n          value: REFRESH_TOKEN_EXPIRY\n        }\n      });\n      transaction.set(sessionKey, JSON.stringify(sessionData), {\n        expiration: {\n          type: \"PX\",\n          value: SESSION_EXPIRY\n        }\n      });\n\n      const transactionResult = await transaction.exec();\n\n      if (!transactionResult) {\n        throw ApiError.unauthorized(\"Refresh token already rotated.\");\n      }\n\n      return {\n        accessToken: newAccessToken,\n        refreshToken: newRefreshToken,\n        sessionId: storedSessionData.sessionId\n      };\n    } finally {\n      await redisClient.unwatch();\n    }\n  }\n\n  static async logoutUser(userId: string, sessionId: string) {\n    const sessionKey = `session:${sessionId}`;\n    const sessionData = await redisClient.get(sessionKey);\n    const userSessionsKey = `user_sessions:${userId}`;\n    if (!sessionData) {\n      throw ApiError.unauthorized(\"Session not found.\");\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.userId !== userId) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const refreshTokenKey = `refreshToken:${session.refreshTokenHash}`;\n\n    await redisClient.del(sessionKey);\n    await redisClient.del(refreshTokenKey);\n    await redisClient.sRem(userSessionsKey, sessionId);\n  }\n\n  static async forgotPassword(email: string) {\n    const user = await db.query.users.findFirst({\n      where: eq(users.email, email)\n    });\n\n    if (!user) {\n      return;\n    }\n\n    const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n    await OtpService.checkOtpRestrictions(email);\n    await OtpService.trackOtpRequests(email);\n\n    const redisKey = `reset_password:${email}:${hashCode}`;\n\n    await redisClient.set(redisKey, hashCode, {\n      expiration: {\n        type: \"PX\",\n        value: RESET_PASSWORD_TOKEN_EXPIRY\n      }\n    });\n\n    await OtpService.sendOtp({\n      email,\n      subject: \"Password Reset\",\n      templateName: \"forgot-password\",\n      name: user.name,\n      code,\n      hashCode\n    });\n  }\n\n  static async verifyResetPasswordOtp(otpCode: string, email: string) {\n    const hashedCode = generateHashedToken(otpCode);\n\n    const redisKey = `reset_password:${email}:${hashedCode}`;\n    const storedHashCode = await redisClient.get(redisKey);\n    if (!storedHashCode) {\n      throw ApiError.unauthorized(\"Invalid or expired otp\");\n    }\n    await OtpService.verifyOtp(storedHashCode, email);\n\n    await redisClient.del(`reset_password:${email}:${hashedCode}`);\n    await redisClient.set(`reset_password:status:${email}`, \"pending\", {\n      expiration: {\n        type: \"PX\",\n        value: RESET_PASSWORD_TOKEN_EXPIRY\n      }\n    });\n  }\n\n  static async getUserSessions(userId: string, currentSessionId: string) {\n    const userSessionsKey = `user_sessions:${userId}`;\n    const sessionIds = await redisClient.sMembers(userSessionsKey);\n\n    const sessions = [];\n    for (const sessionId of sessionIds) {\n      const sessionKey = `session:${sessionId}`;\n      const sessionData = await redisClient.get(sessionKey);\n      if (sessionData) {\n        const session = JSON.parse(sessionData) as SessionData;\n        sessions.push({\n          ...session,\n          isCurrent: sessionId === currentSessionId\n        });\n      }\n    }\n\n    return sessions;\n  }\n\n  static async deleteSession(userId: string, sessionId: string) {\n    const sessionKey = `session:${sessionId}`;\n    const sessionData = await redisClient.get(sessionKey);\n    const userSessionsKey = `user_sessions:${userId}`;\n\n    if (!sessionData) {\n      throw ApiError.notFound(\"Session not found.\");\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.userId !== userId) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const refreshTokenKey = `refreshToken:${session.refreshTokenHash}`;\n\n    await redisClient.del(sessionKey);\n    await redisClient.del(refreshTokenKey);\n    await redisClient.sRem(userSessionsKey, sessionId);\n  }\n\n  static async deleteAllUserSessions(userId: string) {\n    const userSessionsKey = `user_sessions:${userId}`;\n    const sessionIds = await redisClient.sMembers(userSessionsKey);\n\n    if (sessionIds.length === 0) {\n      return;\n    }\n\n    const sessions = await Promise.all(\n      sessionIds.map(async sessionId => {\n        const sessionKey = `session:${sessionId}`;\n        const sessionData = await redisClient.get(sessionKey);\n\n        return {\n          sessionKey,\n          session: sessionData ? (JSON.parse(sessionData) as SessionData) : null\n        };\n      })\n    );\n\n    await Promise.all(\n      sessions.flatMap(({ sessionKey, session }) => {\n        const deletions = [redisClient.del(sessionKey)];\n\n        if (session?.refreshTokenHash) {\n          deletions.push(\n            redisClient.del(`refreshToken:${session.refreshTokenHash}`)\n          );\n        }\n\n        return deletions;\n      })\n    );\n\n    await redisClient.del(userSessionsKey);\n  }\n\n  static async resetPassword(\n    next: NextFunction,\n    email: string,\n    newPassword: string\n  ) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.email, email)\n    });\n\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n      throw (\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${\n            getRemainingTime(user.lockUntil).minutes\n          } minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        )\n      );\n    }\n\n    if (user.failedLoginAttempts >= LOGIN_MAX_ATTEMPTS && user.lockUntil) {\n      throw (\n        ApiError.forbidden(\n          `You have exceeded the maximum number of login attempts. Please try again after ${\n            getRemainingTime(user.lockUntil).minutes\n          } minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        )\n      );\n    }\n\n    if (!user.isEmailVerified) {\n      throw ApiError.unauthorized(\"Please verify your email first.\");\n    }\n\n    const redisKey = `reset_password:status:${email}`;\n    const status = await redisClient.get(redisKey);\n    if (status !== \"pending\") {\n      throw (\n        ApiError.unauthorized(\n          \"Please request a password reset before attempting to set a new password.\"\n        )\n      );\n    }\n\n    const oldPassword = user.password;\n\n    const isOldPassword = await verifyPassword(\n      newPassword,\n      oldPassword as string\n    );\n\n    if (isOldPassword) {\n      throw ApiError.badRequest(\"New password should be different!\");\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n    await db.update(users).set({ password: hashedPassword }).where(eq(users.email, email));\n    await redisClient.del(`reset_password:status:${email}`);\n\n    //? Delete all user sessions\n    await this.deleteAllUserSessions(user.id);\n\n    return {\n      message: \"Password reset successfully. Please login!\"\n    };\n  }\n\n  static async changePassword(\n    next: NextFunction,\n    {\n      newPassword,\n      oldPassword,\n      userId\n    }: {\n      userId: string;\n      newPassword: string;\n      oldPassword: string;\n    }\n  ) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.id, userId)\n    });\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    if (!user.isEmailVerified) {\n      throw ApiError.unauthorized(\"Please verify your email first.\");\n    }\n\n    const isOldPassword = await verifyPassword(\n      oldPassword,\n      user.password || \"\"\n    );\n\n    if (!isOldPassword) {\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    if (newPassword === oldPassword) {\n      throw ApiError.badRequest(\"New password should be different!\");\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n    await db.update(users).set({ password: hashedPassword }).where(eq(users.id, userId));\n\n    await this.deleteAllUserSessions(userId);\n\n    return {\n      message: \"Password changed successfully. Please login again!\"\n    };\n  }\n\n  static async requestDeleteAccount(userId: string, password: string) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.id, userId)\n    });\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const isPasswordValid = await verifyPassword(password, user.password || \"\");\n\n    if (!isPasswordValid) {\n      let lockUntil = null;\n\n      let newAttempts = user.failedLoginAttempts + 1;\n\n      if (newAttempts >= LOGIN_MAX_ATTEMPTS) {\n        lockUntil = new Date(Date.now() + LOCK_TIME_MS);\n      }\n\n      await db.update(users).set({\n        failedLoginAttempts: newAttempts,\n        lockUntil\n      }).where(eq(users.id, user.id));\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    const token = generateSecureToken();\n    const hashedToken = generateHashedToken(token);\n\n    const redisKey = `delete_account:token:${userId}`;\n\n    if (await redisClient.get(redisKey)) {\n      throw ApiError.badRequest(\"Delete account token already requested!\");\n    }\n\n    await redisClient.set(redisKey, hashedToken, {\n      expiration: {\n        type: \"PX\",\n        value: DELETE_ACCOUNT_TOKEN_EXPIRY\n      }\n    });\n\n    const deleteAccountUrl = `${env.CLIENT_URL}/account/delete?token=${token}`;\n    logger.info({ userId }, \"Delete account email queued\");\n    await sendEmail({\n      email: user.email,\n      subject: \"Delete Account Request\",\n      templateName: \"delete-account\",\n      data: {\n        name: user.name,\n        deleteAccountUrl\n      }\n    });\n  }\n\n  static async deleteOrDeactiveAccount({\n    userId,\n    type,\n    token\n  }: {\n    userId: string;\n    type: \"soft\" | \"hard\";\n    token: string;\n  }) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.id, userId)\n    });\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const redisKey = `delete_account:token:${userId}`;\n    const storedToken = await redisClient.get(redisKey);\n    if (!storedToken) {\n      throw ApiError.badRequest(\"Invalid or expired token!\");\n    }\n\n    const isTokenValid = generateHashedToken(token) === storedToken;\n    if (!isTokenValid) {\n      throw ApiError.badRequest(\"Invalid or expired token!\");\n    }\n\n    await redisClient.del(redisKey);\n\n    if (type === \"soft\") {\n\n      await db.update(users).set({\n        isDeleted: true,\n        deletedAt: new Date(),\n        reActivateAvailableAt: new Date(Date.now() + REACTIVATION_AVAILABLE_AT)\n      }).where(eq(users.id, userId));\n      await AuthService.deleteAllUserSessions(userId);\n    } else if (type === \"hard\") {\n      const avatar = user.avatar as AvatarData | string | null | undefined;\n\n      if (avatar && typeof avatar !== \"string\" && avatar.public_id) {\n        await deleteFileFromCloudinary([avatar.public_id]);\n      }\n      await db.delete(users).where(eq(users.id, userId));\n      await AuthService.deleteAllUserSessions(userId);\n    }\n  }\n\n  static async reactivateAccount(userId: string) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.id, userId)\n    });\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n      const remainingTime = getRemainingTime(user.lockUntil);\n      throw ApiError.badRequest(\n        `Your account has been locked. Please try again after ${remainingTime.minutes} minutes and ${remainingTime.seconds} seconds.`\n      );\n    }\n\n    if (!user?.isDeleted || !user?.deletedAt) {\n      throw ApiError.badRequest(\"Your account is already active!\");\n    }\n\n    if (\n      user?.reActivateAvailableAt &&\n      new Date(user?.reActivateAvailableAt) > new Date()\n    ) {\n      throw ApiError.forbidden(\n        `Your account has been locked. Please try again after ${\n          getRemainingTime(user.reActivateAvailableAt).minutes\n        } minutes and ${getRemainingTime(user.reActivateAvailableAt).seconds} seconds.`\n      );\n    }\n\n    await db.update(users).set({\n      isDeleted: false,\n      deletedAt: null,\n      reActivateAvailableAt: null\n    }).where(eq(users.id, userId));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/auth.service.test.ts",
                          "content": "import assert from \"node:assert/strict\";\nimport test from \"node:test\";\n\nconst applyTestEnv = () => {\n  process.env.NODE_ENV = \"test\";\n  process.env.PORT = \"3000\";\n  process.env.DATABASE_URL = \"postgres://user:pass@localhost:5432/testdb\";\n  process.env.CORS_ORIGIN = \"http://localhost:3000\";\n  process.env.CLIENT_URL = \"https://example.com\";\n  process.env.LOG_LEVEL = \"info\";\n  process.env.JWT_ACCESS_SECRET = \"a\".repeat(32);\n  process.env.JWT_REFRESH_SECRET = \"b\".repeat(32);\n  process.env.CRYPTO_SECRET = \"c\".repeat(32);\n  process.env.RESEND_API_KEY = \"test-key\";\n  process.env.EMAIL_FROM = \"noreply@example.com\";\n  process.env.CLOUDINARY_CLOUD_NAME = \"cloud\";\n  process.env.CLOUDINARY_API_KEY = \"key\";\n  process.env.CLOUDINARY_API_SECRET = \"secret\";\n  process.env.GOOGLE_CLIENT_ID = \"google-client\";\n  process.env.GOOGLE_CLIENT_SECRET = \"google-secret\";\n  process.env.GOOGLE_REDIRECT_URI = \"https://example.com/google/callback\";\n  process.env.GITHUB_CLIENT_ID = \"github-client\";\n  process.env.GITHUB_CLIENT_SECRET = \"github-secret\";\n  process.env.GITHUB_REDIRECT_URI = \"https://example.com/github/callback\";\n  process.env.FACEBOOK_APP_ID = \"facebook-app\";\n  process.env.FACEBOOK_APP_SECRET = \"facebook-secret\";\n  process.env.FACEBOOK_REDIRECT_URI = \"https://example.com/facebook/callback\";\n  process.env.REDIS_URL = \"redis://localhost:6379\";\n};\n\ntest(\"registerUser cleans up pending signup state when redis persistence fails\", async t => {\n  applyTestEnv();\n\n  const [{ AuthService }, { OtpService }, { default: db }, { default: redisClient }] =\n    await Promise.all([\n      import(\"./auth.service.ts\"),\n      import(\"./otp.service.ts\"),\n      import(\"../configs/db.ts\"),\n      import(\"../configs/redis.ts\")\n    ]);\n\n  const email = \"test@example.com\";\n  const delCalls: string[] = [];\n  const setKeys: string[] = [];\n\n  t.mock.method(db.query.users, \"findFirst\", async () => null);\n  t.mock.method(OtpService, \"checkOtpRestrictions\", async () => undefined);\n  t.mock.method(OtpService, \"trackOtpRequests\", async () => undefined);\n  t.mock.method(OtpService, \"sendOtp\", async () => undefined);\n  t.mock.method(redisClient, \"get\", async () => null);\n  t.mock.method(redisClient, \"set\", async (key: string) => {\n    setKeys.push(key);\n\n    if (key === `user:pending:${email}`) {\n      throw new Error(\"redis set failed\");\n    }\n\n    return \"OK\";\n  });\n  t.mock.method(redisClient, \"del\", async (key: string) => {\n    delCalls.push(key);\n    return 1;\n  });\n\n  await assert.rejects(\n    () =>\n      AuthService.registerUser({\n        name: \"Test User\",\n        email,\n        password: \"super-secret\",\n        role: \"user\"\n      }),\n    /Failed to register user/\n  );\n\n  const redisKey = setKeys.find(key => key.startsWith(`user:${email}:`));\n  const indexKey = `user:pending:${email}`;\n\n  assert.ok(redisKey);\n  assert.ok(setKeys.includes(indexKey));\n  assert.ok(delCalls.includes(indexKey));\n  assert.ok(delCalls.includes(redisKey!));\n  assert.ok(delCalls.includes(`otp:${email}`));\n  assert.ok(delCalls.includes(`otp_cooldown:${email}`));\n});\n\ntest(\"refreshTokens validates the session refreshTokenHash and rotates via a watched redis transaction\", async () => {\n  const { readFile } = await import(\"node:fs/promises\");\n  const source = await readFile(new URL(\"./auth.service.ts\", import.meta.url), \"utf8\");\n\n  assert.match(source, /await redisClient\\.watch\\(refreshTokenKey, sessionKey\\);/);\n  assert.match(\n    source,\n    /storedSessionData\\.refreshTokenHash !== refreshTokenHash/\n  );\n  assert.match(source, /const transaction = redisClient\\.multi\\(\\);/);\n  assert.match(source, /const transactionResult = await transaction\\.exec\\(\\);/);\n  assert.match(\n    source,\n    /if \\(!transactionResult\\) {\\s*throw ApiError\\.unauthorized\\(\"Refresh token already rotated\\.\"\\);/\n  );\n});\n\ntest(\"resetPassword and changePassword throw ApiError instead of calling next(ApiError)\", async () => {\n  const { readFile } = await import(\"node:fs/promises\");\n  const source = await readFile(new URL(\"./auth.service.ts\", import.meta.url), \"utf8\");\n\n  assert.doesNotMatch(\n    source,\n    /return next\\(ApiError\\.(unauthorized|forbidden|badRequest)\\(/\n  );\n});\n\ntest(\"requestDeleteAccount never logs the raw delete-account token\", async () => {\n  const { readFile } = await import(\"node:fs/promises\");\n  const source = await readFile(new URL(\"./auth.service.ts\", import.meta.url), \"utf8\");\n\n  assert.match(source, /logger\\.info\\(\\{ userId \\}, \"Delete account email queued\"\\)/);\n  assert.doesNotMatch(source, /logger\\.warn\\(`Delete account token: \\$\\{token\\}`\\)/);\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/oauth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport passport from \"passport\";\nimport { facebookOAuth, githubOAuth, googleOAuth } from \"../controllers/oauth.controller\";\n\nconst router = Router();\n\nrouter.get(\n  \"/github\",\n  passport.authenticate(\"github\", { scope: [\"user:email\"] })\n);\n\nrouter.get(\n  \"/github/callback\",\n  passport.authenticate(\"github\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed,\n    session: false\n  }),\n  githubOAuth\n);\n\nrouter.get(\n  \"/facebook\",\n  passport.authenticate(\"facebook\", { scope: [\"email\", \"user_location\"] })\n);\n\nrouter.get(\n  \"/facebook/callback\",\n  passport.authenticate(\"facebook\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed,\n    session: false,\n    failureMessage: true\n  }),\n  facebookOAuth\n);\n\nrouter.get(\n  \"/google\",\n  passport.authenticate(\"google\", {\n    scope: [\"email\", \"profile\", \"openid\"],\n    prompt: \"consent\"\n  })\n);\n\nrouter.get(\n  \"/google/callback\",\n  passport.authenticate(\"google\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed\n    session: false\n  }),\n  googleOAuth\n);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/index.ts",
                          "content": "import { Router } from \"express\";\nimport healthRoutes from \"./health.routes\";\nimport authRoutes from \"./auth.routes\";\nimport oauthRoutes from \"./oauth.routes\";\n\nconst router = Router();\n\nrouter.use(\"/v1/health\", healthRoutes);\nrouter.use(\"/v1/auth\", authRoutes);\nrouter.use(\"/auth\", oauthRoutes); //* Here versioning is not given because, in google and github callback routes, we are not using versioning. process.env.GOOGLE_REDIRECT_URI\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/health.routes.ts",
                          "content": "import { Router } from \"express\";\nimport {\n  healthCheck,\n  detailedHealthCheck\n} from \"../controllers/health.controller\";\n\nconst router = Router();\n\nrouter.get(\"/\", healthCheck);\nrouter.get(\"/detailed\", detailedHealthCheck);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/auth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport { validateRequest } from \"../middlewares/validate-request\";\nimport {\n  ChangePasswordSchema,\n  DeleteAccountSchema,\n  RequestOtpSchema,\n  ResetPasswordSchema,\n  SigninSchema,\n  SignupSchema,\n  UpdateProfileSchema,\n  VerifyOtpSchema\n} from \"../validators/auth\";\nimport {\n  changePassword,\n  deleteAccount,\n  deleteAllUserSessions,\n  deleteUserSession,\n  forgotPassword,\n  getUserProfile,\n  getUserSessions,\n  logoutUser,\n  reactivateAccount,\n  refreshToken,\n  requestDeleteAccount,\n  resetPassword,\n  signinUser,\n  signupUser,\n  updateProfile,\n  verifyResetPasswordOtp,\n  verifyUser\n} from \"../controllers/auth.controller\";\nimport { verifyAuthentication } from \"../middlewares/verify-auth\";\nimport { checkUserAccountRestriction } from \"../middlewares/user-account-restriction\";\nimport {\n  changePasswordLimiter,\n  deleteAccountLimiter,\n  otpRequestLimiter,\n  resetPasswordLimiter,\n  signinRateLimiter,\n  signupRateLimiter\n} from \"../middlewares/rate-limiter\";\nimport upload from \"../middlewares/upload-file\";\n\nconst router = Router();\n\nrouter.post(\n  \"/signup\",\n  validateRequest(SignupSchema),\n  signupRateLimiter,\n  signupUser\n);\n\nrouter.post(\"/verify-user\", validateRequest(VerifyOtpSchema), verifyUser);\n\nrouter.post(\n  \"/signin\",\n  validateRequest(SigninSchema),\n  signinRateLimiter,\n  signinUser\n);\n\nrouter.get(\"/profile\", verifyAuthentication, getUserProfile);\n\nrouter.patch(\n  \"/profile\",\n  upload.single(\"avatar\"),\n  validateRequest(UpdateProfileSchema),\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  updateProfile\n);\n\nrouter.get(\"/sessions\", verifyAuthentication, getUserSessions);\n\nrouter.delete(\n  \"/sessions\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  deleteAllUserSessions\n);\n\nrouter.delete(\n  \"/sessions/:sessionId\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  deleteUserSession\n);\n\nrouter.post(\"/refresh-token\", refreshToken);\n\nrouter.post(\n  \"/logout\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  logoutUser\n);\n\nrouter.post(\n  \"/forgot-password\",\n  validateRequest(RequestOtpSchema.pick({ email: true })),\n  otpRequestLimiter,\n  forgotPassword\n);\n\nrouter.post(\n  \"/verify-reset-otp\",\n  validateRequest(VerifyOtpSchema),\n  otpRequestLimiter,\n  verifyResetPasswordOtp\n);\n\nrouter.post(\n  \"/reset-password\",\n  validateRequest(ResetPasswordSchema),\n  resetPasswordLimiter,\n  resetPassword\n);\n\nrouter.post(\n  \"/change-password\",\n  verifyAuthentication,\n  validateRequest(ChangePasswordSchema),\n  checkUserAccountRestriction,\n  changePasswordLimiter,\n  changePassword\n);\n\nrouter.post(\n  \"/account/request-delete\",\n  verifyAuthentication,\n  validateRequest(SigninSchema.pick({ password: true })),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  requestDeleteAccount\n);\n\nrouter.delete(\n  \"/account/delete\",\n  verifyAuthentication,\n  validateRequest(DeleteAccountSchema),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  deleteAccount\n);\n\nrouter.put(\"/account/reactivate\", verifyAuthentication, reactivateAccount);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/helpers/token.helpers.ts",
                          "content": "import crypto from \"node:crypto\";\n\nexport function generateOTP(length: number = 6, ttlMinutes: number = 5) {\n  const code = crypto\n    .randomInt(0, Math.pow(10, length))\n    .toString()\n    .padStart(length, \"0\");\n\n  const hashCode = crypto\n    .createHash(\"sha256\")\n    .update(String(code))\n    .digest(\"hex\");\n\n  const expiresAt = new Date(Date.now() + ttlMinutes * 60 * 1000).toISOString();\n\n  return { code, hashCode, expiresAt };\n}\n\nexport function generateHashedToken(token: string): string {\n  return crypto.createHash(\"sha256\").update(String(token)).digest(\"hex\");\n}\n\nexport function generateSecureToken(length: number = 32): string {\n  return crypto.randomBytes(length).toString(\"hex\");\n}\n\nexport function verifyHashedToken(token: string, hashedToken: string): boolean {\n  return (\n    crypto.createHash(\"sha256\").update(String(token)).digest(\"hex\") ===\n    hashedToken\n  );\n}\n\nexport function generateTokenAndHashedToken(id: string) {\n  const cryptoSecret = process.env.CRYPTO_SECRET! || \"secret\";\n  const token = crypto\n    .createHmac(\"sha256\", cryptoSecret)\n    .update(String(id))\n    .digest(\"hex\");\n\n  const hashedToken = crypto\n    .createHash(\"sha256\")\n    .update(String(token))\n    .digest(\"hex\");\n  return { token, hashedToken };\n}\n\nexport function generateUUID(): string {\n  return crypto.randomUUID();\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/helpers/cookie.helper.ts",
                          "content": "import { Response } from \"express\";\nimport {\n  ACCESS_TOKEN_EXPIRY,\n  REFRESH_TOKEN_EXPIRY,\n  SESSION_EXPIRY\n} from \"../constants/auth\";\nimport env from \"../configs/env\";\n\nconst isProduction = env.NODE_ENV === \"production\";\n\nexport const COOKIE_OPTIONS = {\n  httpOnly: true,\n  secure: isProduction,\n  sameSite: isProduction ? (\"none\" as const) : (\"lax\" as const),\n  path: \"/\"\n};\n\nexport function setAuthCookies(\n  res: Response,\n  accessToken: string,\n  refreshToken: string,\n  sessionId: string\n) {\n  setCookies(res, [\n    {\n      cookie: \"accessToken\",\n      value: accessToken,\n      maxAge: ACCESS_TOKEN_EXPIRY\n    },\n    {\n      cookie: \"refreshToken\",\n      value: refreshToken,\n      maxAge: REFRESH_TOKEN_EXPIRY,\n      path: \"/api/v1/auth/refresh-token\"\n    },\n    {\n      cookie: \"sid\",\n      value: sessionId,\n      maxAge: SESSION_EXPIRY\n    }\n  ]);\n}\n\nexport function clearAuthCookies(res: Response) {\n  clearCookie(res, \"accessToken\");\n  clearCookie(res, \"refreshToken\", \"/api/v1/auth/refresh-token\");\n  clearCookie(res, \"sid\");\n}\n\nexport function clearCookie(\n  res: Response,\n  cookie: string = \"sid\",\n  path: string = \"/\"\n) {\n  res.clearCookie(cookie, {\n    ...COOKIE_OPTIONS,\n    path\n  });\n}\n\ntype Cookie = {\n  cookie: string;\n  value: string;\n  maxAge: number;\n  path?: string;\n};\n\nexport function setCookies(res: Response, cookies: Cookie[]) {\n  cookies.forEach(({ cookie, value, maxAge, path = \"/\" }) => {\n    res.cookie(cookie, value, {\n      ...COOKIE_OPTIONS,\n      path,\n      maxAge\n    });\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/helpers/cookie.helper.test.ts",
                          "content": "import assert from \"node:assert/strict\";\nimport { readFile } from \"node:fs/promises\";\nimport test from \"node:test\";\n\ntest(\"mvc cookie helper clears refreshToken using the refresh-token path\", async () => {\n  const helperSource = await readFile(\n    new URL(\"./cookie.helper.ts\", import.meta.url),\n    \"utf8\"\n  );\n\n  assert.match(\n    helperSource,\n    /clearCookie\\(res, \"refreshToken\", \"\\/api\\/v1\\/auth\\/refresh-token\"\\)/\n  );\n  assert.match(\n    helperSource,\n    /res\\.clearCookie\\(cookie, \\{\\s*\\.\\.\\.COOKIE_OPTIONS,\\s*path\\s*\\}\\)/\n  );\n});\n\ntest(\"mvc refreshToken controller no longer redundantly clears refreshToken\", async () => {\n  const controllerSource = await readFile(\n    new URL(\"../controllers/auth.controller.ts\", import.meta.url),\n    \"utf8\"\n  );\n\n  assert.doesNotMatch(controllerSource, /clearCookie\\(res, \"refreshToken\"\\)/);\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/helpers/auth.helpers.ts",
                          "content": "import { OTPType } from \"@/types/user\";\nimport argon2 from \"argon2\";\n\nexport async function hashPassword(password: string): Promise<string> {\n  return argon2.hash(password);\n}\n\nexport async function verifyPassword(\n  password: string,\n  hash: string\n): Promise<boolean> {\n  return argon2.verify(hash, password);\n}\n\nexport const buildRedisKey = (\n  email: string,\n  otpType: OTPType,\n  suffix: string\n) => `otp:${suffix}:${email}:${otpType}`;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/verify-auth.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Response } from \"express\";\nimport { verifyAccessToken } from \"../utils/jwt\";\nimport { ApiError } from \"../utils/api-error\";\nimport { SessionData, UserRequest } from \"../types/user\";\nimport redisClient from \"@/configs/redis\";\n\nexport async function verifyAuthentication(\n  req: UserRequest,\n  _res: Response,\n  next: NextFunction\n): Promise<void> {\n  const authHeader = req.headers.authorization || \"\";\n  const token = authHeader.startsWith(\"Bearer \")\n    ? authHeader.split(\" \")[1]\n    : null;\n\n  const accessToken = req.cookies?.accessToken || token;\n  if (!accessToken) {\n    return next(ApiError.unauthorized(\"Missing access token\"));\n  }\n\n  try {\n    const decoded = verifyAccessToken(accessToken);\n\n    const sessionKey = `session:${decoded.sessionId}`;\n    const sessionData = await redisClient.get(sessionKey);\n    if (!sessionData) {\n      return next(ApiError.unauthorized(\"Session not found\"));\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.ip !== req.ip) {\n      return next(ApiError.unauthorized(\"Suspicious session\"));\n    }\n\n    if (session.userAgent !== req.headers[\"user-agent\"]) {\n      return next(ApiError.unauthorized(\"Suspicious session\"));\n    }\n\n    if (session.expiresAt < new Date()) {\n      return next(ApiError.unauthorized(\"Session expired\"));\n    }\n\n    req.user = decoded;\n    return next();\n  } catch (err: any) {\n    if (err.name === \"TokenExpiredError\") {\n      return next(ApiError.unauthorized(\"Access token expired\"));\n    }\n    return next(ApiError.unauthorized(\"Invalid access token\"));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/validate-request.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport z, { ZodError, type ZodObject } from \"zod\";\n\nimport { ApiError } from \"../utils/api-error\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const validateRequest = (schema: ZodObject<any>) => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    try {\n      schema.parse(req.body);\n\n      next();\n    } catch (error) {\n      if (!(error instanceof ZodError)) {\n        return next(error);\n      }\n\n      return next(\n        ApiError.badRequest(\n          \"Invalid request data\",\n          z.flattenError(error).fieldErrors || z.flattenError(error)\n        )\n      );\n    }\n  };\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/validate-id.ts",
                          "content": "import { ApiError } from \"../utils/api-error\";\nimport { NextFunction, Request, Response } from \"express\";\n\nexport const validateObjectId = (paramName: string = \"id\") => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const value =\n      req?.params[paramName] || req?.body[paramName] || req?.query[paramName];\n    if (!value || typeof value !== \"string\" || value.trim().length === 0) {\n      throw ApiError.badRequest(`Invalid ${paramName}`);\n    }\n\n    next();\n  };\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/user-account-restriction.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Response } from \"express\";\nimport { UserRequest } from \"../types/user\";\nimport db from \"../configs/db\";\nimport { users } from \"../drizzle/schemas/user.schema\";\nimport { eq } from \"drizzle-orm\";\nimport { ApiError } from \"../utils/api-error\";\nimport { logger } from \"../utils/logger\";\nimport { getRemainingTime } from \"@/utils/date\";\n\nexport async function checkUserAccountRestriction(\n  req: UserRequest,\n  _res: Response,\n  next: NextFunction\n): Promise<void> {\n  try {\n    if (!req.user?._id) {\n      return next(ApiError.unauthorized(\"Unauthorized\"));\n    }\n\n    const user = await db.query.users.findFirst({\n      where: eq(users.id, req.user._id)\n    });\n\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n    }\n\n    if (user.isDeleted || user.deletedAt) {\n      return next(ApiError.forbidden(\"Your account has been deactivated.\"));\n    }\n\n    if (user.lockUntil && user.lockUntil.getTime() > Date.now()) {\n      const remainingTime = getRemainingTime(user.lockUntil);\n\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${remainingTime.minutes} minutes and ${remainingTime.seconds} seconds.`\n        )\n      );\n    }\n\n    if (!user.isEmailVerified) {\n      return next(\n        ApiError.forbidden(\"Email not verified. Please verify your email.\")\n      );\n    }\n\n    return next();\n  } catch (err: any) {\n    logger.error(err?.message || err);\n    return next(ApiError.server(\"Something went wrong\"));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/upload-file.ts",
                          "content": "import multer from \"multer\";\n\nexport const ALLOWED_FILE_TYPES = [\n  \"image/jpeg\",\n  \"image/png\",\n  \"image/webp\",\n  \"video/mp4\",\n  \"video/mpeg\",\n  \"video/quicktime\",\n  \"application/pdf\"\n];\n\nexport const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB\n\nconst storage = multer.memoryStorage();\n\nconst fileFilter: multer.Options[\"fileFilter\"] = (_req, file, cb) => {\n  if (!ALLOWED_FILE_TYPES.includes(file.mimetype)) {\n    return cb(null, false);\n  }\n  cb(null, true);\n};\n\nconst upload = multer({\n  storage,\n  limits: { fileSize: MAX_FILE_SIZE },\n  fileFilter\n});\n\nexport default upload;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/security-header.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport cors from \"cors\";\nimport { Express } from \"express\";\nimport helmet from \"helmet\";\nimport env from \"../configs/env\";\n\nexport const configureSecurityHeaders = (app: Express) => {\n  // Use Helmet to set various security-related HTTP headers\n  app.use(helmet());\n\n  // Configure CORS\n  app.use(\n    cors({\n      origin: env.CORS_ORIGIN || \"*\",\n      credentials: true,\n      methods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\"],\n      allowedHeaders: [\"Content-Type\", \"Authorization\", \"X-Requested-With\"]\n    })\n  );\n\n  // Additional custom security headers\n  app.use((req: Request, res: Response, next: NextFunction) => {\n    res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n    res.setHeader(\"X-Frame-Options\", \"DENY\");\n    res.setHeader(\"X-XSS-Protection\", \"1; mode=block\");\n    next();\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/rate-limiter.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Request, Response } from \"express\";\nimport { rateLimit } from \"express-rate-limit\";\nimport { STATUS_CODES } from \"../constants/status-codes\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const rateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000, // 15 minutes\n  max: 100, // Limit each IP to 100 requests per window\n  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers\n  legacyHeaders: false, // Disable the `X-RateLimit-*` headers\n  message: {\n    success: false,\n    message:\n      \"Too many requests from this IP, please try again after 15 minutes\",\n    status: 429\n  },\n  handler: (req: Request, res: Response, next: NextFunction, options: any) => {\n    next(new ApiError(STATUS_CODES.TOO_MANY_REQUESTS, options.message.message));\n  }\n});\n\n/**\n * Stricter rate limiter for sensitive routes (e.g., auth, login)\n */\nexport const authRateLimiter = rateLimit({\n  windowMs: 60 * 60 * 1000, // 1 hour\n  max: 5, // Limit each IP to 5 failed attempts per hour\n  handler: (req, res, next, options) => {\n    next(\n      ApiError.tooManyRequests(\n        \"Too many login attempts, please try again after an hour\"\n      )\n    );\n  }\n});\n\n/**\n * Rate limiter for login route\n */\nexport const signinRateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many login attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\n/**\n * Rate limiter for registration route\n */\nexport const signupRateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many registration attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const otpRequestLimiter = rateLimit({\n  windowMs: 10 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many OTP requests. Please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const otpVerificationLimiter = rateLimit({\n  windowMs: 10 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many OTP verification attempts. Please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const resetPasswordLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many password reset attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const deleteAccountLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many account deletion attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const changePasswordLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many password change attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/not-found-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const notFoundHandler = (\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => {\n  throw ApiError.notFound(`Route ${req.method} ${req.originalUrl} not found`);\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/error-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport env from \"../configs/env\";\n\nimport { logger } from \"../utils/logger\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const errorHandler = (\n  err: Error,\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => {\n  if (res.headersSent) {\n    return next(err);\n  }\n  let statusCode = 500;\n  let message = \"Internal server error\";\n  let errors: unknown;\n\n  if (err instanceof ApiError) {\n    statusCode = err.statusCode;\n    message = err.message;\n    errors = err.errors;\n  }\n\n  logger.error(\n    err,\n    `Error: ${message} | Status: ${statusCode} | Path: ${req.method} ${req.originalUrl}`\n  );\n\n  const response = {\n    success: false,\n    message,\n    statusCode,\n    ...(errors !== undefined && { errors }),\n    ...(env.NODE_ENV === \"development\" && { stack: err.stack })\n  };\n\n  res.status(statusCode).json(response);\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/forgot-password.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Verify your OTP</title>\r\n</head>\r\n\r\n<body\r\n  style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f7; margin: 0; padding: 0; color: #51545e;\">\r\n  <div style=\"background-color: #f4f4f7; padding: 40px 0;\">\r\n    <div\r\n      style=\"margin: 0 auto; background-color: #ffffff; padding: 40px 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px;\">\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <h1 style=\"color: #333333; font-size: 24px; font-weight: 700; margin: 0;\">\r\n          Forgot Password - Verify OTP\r\n        </h1>\r\n      </div>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 24px; color: #51545e;\">Hello <strong>\r\n          <%= name %>\r\n        </strong>,</p>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 30px; color: #51545e;\">\r\n        Thank you for using our service. Please use the following One-Time Password (OTP) to verify your password reset\r\n        request. This\r\n        code is valid for 5 minutes.\r\n      </p>\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <div\r\n          style=\"display: inline-block; padding: 16px 40px; background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;\">\r\n          <span\r\n            style=\"font-family: 'Courier New', Courier, monospace; font-size: 32px; font-weight: 700; letter-spacing: 6px; color: #1e293b;\">\r\n            <%= code %>\r\n          </span>\r\n        </div>\r\n      </div>\r\n\r\n      <p style=\"font-size: 14px; line-height: 1.6; color: #64748b; margin-top: 24px;\">\r\n        If you didn't request this request, you can safely ignore this email.\r\n      </p>\r\n\r\n      <div style=\"margin-top: 40px; padding-top: 24px; border-top: 1px solid #e2e8f0; text-align: center;\">\r\n        <p style=\"font-size: 12px; color: #94a3b8; margin: 0;\">\r\n          &copy; <%= new Date().getFullYear() %>. All rights reserved.\r\n        </p>\r\n      </div>\r\n\r\n    </div>\r\n  </div>\r\n</body>\r\n\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/email-verification.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Verify your email</title>\r\n</head>\r\n\r\n<body\r\n  style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f7; margin: 0; padding: 0; color: #51545e;\">\r\n  <div style=\"background-color: #f4f4f7; padding: 40px 0;\">\r\n    <div\r\n      style=\"margin: 0 auto; background-color: #ffffff; padding: 40px 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px;\">\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <h1 style=\"color: #333333; font-size: 24px; font-weight: 700; margin: 0;\">Verify your email</h1>\r\n      </div>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 24px; color: #51545e;\">Hello <strong>\r\n          <%= name %>\r\n        </strong>,</p>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 30px; color: #51545e;\">\r\n        Thank you for registering. Please use the following One-Time Password (OTP) to verify your email address. This\r\n        code is valid for 5 minutes.\r\n      </p>\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <div\r\n          style=\"display: inline-block; padding: 16px 40px; background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;\">\r\n          <span\r\n            style=\"font-family: 'Courier New', Courier, monospace; font-size: 32px; font-weight: 700; letter-spacing: 6px; color: #1e293b;\">\r\n            <%= code %>\r\n          </span>\r\n        </div>\r\n      </div>\r\n\r\n      <p style=\"font-size: 14px; line-height: 1.6; color: #64748b; margin-top: 24px;\">\r\n        If you didn't request this verification, you can safely ignore this email.\r\n      </p>\r\n\r\n      <div style=\"margin-top: 40px; padding-top: 24px; border-top: 1px solid #e2e8f0; text-align: center;\">\r\n        <p style=\"font-size: 12px; color: #94a3b8; margin: 0;\">\r\n          &copy; <%= new Date().getFullYear() %>. All rights reserved.\r\n        </p>\r\n      </div>\r\n\r\n    </div>\r\n  </div>\r\n</body>\r\n\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/delete-account.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Delete Account Request</title>\r\n  <style>\r\n    body {\r\n      font-family: Arial, sans-serif;\r\n      line-height: 1.6;\r\n      margin: 0;\r\n      padding: 20px;\r\n      color: #333;\r\n    }\r\n    p {\r\n      margin-bottom: 10px;\r\n    }\r\n    a {\r\n      color: #007bff;\r\n      text-decoration: none;\r\n    }\r\n    a:hover {\r\n      text-decoration: underline;\r\n    }\r\n  </style>\r\n</head>\r\n<body>\r\n  <p>Hello <%= name %>,</p>\r\n  <p>We received a request to delete your account. If you confirm this action, your account will be permanently deleted.</p>\r\n  <p>To confirm, please click the link below:</p>\r\n  <a href=\"<%= deleteAccountUrl %>\">Confirm Delete Account</a>\r\n  <p>If you did not request this action, please ignore this email or reply to let us know. Your account is still secure.</p>\r\n  <p>Thank you,</p>\r\n</body>\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/index.ts",
                          "content": "export * from \"./schemas/user.schema\";\n"
                        },
                        {
                          "type": "file",
                          "path": "src/docs/swagger.json",
                          "content": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"Hybrid Auth API\",\n    \"description\": \"Hybrid Auth API\",\n    \"version\": \"1.0.0\"\n  },\n  \"host\": \"localhost:9000/api\",\n  \"basePath\": \"/\",\n  \"schemes\": [\"http\"],\n  \"paths\": {\n    \"/verify-otp\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"example\": \"any\"\n                },\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                },\n                \"role\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/signin\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/profile\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      },\n      \"patch\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/refresh-token\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/logout\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/reset-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/change-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/delete-account\": {\n      \"delete\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/reactivate-account\": {\n      \"put\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/detailed\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/health/\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/health/detailed\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/verify-otp\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"example\": \"any\"\n                },\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                },\n                \"role\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/signin\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/profile\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      },\n      \"patch\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/refresh-token\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/logout\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/reset-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/change-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/delete-account\": {\n      \"delete\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/reactivate-account\": {\n      \"put\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/github\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/github/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/google\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/google/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/github\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/github/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/google\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/google/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    }\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/controllers/oauth.controller.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport { Profile as GithubProfile } from \"passport-github2\";\nimport { Profile as GoogleProfile } from \"passport-google-oauth20\";\nimport { Profile as FacebookProfile } from \"passport-facebook\";\n\nimport { ApiResponse } from \"../utils/api-response\";\nimport { AsyncHandler } from \"../utils/async-handler\";\nimport { ApiError } from \"../utils/api-error\";\nimport { OAuthService } from \"../services/oauth.service\";\nimport { setAuthCookies } from \"../helpers/cookie.helper\";\n\n//? LOGIN WITH GITHUB\nexport const githubOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as GithubProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authenticated failed!\"));\n    }\n\n    const user = {\n      provider: data?.provider as \"local\" | \"google\" | \"github\",\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails && data?.emails[0]?.value,\n      isEmailVerified: true,\n      avatar: data.photos && data.photos[0].value,\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(user, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    //? save the data into your databases\n\n    ApiResponse.ok(res, \"Signin Successfull\", {\n      user: {\n        id: existingUser.id,\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n\n//? LOGIN WITH GOOGLE\nexport const googleOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as GoogleProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authenticated failed!\"));\n    }\n\n    const userInfo = {\n      provider: data?.provider as \"local\" | \"google\" | \"github\",\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails && data?.emails[0]?.value,\n      isEmailVerified:\n        (data?.emails && data?.emails[0]?.verified === true) || true,\n      avatar: data.profileUrl || (data.photos && data.photos[0].value),\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    ApiResponse.ok(res, \"Signin Successfull\", {\n      user: {\n        id: existingUser.id,\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n\n//? LOGIN WITH FACEBOOK\nexport const facebookOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as FacebookProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authenticated failed!\"));\n    }\n\n    const userInfo = {\n      provider: data?.provider as \"local\" | \"google\" | \"github\",\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails && data?.emails[0]?.value,\n      isEmailVerified: true,\n      avatar: data.profileUrl || (data.photos && data.photos[0].value),\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    ApiResponse.ok(res, \"Signin Successfull\", {\n      user: {\n        id: existingUser.id,\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/controllers/health.controller.ts",
                          "content": "import { Request, Response } from \"express\";\nimport { ApiResponse } from \"../utils/api-response\";\nimport { AsyncHandler } from \"../utils/async-handler\";\n\n/**\n * Basic health check endpoint\n * GET /api/health\n */\nexport const healthCheck = AsyncHandler(\n  async (_req: Request, res: Response) => {\n    return ApiResponse.Success(res, \"Service is healthy\", {\n      status: \"healthy\",\n      timestamp: new Date().toISOString(),\n      uptime: process.uptime()\n    });\n  }\n);\n\n/**\n * Detailed health check with system information\n * GET /api/health/detailed\n */\nexport const detailedHealthCheck = AsyncHandler(\n  async (_req: Request, res: Response) => {\n    const healthData = {\n      status: \"healthy\",\n      timestamp: new Date().toISOString(),\n      uptime: process.uptime(),\n      environment: process.env.NODE_ENV || \"development\",\n      version: process.env.npm_package_version || \"1.0.0\",\n      memory: {\n        used:\n          Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) /\n          100,\n        total:\n          Math.round((process.memoryUsage().heapTotal / 1024 / 1024) * 100) /\n          100,\n        unit: \"MB\"\n      },\n      cpu: {\n        usage: process.cpuUsage()\n      }\n    };\n\n    return ApiResponse.Success(res, \"Service is healthy\", healthData);\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/controllers/auth.controller.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport { ApiResponse } from \"../utils/api-response\";\nimport { AsyncHandler } from \"../utils/async-handler\";\n\nimport { ApiError } from \"../utils/api-error\";\nimport { AuthService } from \"../services/auth.service\";\nimport {\n  clearAuthCookies,\n  setAuthCookies\n} from \"../helpers/cookie.helper\";\nimport { AvatarData, UserRequest } from \"../types/user\";\nimport {\n  deleteFileFromCloudinary,\n  uploadToCloudinary\n} from \"../services/cloudinary.service\";\nimport { DeleteAccountType, VerifyOtpType } from \"../validators/auth\";\nimport db from \"../configs/db\";\nimport { users } from \"../drizzle/schemas/user.schema\";\nimport { eq } from \"drizzle-orm\";\n\n//? SIGNUP USER\nexport const signupUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { name, email, password, role } = req.body;\n    if (!name || !email || !password) {\n      return next(ApiError.badRequest(\"Name, email and password are required\"));\n    }\n\n    await AuthService.registerUser({\n      name,\n      email,\n      password,\n      role\n    });\n\n    return ApiResponse.Success(\n      res,\n      \"User registered successfully. Please check your email for verification.\"\n    );\n  }\n);\n\n//? VERIFY USER\nexport const verifyUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, otpCode }: VerifyOtpType = req.body;\n\n    if (!email || !otpCode) {\n      return next(ApiError.badRequest(\"Email and code are required\"));\n    }\n\n    await AuthService.verifyUser({ email, otpCode });\n\n    return ApiResponse.ok(res, \"User verified successfully\");\n  }\n);\n\n//? SIGNIN USER\nexport const signinUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, password } = req.body;\n    if (!email || !password) {\n      return next(ApiError.badRequest(\"Email and password are required\"));\n    }\n\n    const ip = req.ip || \"Unknown\";\n    const userAgent = req.headers[\"user-agent\"] || \"Unknown\";\n\n    await AuthService.signinUser(\n      { email, password, ip, userAgent },\n      {\n        setAuthCookie: (\n          accessToken: string,\n          refreshToken: string,\n          sessionId: string\n        ) => {\n          setAuthCookies(res, accessToken, refreshToken, sessionId);\n        }\n      }\n    );\n\n    return ApiResponse.ok(res, \"User signed in successfully!\");\n  }\n);\n\n//? GET USER PROFILE\nexport const getUserProfile = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(userId.toString());\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    if (user.isDeleted) {\n      return next(ApiError.notFound(\"This account has been deactivated.\"));\n    }\n\n    const result = await AuthService.getUserSessions(\n      userId.toString(),\n      currentSessionId\n    );\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to get user sessions!\"));\n    }\n\n    return ApiResponse.ok(res, \"User profile fetched successfully\", {\n      user: {\n        id: user.id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        avatar: user.avatar,\n        isEmailVerified: user.isEmailVerified,\n        lastLoginAt: user.lastLoginAt,\n        sessions: result\n      }\n    });\n  }\n);\n\n//? UPDATE PROFILE\nexport const updateProfile = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const data = req.body;\n    const { name } = data;\n\n    if (!req.user?.id) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(req.user?.id.toString());\n\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    const avatar = user.avatar as AvatarData | null;\n    let updatedAvatar: AvatarData | null = avatar;\n\n    if (req?.file) {\n      const file = await uploadToCloudinary(req.file.buffer, {\n        folder: \"uploads/files\",\n        resource_type: \"auto\"\n      });\n      updatedAvatar = {\n        public_id: file.public_id,\n        url: file.url,\n        size: file.size\n      };\n    }\n\n    const updateData: Record<string, unknown> = {};\n    if (name) updateData.name = name;\n    if (updatedAvatar !== avatar) updateData.avatar = updatedAvatar;\n\n    if (Object.keys(updateData).length > 0) {\n      await db.update(users).set(updateData).where(eq(users.id, user.id));\n    }\n\n    if (\n      avatar?.public_id &&\n      avatar.public_id !== updatedAvatar?.public_id\n    ) {\n      await deleteFileFromCloudinary([avatar.public_id]);\n    }\n\n    const updatedUser = await AuthService.getUserProfile(user.id);\n\n    return ApiResponse.Success(res, \"Profile updated successfully!\", {\n      user: {\n        id: updatedUser?.id,\n        name: updatedUser?.name,\n        email: updatedUser?.email,\n        role: updatedUser?.role,\n        avatar: updatedUser?.avatar,\n        isEmailVerified: updatedUser?.isEmailVerified,\n        lastLoginAt: updatedUser?.lastLoginAt\n      }\n    });\n  }\n);\n\n//? REFRESH TOKENS\nexport const refreshToken = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const accessToken = req.cookies?.accessToken;\n    const refreshToken = req.cookies?.refreshToken;\n\n    const token = await AuthService.refreshTokens(accessToken, refreshToken);\n\n    if (!token) {\n      return next(ApiError.server(\"Failed to refresh tokens!\"));\n    }\n\n    const newAccessToken = token.accessToken;\n    const newRefreshToken = token.refreshToken;\n    setAuthCookies(res, newAccessToken, newRefreshToken, token.sessionId);\n\n    return ApiResponse.Success(res, \"Tokens refreshed successfully!\");\n  }\n);\n\n//? LOGOUT\nexport const logoutUser = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req.user?.id;\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const currentSessionId = req.user?.sessionId;\n    if (!currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.logoutUser(userId.toString(), currentSessionId);\n\n    clearAuthCookies(res);\n\n    return ApiResponse.Success(res, \"Logged out successfully!\");\n  }\n);\n\n//? FORGOT PASSWORD\nexport const forgotPassword = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email } = req.body;\n    if (!email) {\n      return next(ApiError.badRequest(\"Email is required!\"));\n    }\n\n    await AuthService.forgotPassword(email);\n\n    return ApiResponse.ok(\n      res,\n      \"If an account exists, a reset code has been sent to your email.\"\n    );\n  }\n);\n\n//? VERIFY RESET PASSWORD TOKEN\nexport const verifyResetPasswordOtp = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { otpCode, email } = req.body;\n    if (!otpCode || !email) {\n      return next(ApiError.badRequest(\"OtpCode and email are required!\"));\n    }\n\n    await AuthService.verifyResetPasswordOtp(otpCode, email);\n\n    return ApiResponse.ok(\n      res,\n      \"Password reset otp verified successfully. You can now reset your password.\"\n    );\n  }\n);\n\n//? RESET PASSWORD\nexport const resetPassword = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { newPassword, email } = req.body;\n    if (!email || !newPassword) {\n      return next(ApiError.badRequest(\"Newpassword and email are required!\"));\n    }\n\n    const result = await AuthService.resetPassword(next, email, newPassword);\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to reset password!\"));\n    }\n\n    clearAuthCookies(res);\n\n    return ApiResponse.ok(\n      res,\n      result.message || \"Password reset successfully!\"\n    );\n  }\n);\n\n//? CHANGE PASSWORD\nexport const changePassword = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const { oldPassword, newPassword } = req.body;\n\n    if (!oldPassword || !newPassword) {\n      return next(\n        ApiError.badRequest(\"Old password and new password are required\")\n      );\n    }\n\n    const result = await AuthService.changePassword(next, {\n      userId: userId.toString(),\n      oldPassword,\n      newPassword\n    });\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to change password!\"));\n    }\n\n    clearAuthCookies(res);\n\n    return ApiResponse.ok(\n      res,\n      result.message || \"Password changed successfully!\"\n    );\n  }\n);\n\n//? REQUEST DELETE ACCOUNT\nexport const requestDeleteAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n    const { password } = req.body;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    if (!password) {\n      return next(ApiError.badRequest(\"Password is required!\"));\n    }\n\n    await AuthService.requestDeleteAccount(userId, password);\n\n    return ApiResponse.ok(\n      res,\n      \"Account deletion request sent successfully. Please check your email to confirm.\"\n    );\n  }\n);\n\n//? DELETE/DEACTIVATE ACCOUNT\nexport const deleteAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { userId, type }: DeleteAccountType = req.body;\n\n    if (!userId || !type) {\n      return next(ApiError.badRequest(\"User id and type are required!\"));\n    }\n\n    const reqUserId = req?.user?.id;\n\n    if (!reqUserId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n    const token = req.query.token as string;\n    if (!token) {\n      return next(\n        ApiError.badRequest(\n          `${type === \"hard\" ? \"Delete\" : \"Deactivate\"} account token is required!`\n        )\n      );\n    }\n\n    if (userId !== reqUserId.toString()) {\n      return next(\n        ApiError.unauthorized(\"You are not authorized to perform this action\")\n      );\n    }\n\n    await AuthService.deleteOrDeactiveAccount({ userId, type, token });\n\n    clearAuthCookies(res);\n\n    return ApiResponse.Success(\n      res,\n      `Account ${type === \"soft\" ? \"deactivated\" : \"deleted\"} successfully!`\n    );\n  }\n);\n\n//? REACTIVATE ACCOUNT\nexport const reactivateAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.reactivateAccount(userId);\n\n    return ApiResponse.Success(res, \"Account reactivated successfully!\");\n  }\n);\n\n//? GET USER SESSIONS\nexport const getUserSessions = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const result = await AuthService.getUserSessions(\n      userId.toString(),\n      currentSessionId\n    );\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to get user sessions!\"));\n    }\n\n    return ApiResponse.ok(res, \"User sessions fetched successfully\", result);\n  }\n);\n\n//? DELETE SESSION\nexport const deleteUserSession = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n    const { sessionId } = req.params;\n\n    if (!userId || !sessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.deleteSession(userId, sessionId as string);\n\n    const reqSId = req.cookies?.sid;\n\n    const isCurrentSession = sessionId === reqSId;\n    if (isCurrentSession) {\n      clearAuthCookies(res);\n    }\n\n    return ApiResponse.Success(res, \"User session deleted successfully!\");\n  }\n);\n\n//? DELETE ALL SESSIONS\nexport const deleteAllUserSessions = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.deleteAllUserSessions(userId);\n\n    clearAuthCookies(res);\n\n    return ApiResponse.Success(res, \"User sessions deleted successfully!\");\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/constants/status-codes.ts",
                          "content": "export const STATUS_CODES = {\n  // 2xx Success\n  OK: 200,\n  CREATED: 201,\n  ACCEPTED: 202,\n  NO_CONTENT: 204,\n\n  // 3xx Redirection\n  MOVED_PERMANENTLY: 301,\n  FOUND: 302,\n  NOT_MODIFIED: 304,\n\n  // 4xx Client Errors\n  BAD_REQUEST: 400,\n  UNAUTHORIZED: 401,\n  FORBIDDEN: 403,\n  NOT_FOUND: 404,\n  CONFLICT: 409,\n  UNPROCESSABLE_ENTITY: 422,\n  TOO_MANY_REQUESTS: 429,\n\n  // 5xx Server Errors\n  INTERNAL_SERVER_ERROR: 500,\n  NOT_IMPLEMENTED: 501,\n  BAD_GATEWAY: 502,\n  SERVICE_UNAVAILABLE: 503,\n  GATEWAY_TIMEOUT: 504\n} as const;\n\nexport type StatusCode = (typeof STATUS_CODES)[keyof typeof STATUS_CODES];\n"
                        },
                        {
                          "type": "file",
                          "path": "src/constants/auth.ts",
                          "content": "export const OTP_MAX_ATTEMPTS = 5;\n\nexport const OTP_TYPES = [\n  \"signin\",\n  \"email-verification\",\n  \"password-reset\",\n  \"password-change\"\n] as const;\n\nexport const NEXT_OTP_DELAY = 1 * 60 * 1000; // 1 minute\n\nexport const LOGIN_MAX_ATTEMPTS = 5 as const;\n\nexport const OTP_CODE_LENGTH = 6 as const;\n\nexport const OTP_COOL_DOWN = 60;\n\nexport const OTP_EXPIRES_IN = 5 * 60 * 1000; // 5 minutes\n\nexport const OTP_SPAM_LOCK_TIME = 3600; // 1 hour\n\nexport const LOCK_TIME_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport const ACCESS_TOKEN_EXPIRY = 15 * 60 * 1000; // 15 minutes\n\nexport const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days\n\nexport const SESSION_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days\n\nexport const RESET_PASSWORD_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n\nexport const REACTIVATION_AVAILABLE_AT = 24 * 60 * 60 * 1000; // 24 hours\n\nexport const DELETE_ACCOUNT_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/swagger.ts",
                          "content": "import swaggerUi from \"swagger-ui-express\";\nimport { Express } from \"express\";\nimport env from \"../configs/env\";\n\nimport swaggerDocument from \"../docs/swagger.json\";\n\nexport const setupSwagger = (app: Express) => {\n  if (env.NODE_ENV !== \"development\") return;\n\n  app.use(\"/api/docs\", swaggerUi.serve, swaggerUi.setup(swaggerDocument));\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/resend.ts",
                          "content": "import { Resend } from \"resend\";\nimport env from \"./env\";\n\nexport const resend = new Resend(env.RESEND_API_KEY);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/redis.ts",
                          "content": "import { createClient } from \"redis\";\nimport { env } from \"./env\";\n\nconst redisClient = createClient({\n  url: env.REDIS_URL\n});\n\nexport default redisClient;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/passport.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport passport from \"passport\";\nimport {\n  Strategy as GitHubStrategy,\n  Profile as GithubProfile\n} from \"passport-github2\";\n\nimport {\n  Strategy as GoogleStrategy,\n  Profile as GoogleProfile\n} from \"passport-google-oauth20\";\n\nimport {\n  Strategy as FacebookStrategy,\n  Profile as FacebookProfile\n} from \"passport-facebook\"; // npm i --save-dev @types/passport-facebook\n\nimport env from \"./env\";\n\n//? GITHUB STRATEGY\npassport.use(\n  new GitHubStrategy(\n    {\n      clientID: env.GITHUB_CLIENT_ID,\n      clientSecret: env.GITHUB_CLIENT_SECRET,\n      callbackURL: env.GITHUB_REDIRECT_URI\n    },\n    function (\n      accessToken: string,\n      refreshToken: string,\n      profile: GithubProfile,\n      cb: (error: Error | null, user?: any) => void\n    ) {\n      // console.log({ profile });\n      return cb(null, profile);\n    }\n  )\n);\n\n//? GOOGLE STRATEGY\npassport.use(\n  new GoogleStrategy(\n    {\n      clientID: env.GOOGLE_CLIENT_ID,\n      clientSecret: env.GOOGLE_CLIENT_SECRET,\n      callbackURL: env.GOOGLE_REDIRECT_URI\n    },\n    function (accessToken, refreshToken, profile: GoogleProfile, cb) {\n      return cb(null, profile);\n    }\n  )\n);\n\n//? FACEBOOK STRATEGY\npassport.use(\n  new FacebookStrategy(\n    {\n      clientID: env.FACEBOOK_APP_ID,\n      clientSecret: env.FACEBOOK_APP_SECRET,\n      callbackURL: env.FACEBOOK_REDIRECT_URI\n    },\n    function (accessToken, refreshToken, profile: FacebookProfile, cb) {\n      // console.log({ profile });\n      return cb(null, profile);\n    }\n  )\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/env.ts",
                          "content": "/* eslint-disable no-console */\nimport \"dotenv-flow/config\";\nimport { z } from \"zod\";\n\nexport const envSchema = z.object({\n  NODE_ENV: z\n    .enum([\"development\", \"test\", \"production\"])\n    .default(\"development\"),\n\n  PORT: z.string().regex(/^\\d+$/, \"PORT must be a number\").transform(Number),\n\n  DATABASE_URL: z.url(),\n\n  CORS_ORIGIN: z.string(),\n  CLIENT_URL: z.url(),\n\n  LOG_LEVEL: z\n    .enum([\"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\"])\n    .default(\"info\"),\n\n  JWT_ACCESS_SECRET: z.string().min(32),\n  JWT_REFRESH_SECRET: z.string().min(32),\n\n  CRYPTO_SECRET: z.string().min(32),\n\n  RESEND_API_KEY: z.string(),\n  EMAIL_FROM: z.email(),\n\n  CLOUDINARY_CLOUD_NAME: z.string(),\n  CLOUDINARY_API_KEY: z.string(),\n  CLOUDINARY_API_SECRET: z.string(),\n\n  GOOGLE_CLIENT_ID: z.string(),\n  GOOGLE_CLIENT_SECRET: z.string(),\n  GOOGLE_REDIRECT_URI: z.url(),\n\n  GITHUB_CLIENT_ID: z.string(),\n  GITHUB_CLIENT_SECRET: z.string(),\n  GITHUB_REDIRECT_URI: z.url(),\n\n  FACEBOOK_APP_ID: z.string(),\n  FACEBOOK_APP_SECRET: z.string(),\n  FACEBOOK_REDIRECT_URI: z.url(),\n\n  REDIS_URL: z.url()\n});\n\nexport type Env = z.infer<typeof envSchema>;\n\nconst result = envSchema.safeParse(process.env);\n\nif (!result.success) {\n  console.error(\"❌ Invalid environment configuration\");\n  console.error(z.treeifyError(result.error));\n  process.exit(1);\n}\n\nexport const env: Readonly<Env> = Object.freeze(result.data);\n\nexport default env;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/db.ts",
                          "content": "import { drizzle } from \"drizzle-orm/node-postgres\";\nimport env from \"./env\";\nimport * as schema from \"../drizzle\";\n\nconst db = drizzle(env.DATABASE_URL!, {\n  schema,\n  logger: env.NODE_ENV === \"development\"\n});\n\nexport default db;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/cloudinary.ts",
                          "content": "import { v2 as cloudinary } from \"cloudinary\";\nimport env from \"./env\";\n\ncloudinary.config({\n  cloud_name: env.CLOUDINARY_CLOUD_NAME,\n  api_key: env.CLOUDINARY_API_KEY,\n  api_secret: env.CLOUDINARY_API_SECRET\n});\n\nexport default cloudinary;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/schemas/user.schema.ts",
                          "content": "import { \n  pgTable, \n  text, \n  timestamp, \n  boolean, \n  integer,\n  json\n} from \"drizzle-orm/pg-core\";\nimport { createId } from \"@paralleldrive/cuid2\";\n\nexport const users = pgTable(\"users\", {\n  id: text(\"id\").primaryKey().$defaultFn(() => createId()),\n  name: text(\"name\").notNull(),\n  email: text(\"email\").notNull().unique(),\n  password: text(\"password\"),\n  role: text(\"role\", { enum: [\"user\", \"admin\"] }).default(\"user\").notNull(),\n  isEmailVerified: boolean(\"is_email_verified\").default(false).notNull(),\n  lastLoginAt: timestamp(\"last_login_at\"),\n  failedLoginAttempts: integer(\"failed_login_attempts\").default(0).notNull(),\n  lockUntil: timestamp(\"lock_until\"),\n  avatar: json(\"avatar\"), // { public_id: string, url: string, size: number }\n  \n  provider: text(\"provider\", { enum: [\"local\", \"google\", \"github\"] }).default(\"local\").notNull(),\n  providerId: text(\"provider_id\"),\n  \n  isDeleted: boolean(\"is_deleted\").default(false).notNull(),\n  deletedAt: timestamp(\"deleted_at\"),\n  reActivateAvailableAt: timestamp(\"re_activate_available_at\"),\n  \n  createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n  updatedAt: timestamp(\"updated_at\").defaultNow().notNull(),\n});\n\nexport type User = typeof users.$inferSelect;\nexport type NewUser = typeof users.$inferInsert;\n"
                        }
                      ]
                    },
                    "feature": {
                      "files": [
                        {
                          "type": "file",
                          "path": "swagger.config.ts",
                          "content": "import swaggerAutoGen from \"swagger-autogen\";\n\nconst doc = {\n  info: {\n    title: \"Hybrid Auth API\",\n    description: \"Hybrid Auth API\",\n    version: \"1.0.0\"\n  },\n  host: \"localhost:9000/api\",\n  schemes: [\"http\"]\n};\n\nconst outputFile = \"./src/docs/swagger.json\";\nconst endpointsFiles = [\"./src/routes/*.ts\"];\n\nswaggerAutoGen(outputFile, endpointsFiles, doc);\n"
                        },
                        {
                          "type": "file",
                          "path": "README.md",
                          "content": "# Hybrid Auth PostgreSQL Feature\n\nMinimal Node.js + Express + TypeScript Feature starter using PostgreSQL, Drizzle ORM, and hybrid authentication (local and OAuth via Passport or similar).\n\n## Features\n\n- Express + TypeScript Feature structure\n- PostgreSQL + Drizzle integration\n- Hybrid auth: local credentials and OAuth providers (e.g., Google, GitHub, Facebook)\n- Session-based authentication compatible with production\n- Environment-driven configuration with `.env`\n- Dev and production scripts\n\n## What This Provides\n\n- A clean starting point for credential and OAuth login\n- Prewired Express app with routing and session middleware\n- PostgreSQL connection wiring ready for your schema and data models\n- TypeScript configuration and scripts for iterative dev and production builds\n- Example environment keys you can enable as needed\n\n## Quick Start\n\n1. Install dependencies:\n   - `npm install`\n2. Configure environment:\n   - Create `.env` (copy from `.env.example` if present).\n   - Set variables shown below.\n3. Run in development:\n   - `npm run dev`\n4. Build and run in production:\n   - `npm run build`\n   - `npm start`\n\n## Requirements\n\n- Node.js 18+\n- PostgreSQL\n\n## Environment Variables\n\n- `DATABASE_URL` — PostgreSQL connection string\n- `REDIS_URL` — Redis connection string for session storage\n- `PORT` — server port (e.g., 3000)\n- `NODE_ENV` — `development` or `production`\n- `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`, `CRYPTO_SECRET`\n- `EMAIL_FROM`, `RESEND_API_KEY`\n- Optional OAuth (enable what you use):\n  - `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI`\n  - `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GITHUB_REDIRECT_URI`\n  - `FACEBOOK_APP_ID`, `FACEBOOK_APP_SECRET`, `FACEBOOK_REDIRECT_URI`\n\n## Scripts\n\n- `npm run dev` — start development server\n- `npm run build` — compile TypeScript\n- `npm start` — start compiled app\n\n## Notes\n\n- Never commit `.env` or secrets.\n- Run your Drizzle migrations before starting the app.\n"
                        },
                        {
                          "type": "file",
                          "path": "package.json",
                          "content": "{\n  \"name\": \"servercn-hybrid-auth\",\n  \"version\": \"1.0.0\",\n  \"main\": \"dist/server.js\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development npx tsx watch src/server.ts\",\n    \"build\": \"rm -rf dist && tsc && tsc-alias\",\n    \"start\": \"cross-env NODE_ENV=production node dist/server.js\",\n    \"db:generate\": \"drizzle-kit generate\",\n    \"db:migrate\": \"drizzle-kit migrate\",\n    \"db:push\": \"drizzle-kit push\",\n    \"db:studio\": \"drizzle-kit studio\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"docs\": \"npx tsx swagger.config.ts\",\n    \"prepare\": \"husky\",\n    \"lint:check\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"format:check\": \"npx prettier . --check\",\n    \"format:fix\": \"npx prettier . --write\"\n  },\n  \"lint-staged\": {\n    \"src/**/*.ts\": [\n      \"eslint --fix\",\n      \"prettier --write\",\n      \"tsc --noEmit\"\n    ]\n  },\n  \"dependencies\": {},\n  \"devDependencies\": {}\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "drizzle.config.ts",
                          "content": "import { Config, defineConfig } from \"drizzle-kit\";\nimport env from \"./src/shared/configs/env\";\n\nexport default defineConfig({\n  out: \"./src/drizzle/migrations\",\n  schema: \"./src/drizzle/schemas/*\",\n  dialect: \"postgresql\",\n  dbCredentials: {\n    url: env.DATABASE_URL!\n  },\n  verbose: true,\n  strict: true\n}) satisfies Config;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/server.ts",
                          "content": "import app from \"./app\";\nimport env from \"./shared/configs/env\";\nimport redisClient from \"./shared/configs/redis\";\nimport { logger } from \"./shared/utils/logger\";\nimport { configureGracefulShutdown } from \"./shared/utils/shutdown\";\n\nconst port = env.PORT || 9000;\n\nredisClient\n  .connect()\n  .then(() => {\n    logger.info(\"Redis Connection Success\");\n    const server = app.listen(port, () => {\n      logger.info(`[server]: Server is running at http://localhost:${port}`);\n      logger.info(`[server]: Environment: ${env.NODE_ENV}`);\n      logger.info(\n        `[server]: Swagger docs are available at http://localhost:${port}/api/docs`\n      );\n    });\n\n    configureGracefulShutdown(server);\n  })\n  .catch((error: Error) => {\n    logger.error(error, \"Redis Connection Failed\");\n    process.exit(1);\n  });\n"
                        },
                        {
                          "type": "file",
                          "path": "src/app.ts",
                          "content": "import express, { Express, Request, Response } from \"express\";\nimport cookieParser from \"cookie-parser\";\nimport morgan from \"morgan\";\nimport { notFoundHandler } from \"./shared/middlewares/not-found-handler\";\nimport { errorHandler } from \"./shared/middlewares/error-handler\";\nimport env from \"./shared/configs/env\";\nimport { configureSecurityHeaders } from \"./shared/middlewares/security-header\";\n\nimport Routes from \"./routes/index\";\n\nimport \"./shared/configs/passport\";\n\nimport sourceMapSupport from \"source-map-support\";\nimport { setupSwagger } from \"./shared/configs/swagger\";\nsourceMapSupport.install();\n\nconst app: Express = express();\n\n//? Apply security headers before other middlewares and routes\nconfigureSecurityHeaders(app);\n\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\napp.use(cookieParser());\napp.use(morgan(env.NODE_ENV === \"development\" ? \"dev\" : \"combined\"));\n\n//? Swagger Setup\nsetupSwagger(app);\n\n//? Routes\napp.get(\"/\", (req: Request, res: Response) => {\n  res.redirect(\"/api/v1/health\");\n});\n\napp.use(\"/api\", Routes);\n\n//? Not-found-handler (should be after routes)\napp.use(notFoundHandler);\n\n//? Global error handler (should be last)\napp.use(errorHandler);\n\nexport default app;\n"
                        },
                        {
                          "type": "file",
                          "path": ".husky/pre-commit",
                          "content": "npx lint-staged\n"
                        },
                        {
                          "type": "file",
                          "path": "src/types/global.d.ts",
                          "content": "import { Request } from \"express\";\n\nexport interface UserRequest extends Request {\n  user?: {\n    _id?: string | undefined;\n    id?: string | undefined;\n    role?: \"user\" | \"admin\" | undefined;\n    sessionId?: string | undefined;\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/index.ts",
                          "content": "import { Router } from \"express\";\nimport healthRoutes from \"../modules/health/health.routes\";\nimport authRoutes from \"../modules/auth/auth.routes\";\nimport oauthRoutes from \"../modules/oauth/oauth.routes\";\n\nconst router = Router();\n\nrouter.use(\"/v1/health\", healthRoutes);\nrouter.use(\"/v1/auth\", authRoutes);\nrouter.use(\"/auth\", oauthRoutes); //* Here versioning is not given because, in google and github callback routes, we are not using versioning. process.env.GOOGLE_REDIRECT_URI\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/forgot-password.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Verify your OTP</title>\r\n</head>\r\n\r\n<body\r\n  style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f7; margin: 0; padding: 0; color: #51545e;\">\r\n  <div style=\"background-color: #f4f4f7; padding: 40px 0;\">\r\n    <div\r\n      style=\"margin: 0 auto; background-color: #ffffff; padding: 40px 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px;\">\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <h1 style=\"color: #333333; font-size: 24px; font-weight: 700; margin: 0;\">\r\n          Forgot Password - Verify OTP\r\n        </h1>\r\n      </div>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 24px; color: #51545e;\">Hello <strong>\r\n          <%= name %>\r\n        </strong>,</p>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 30px; color: #51545e;\">\r\n        Thank you for using our service. Please use the following One-Time Password (OTP) to verify your password reset\r\n        request. This\r\n        code is valid for 5 minutes.\r\n      </p>\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <div\r\n          style=\"display: inline-block; padding: 16px 40px; background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;\">\r\n          <span\r\n            style=\"font-family: 'Courier New', Courier, monospace; font-size: 32px; font-weight: 700; letter-spacing: 6px; color: #1e293b;\">\r\n            <%= code %>\r\n          </span>\r\n        </div>\r\n      </div>\r\n\r\n      <p style=\"font-size: 14px; line-height: 1.6; color: #64748b; margin-top: 24px;\">\r\n        If you didn't request this request, you can safely ignore this email.\r\n      </p>\r\n\r\n      <div style=\"margin-top: 40px; padding-top: 24px; border-top: 1px solid #e2e8f0; text-align: center;\">\r\n        <p style=\"font-size: 12px; color: #94a3b8; margin: 0;\">\r\n          &copy; <%= new Date().getFullYear() %>. All rights reserved.\r\n        </p>\r\n      </div>\r\n\r\n    </div>\r\n  </div>\r\n</body>\r\n\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/email-verification.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Verify your email</title>\r\n</head>\r\n\r\n<body\r\n  style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f7; margin: 0; padding: 0; color: #51545e;\">\r\n  <div style=\"background-color: #f4f4f7; padding: 40px 0;\">\r\n    <div\r\n      style=\"margin: 0 auto; background-color: #ffffff; padding: 40px 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px;\">\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <h1 style=\"color: #333333; font-size: 24px; font-weight: 700; margin: 0;\">Verify your email</h1>\r\n      </div>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 24px; color: #51545e;\">Hello <strong>\r\n          <%= name %>\r\n        </strong>,</p>\r\n\r\n      <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 30px; color: #51545e;\">\r\n        Thank you for registering. Please use the following One-Time Password (OTP) to verify your email address. This\r\n        code is valid for 5 minutes.\r\n      </p>\r\n\r\n      <div style=\"text-align: center; margin-bottom: 30px;\">\r\n        <div\r\n          style=\"display: inline-block; padding: 16px 40px; background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;\">\r\n          <span\r\n            style=\"font-family: 'Courier New', Courier, monospace; font-size: 32px; font-weight: 700; letter-spacing: 6px; color: #1e293b;\">\r\n            <%= code %>\r\n          </span>\r\n        </div>\r\n      </div>\r\n\r\n      <p style=\"font-size: 14px; line-height: 1.6; color: #64748b; margin-top: 24px;\">\r\n        If you didn't request this verification, you can safely ignore this email.\r\n      </p>\r\n\r\n      <div style=\"margin-top: 40px; padding-top: 24px; border-top: 1px solid #e2e8f0; text-align: center;\">\r\n        <p style=\"font-size: 12px; color: #94a3b8; margin: 0;\">\r\n          &copy; <%= new Date().getFullYear() %>. All rights reserved.\r\n        </p>\r\n      </div>\r\n\r\n    </div>\r\n  </div>\r\n</body>\r\n\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/email-templates/delete-account.ejs",
                          "content": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>Delete Account Request</title>\r\n  <style>\r\n    body {\r\n      font-family: Arial, sans-serif;\r\n      line-height: 1.6;\r\n      margin: 0;\r\n      padding: 20px;\r\n      color: #333;\r\n    }\r\n    p {\r\n      margin-bottom: 10px;\r\n    }\r\n    a {\r\n      color: #007bff;\r\n      text-decoration: none;\r\n    }\r\n    a:hover {\r\n      text-decoration: underline;\r\n    }\r\n  </style>\r\n</head>\r\n<body>\r\n  <p>Hello <%= name %>,</p>\r\n  <p>We received a request to delete your account. If you confirm this action, your account will be permanently deleted.</p>\r\n  <p>To confirm, please click the link below:</p>\r\n  <a href=\"<%= deleteAccountUrl %>\">Confirm Delete Account</a>\r\n  <p>If you did not request this action, please ignore this email or reply to let us know. Your account is still secure.</p>\r\n  <p>Thank you,</p>\r\n</body>\r\n</html>"
                        },
                        {
                          "type": "file",
                          "path": "src/docs/swagger.json",
                          "content": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"Hybrid Auth API\",\n    \"description\": \"Hybrid Auth API\",\n    \"version\": \"1.0.0\"\n  },\n  \"host\": \"localhost:9000/api\",\n  \"basePath\": \"/\",\n  \"schemes\": [\"http\"],\n  \"paths\": {\n    \"/verify-otp\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"example\": \"any\"\n                },\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                },\n                \"role\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/signin\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/profile\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      },\n      \"patch\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/refresh-token\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/logout\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/reset-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/change-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/delete-account\": {\n      \"delete\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/reactivate-account\": {\n      \"put\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/detailed\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/health/\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/health/detailed\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/verify-otp\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"example\": \"any\"\n                },\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                },\n                \"role\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/signin\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"password\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/profile\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      },\n      \"patch\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/refresh-token\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/logout\": {\n      \"post\": {\n        \"description\": \"\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/reset-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/change-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/delete-account\": {\n      \"delete\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/reactivate-account\": {\n      \"put\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/github\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/github/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/google\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/auth/google/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/github\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/github/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/google\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/google/callback\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    }\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/index.ts",
                          "content": "export * from \"./schemas/user.schema\";\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/shutdown.ts",
                          "content": "import { Server } from \"http\";\nimport { logger } from \"./logger\";\n\nexport const configureGracefulShutdown = (server: Server) => {\n  const signals = [\"SIGTERM\", \"SIGINT\"];\n\n  signals.forEach(signal => {\n    process.on(signal, () => {\n      logger.info(`\\n${signal} signal received. Shutting down gracefully...`);\n\n      server.close(err => {\n        if (err) {\n          logger.error(err, \"Error during server close\");\n          process.exit(1);\n        }\n\n        logger.info(\"HTTP server closed.\");\n        process.exit(0);\n      });\n\n      // Force shutdown after 10 seconds\n      setTimeout(() => {\n        logger.error(\n          \"Could not close connections in time, forcefully shutting down\"\n        );\n        process.exit(1);\n      }, 10000);\n    });\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/send-mail.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport env from \"../configs/env\";\nimport { resend } from \"../configs/resend\";\nimport { renderEmailTemplates } from \"./render-email-template\";\n\nexport type SendMailType = {\n  from?: string;\n  subject: string;\n  data: Record<string, any>;\n  email: string;\n  html?: string;\n  templateName: string;\n};\n\nexport async function sendEmail({\n  from,\n  email,\n  subject,\n  data,\n  html,\n  templateName\n}: SendMailType) {\n  const htmlContent =\n    (await renderEmailTemplates(templateName, data)) || html || \"\";\n\n  return await resend.emails.send({\n    from: from || env.EMAIL_FROM,\n    to: email,\n    subject,\n    replyTo: email,\n    html: htmlContent\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/render-email-template.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport ejs from \"ejs\"; // npm i --save-dev @types/ejs\nimport path from \"node:path\";\n\nexport async function renderEmailTemplates(\n  templateName: string,\n  data: Record<string, any>\n) {\n  const templatePath = path.join(\n    process.cwd(),\n    \"src\",\n    \"email-templates\",\n    `${templateName}.ejs`\n  );\n  return ejs.renderFile(templatePath, data);\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/logger.ts",
                          "content": "import pino from \"pino\";\nimport env from \"../configs/env\";\n\nexport const logger = pino({\n  level: env.LOG_LEVEL,\n  transport:\n    env.NODE_ENV !== \"production\"\n      ? {\n          target: \"pino-pretty\",\n          options: {\n            colorize: true,\n            translateTime: \"yyyy-mm-dd HH:MM:ss\",\n            ignore: \"pid,hostname\"\n          }\n        }\n      : undefined\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/jwt.ts",
                          "content": "import jwt from \"jsonwebtoken\";\nimport env from \"../configs/env\";\n\nconst JWT_ACCESS_TOKEN_EXPIRY = \"15m\";\nconst JWT_REFRESH_TOKEN_EXPIRY = \"7d\";\n\nexport function generateAccessToken(user: {\n  _id: string;\n  role: \"user\" | \"admin\";\n  sessionId: string;\n}) {\n  return jwt.sign(\n    { _id: user._id, role: user.role, sessionId: user.sessionId },\n    env.JWT_ACCESS_SECRET!,\n    {\n      expiresIn: JWT_ACCESS_TOKEN_EXPIRY\n    }\n  );\n}\n\nexport function generateRefreshToken(user: { _id: string; sessionId: string }) {\n  return jwt.sign(\n    { _id: user._id, sessionId: user.sessionId },\n    env.JWT_REFRESH_SECRET!,\n    {\n      expiresIn: JWT_REFRESH_TOKEN_EXPIRY\n    }\n  );\n}\n\nexport function verifyAccessToken(token: string) {\n  return jwt.verify(token, env.JWT_ACCESS_SECRET!) as {\n    _id: string;\n    role: \"user\" | \"admin\";\n    sessionId: string;\n  };\n}\n\nexport function verifyRefreshToken(token: string) {\n  return jwt.verify(token, env.JWT_REFRESH_SECRET!) as {\n    _id: string;\n    sessionId: string;\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/date.ts",
                          "content": "export function getRemainingTime(date: Date) {\n  const now = new Date();\n  let diff = date.getTime() - now.getTime();\n\n  if (diff <= 0) {\n    return {\n      days: 0,\n      minutes: 0,\n      seconds: 0\n    };\n  }\n\n  const seconds = Math.floor((diff / 1000) % 60);\n  const minutes = Math.floor((diff / (1000 * 60)) % 60);\n  const days = Math.floor(diff / (1000 * 60 * 60 * 24));\n\n  return {\n    days,\n    minutes,\n    seconds\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/async-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\n\nexport type AsyncRouteHandler = (\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => Promise<unknown>;\n\nexport function AsyncHandler(fn: AsyncRouteHandler) {\n  return function (req: Request, res: Response, next: NextFunction) {\n    Promise.resolve()\n      .then(() => fn(req, res, next))\n      .catch(next);\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/api-response.ts",
                          "content": "import { STATUS_CODES, StatusCode } from \"../constants/status-codes\";\nimport type { Response } from \"express\";\n\ntype ApiResponseParams<T> = {\n  success: boolean;\n  message: string;\n  statusCode: StatusCode;\n  data?: T | null;\n  errors?: unknown;\n};\n\nexport class ApiResponse<T = unknown> {\n  public readonly success: boolean;\n  public readonly message: string;\n  public readonly statusCode: StatusCode;\n  public readonly data?: T | null;\n  public readonly errors?: unknown;\n\n  constructor({\n    success,\n    message,\n    statusCode,\n    data,\n    errors\n  }: ApiResponseParams<T>) {\n    this.success = success;\n    this.message = message;\n    this.statusCode = statusCode;\n    this.data = data;\n    this.errors = errors;\n  }\n\n  send(res: Response): Response {\n    return res.status(this.statusCode).json({\n      success: this.success,\n      message: this.message,\n      statusCode: this.statusCode,\n      ...(this.data !== undefined && { data: this.data }),\n      ...(this.errors !== undefined && { errors: this.errors })\n    });\n  }\n\n  static Success<T>(\n    res: Response,\n    message: string,\n    data?: T,\n    statusCode: StatusCode = STATUS_CODES.OK\n  ): Response {\n    return new ApiResponse<T>({\n      success: true,\n      message,\n      data,\n      statusCode\n    }).send(res);\n  }\n\n  static ok<T>(res: Response, message = \"OK\", data?: T) {\n    return ApiResponse.Success(res, message, data, STATUS_CODES.OK);\n  }\n\n  static created<T>(res: Response, message = \"Created\", data?: T) {\n    return ApiResponse.Success(res, message, data, STATUS_CODES.CREATED);\n  }\n}\n\n/*\n * Usage:\n * ApiResponse.ok(res, \"OK\", data);\n * ApiResponse.created(res, \"Created\", data);\n */\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/api-error.ts",
                          "content": "import { STATUS_CODES, StatusCode } from \"../constants/status-codes\";\n\nexport class ApiError extends Error {\n  public readonly statusCode: StatusCode;\n  public readonly isOperational: boolean;\n  public readonly errors?: unknown;\n\n  constructor(\n    statusCode: StatusCode,\n    message: string,\n    errors?: unknown,\n    isOperational = true\n  ) {\n    super(message);\n    this.name = \"ApiError\";\n    this.statusCode = statusCode;\n    this.errors = errors;\n    this.isOperational = isOperational;\n\n    Error.captureStackTrace(this, this.constructor);\n  }\n\n  static badRequest(message = \"Bad Request\", errors?: unknown) {\n    return new ApiError(STATUS_CODES.BAD_REQUEST, message, errors);\n  }\n\n  static unauthorized(message = \"Unauthorized\") {\n    return new ApiError(STATUS_CODES.UNAUTHORIZED, message);\n  }\n\n  static forbidden(message = \"Forbidden\") {\n    return new ApiError(STATUS_CODES.FORBIDDEN, message);\n  }\n\n  static notFound(message = \"Not Found\") {\n    return new ApiError(STATUS_CODES.NOT_FOUND, message);\n  }\n\n  static conflict(message = \"Conflict\") {\n    return new ApiError(STATUS_CODES.CONFLICT, message);\n  }\n\n  static server(message = \"Internal Server Error\") {\n    return new ApiError(STATUS_CODES.INTERNAL_SERVER_ERROR, message);\n  }\n\n  static unprocessableEntity(message = \"Unprocessable Entity\") {\n    return new ApiError(STATUS_CODES.UNPROCESSABLE_ENTITY, message);\n  }\n\n  static tooManyRequests(message = \"Too Many Requests\") {\n    return new ApiError(STATUS_CODES.TOO_MANY_REQUESTS, message);\n  }\n}\n\n/*\n  ? Usage:\n  * throw new ApiError(404, \"Not found\");\n  * throw ApiError.badRequest(\"Bad request\");\n */\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/verify-auth.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Response } from \"express\";\nimport { verifyAccessToken } from \"../utils/jwt\";\nimport { ApiError } from \"../utils/api-error\";\nimport redisClient from \"../configs/redis\";\nimport { UserRequest } from \"../../types/global\";\nimport { SessionData } from \"../../modules/auth/auth.types\";\n\nexport async function verifyAuthentication(\n  req: UserRequest,\n  _res: Response,\n  next: NextFunction\n): Promise<void> {\n  const authHeader = req.headers.authorization || \"\";\n  const token = authHeader.startsWith(\"Bearer \")\n    ? authHeader.split(\" \")[1]\n    : null;\n\n  const accessToken = req.cookies?.accessToken || token;\n  if (!accessToken) {\n    return next(ApiError.unauthorized(\"Missing access token\"));\n  }\n\n  try {\n    const decoded = verifyAccessToken(accessToken);\n\n    const sessionKey = `session:${decoded.sessionId}`;\n    const sessionData = await redisClient.get(sessionKey);\n    if (!sessionData) {\n      return next(ApiError.unauthorized(\"Session not found\"));\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.ip !== req.ip) {\n      return next(ApiError.unauthorized(\"Suspicious session\"));\n    }\n\n    if (session.userAgent !== req.headers[\"user-agent\"]) {\n      return next(ApiError.unauthorized(\"Suspicious session\"));\n    }\n\n    if (session.expiresAt < new Date()) {\n      return next(ApiError.unauthorized(\"Session expired\"));\n    }\n\n    req.user = decoded;\n    return next();\n  } catch (err: any) {\n    if (err.name === \"TokenExpiredError\") {\n      return next(ApiError.unauthorized(\"Access token expired\"));\n    }\n    return next(ApiError.unauthorized(\"Invalid access token\"));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/validate-request.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { Request, Response, NextFunction } from \"express\";\nimport z, { ZodError, type ZodObject } from \"zod\";\n\nimport { ApiError } from \"../utils/api-error\";\n\nexport const validateRequest = (schema: ZodObject<any>) => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    try {\n      schema.parse(req.body);\n\n      next();\n    } catch (error) {\n      if (!(error instanceof ZodError)) {\n        return next(error);\n      }\n\n      return next(\n        ApiError.badRequest(\n          \"Invalid request data\",\n          z.flattenError(error).fieldErrors || z.flattenError(error)\n        )\n      );\n    }\n  };\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/validate-id.ts",
                          "content": "import { ApiError } from \"../utils/api-error\";\nimport { NextFunction, Request, Response } from \"express\";\n\nexport const validateObjectId = (paramName: string = \"id\") => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const value =\n      req?.params[paramName] || req?.body[paramName] || req?.query[paramName];\n    if (!value || typeof value !== \"string\" || value.trim().length === 0) {\n      throw ApiError.badRequest(`Invalid ${paramName}`);\n    }\n\n    next();\n  };\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/user-account-restriction.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Response } from \"express\";\nimport { UserRequest } from \"../../types/global\";\nimport db from \"../configs/db\";\nimport { users } from \"../../drizzle/schemas/user.schema\";\nimport { eq } from \"drizzle-orm\";\nimport { ApiError } from \"../utils/api-error\";\nimport { logger } from \"../utils/logger\";\nimport { getRemainingTime } from \"../utils/date\";\n\nexport async function checkUserAccountRestriction(\n  req: UserRequest,\n  _res: Response,\n  next: NextFunction\n): Promise<void> {\n  try {\n    if (!req.user?._id) {\n      return next(ApiError.unauthorized(\"Unauthorized\"));\n    }\n\n    const user = await db.query.users.findFirst({\n      where: eq(users.id, req.user._id)\n    });\n\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n    }\n\n    if (user.isDeleted || user.deletedAt) {\n      return next(ApiError.forbidden(\"Your account has been deactivated.\"));\n    }\n\n    if (user.lockUntil && user.lockUntil.getTime() > Date.now()) {\n      const remainingTime = getRemainingTime(user.lockUntil);\n\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${remainingTime.minutes} minutes and ${remainingTime.seconds} seconds.`\n        )\n      );\n    }\n\n    if (!user.isEmailVerified) {\n      return next(\n        ApiError.forbidden(\"Email not verified. Please verify your email.\")\n      );\n    }\n\n    return next();\n  } catch (err: any) {\n    logger.error(err?.message || err);\n    return next(ApiError.server(\"Something went wrong\"));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/upload-file.ts",
                          "content": "import multer from \"multer\";\n\nexport const ALLOWED_FILE_TYPES = [\n  \"image/jpeg\",\n  \"image/png\",\n  \"image/webp\",\n  \"video/mp4\",\n  \"video/mpeg\",\n  \"video/quicktime\",\n  \"application/pdf\"\n];\n\nexport const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB\n\nconst storage = multer.memoryStorage();\n\nconst fileFilter: multer.Options[\"fileFilter\"] = (_req, file, cb) => {\n  if (!ALLOWED_FILE_TYPES.includes(file.mimetype)) {\n    return cb(null, false);\n  }\n  cb(null, true);\n};\n\nconst upload = multer({\n  storage,\n  limits: { fileSize: MAX_FILE_SIZE },\n  fileFilter\n});\n\nexport default upload;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/security-header.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport cors from \"cors\";\nimport { Express } from \"express\";\nimport helmet from \"helmet\";\nimport env from \"../configs/env\";\n\nexport const configureSecurityHeaders = (app: Express) => {\n  // Use Helmet to set various security-related HTTP headers\n  app.use(helmet());\n\n  // Configure CORS\n  app.use(\n    cors({\n      origin: env.CORS_ORIGIN || \"*\",\n      credentials: true,\n      methods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\"],\n      allowedHeaders: [\"Content-Type\", \"Authorization\", \"X-Requested-With\"]\n    })\n  );\n\n  // Additional custom security headers\n  app.use((req: Request, res: Response, next: NextFunction) => {\n    res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n    res.setHeader(\"X-Frame-Options\", \"DENY\");\n    res.setHeader(\"X-XSS-Protection\", \"1; mode=block\");\n    next();\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/rate-limiter.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextFunction, Request, Response } from \"express\";\nimport { rateLimit } from \"express-rate-limit\";\nimport { STATUS_CODES } from \"../constants/status-codes\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const rateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000, // 15 minutes\n  max: 100, // Limit each IP to 100 requests per window\n  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers\n  legacyHeaders: false, // Disable the `X-RateLimit-*` headers\n  message: {\n    success: false,\n    message:\n      \"Too many requests from this IP, please try again after 15 minutes\",\n    status: 429\n  },\n  handler: (req: Request, res: Response, next: NextFunction, options: any) => {\n    next(new ApiError(STATUS_CODES.TOO_MANY_REQUESTS, options.message.message));\n  }\n});\n\n/**\n * Stricter rate limiter for sensitive routes (e.g., auth, login)\n */\nexport const authRateLimiter = rateLimit({\n  windowMs: 60 * 60 * 1000, // 1 hour\n  max: 5, // Limit each IP to 5 failed attempts per hour\n  handler: (req, res, next, options) => {\n    next(\n      ApiError.tooManyRequests(\n        \"Too many login attempts, please try again after an hour\"\n      )\n    );\n  }\n});\n\n/**\n * Rate limiter for login route\n */\nexport const signinRateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many login attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\n/**\n * Rate limiter for registration route\n */\nexport const signupRateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many registration attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const otpRequestLimiter = rateLimit({\n  windowMs: 10 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many OTP requests. Please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const otpVerificationLimiter = rateLimit({\n  windowMs: 10 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many OTP verification attempts. Please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const resetPasswordLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 6,\n  message: {\n    success: false,\n    message: \"Too many password reset attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const deleteAccountLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many account deletion attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\nexport const changePasswordLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 5,\n  message: {\n    success: false,\n    message: \"Too many password change attempts, please try again later.\",\n    statusCode: 429\n  },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/not-found-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const notFoundHandler = (\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => {\n  throw ApiError.notFound(`Route ${req.method} ${req.originalUrl} not found`);\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/error-handler.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport env from \"../configs/env\";\n\nimport { logger } from \"../utils/logger\";\nimport { ApiError } from \"../utils/api-error\";\n\nexport const errorHandler = (\n  err: Error,\n  req: Request,\n  res: Response,\n  next: NextFunction\n) => {\n  if (res.headersSent) {\n    return next(err);\n  }\n  let statusCode = 500;\n  let message = \"Internal server error\";\n  let errors: unknown;\n\n  if (err instanceof ApiError) {\n    statusCode = err.statusCode;\n    message = err.message;\n    errors = err.errors;\n  }\n\n  logger.error(\n    err,\n    `Error: ${message} | Status: ${statusCode} | Path: ${req.method} ${req.originalUrl}`\n  );\n\n  const response = {\n    success: false,\n    message,\n    statusCode,\n    ...(errors !== undefined && { errors }),\n    ...(env.NODE_ENV === \"development\" && { stack: err.stack })\n  };\n\n  res.status(statusCode).json(response);\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/helpers/token.helpers.ts",
                          "content": "import crypto from \"node:crypto\";\n\nexport function generateOTP(length: number = 6, ttlMinutes: number = 5) {\n  const code = crypto\n    .randomInt(0, Math.pow(10, length))\n    .toString()\n    .padStart(length, \"0\");\n\n  const hashCode = crypto\n    .createHash(\"sha256\")\n    .update(String(code))\n    .digest(\"hex\");\n\n  const expiresAt = new Date(Date.now() + ttlMinutes * 60 * 1000).toISOString();\n\n  return { code, hashCode, expiresAt };\n}\n\nexport function generateHashedToken(token: string): string {\n  return crypto.createHash(\"sha256\").update(String(token)).digest(\"hex\");\n}\n\nexport function createTokenLogFingerprint(\n  token: string,\n  length: number = 12\n): string {\n  return generateHashedToken(token).slice(0, length);\n}\n\nexport function generateSecureToken(length: number = 32): string {\n  return crypto.randomBytes(length).toString(\"hex\");\n}\n\nexport function verifyHashedToken(token: string, hashedToken: string): boolean {\n  return (\n    crypto.createHash(\"sha256\").update(String(token)).digest(\"hex\") ===\n    hashedToken\n  );\n}\n\nexport function generateTokenAndHashedToken(id: string) {\n  const cryptoSecret = process.env.CRYPTO_SECRET;\n\n  if (!cryptoSecret?.trim()) {\n    throw new Error(\"CRYPTO_SECRET is required to generate secure tokens\");\n  }\n\n  const token = crypto\n    .createHmac(\"sha256\", cryptoSecret)\n    .update(String(id))\n    .digest(\"hex\");\n\n  const hashedToken = crypto\n    .createHash(\"sha256\")\n    .update(String(token))\n    .digest(\"hex\");\n  return { token, hashedToken };\n}\n\nexport function generateUUID(): string {\n  return crypto.randomUUID();\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/helpers/token.helpers.test.ts",
                          "content": "import test from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport {\n  createTokenLogFingerprint,\n  generateHashedToken\n} from \"./token.helpers.ts\";\n\ntest(\"createTokenLogFingerprint never includes the raw token in log metadata\", () => {\n  const token = \"delete-account-raw-token\";\n  const tokenFingerprint = createTokenLogFingerprint(token);\n  const logMetadata = JSON.stringify({\n    userId: \"user-123\",\n    tokenFingerprint\n  });\n\n  assert.equal(tokenFingerprint, generateHashedToken(token).slice(0, 12));\n  assert.notEqual(tokenFingerprint, token);\n  assert.equal(logMetadata.includes(token), false);\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/helpers/cookie.helper.ts",
                          "content": "import { Response } from \"express\";\nimport {\n  ACCESS_TOKEN_EXPIRY,\n  REFRESH_TOKEN_EXPIRY,\n  SESSION_EXPIRY\n} from \"../../modules/auth/auth.constants\";\nimport env from \"../configs/env\";\n\nconst isProduction = env.NODE_ENV === \"production\";\n\nexport const COOKIE_OPTIONS = {\n  httpOnly: true,\n  secure: isProduction,\n  sameSite: isProduction ? (\"none\" as const) : (\"lax\" as const),\n  path: \"/\"\n};\n\nexport function setAuthCookies(\n  res: Response,\n  accessToken: string,\n  refreshToken: string,\n  sessionId: string\n) {\n  setCookies(res, [\n    {\n      cookie: \"accessToken\",\n      value: accessToken,\n      maxAge: ACCESS_TOKEN_EXPIRY\n    },\n    {\n      cookie: \"refreshToken\",\n      value: refreshToken,\n      maxAge: REFRESH_TOKEN_EXPIRY,\n      path: \"/api/v1/auth/refresh-token\"\n    },\n    {\n      cookie: \"sid\",\n      value: sessionId,\n      maxAge: SESSION_EXPIRY\n    }\n  ]);\n}\n\nexport function clearAuthCookies(res: Response) {\n  clearCookie(res, \"accessToken\");\n  clearCookie(res, \"refreshToken\", \"/api/v1/auth/refresh-token\");\n  clearCookie(res, \"sid\");\n}\n\nexport function clearCookie(\n  res: Response,\n  cookie: string = \"sid\",\n  path: string = \"/\"\n) {\n  res.clearCookie(cookie, {\n    ...COOKIE_OPTIONS,\n    path\n  });\n}\n\ntype Cookie = {\n  cookie: string;\n  value: string;\n  maxAge: number;\n  path?: string;\n};\n\nexport function setCookies(res: Response, cookies: Cookie[]) {\n  cookies.forEach(({ cookie, value, maxAge, path = \"/\" }) => {\n    res.cookie(cookie, value, {\n      ...COOKIE_OPTIONS,\n      path,\n      maxAge\n    });\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/helpers/cookie.helper.test.ts",
                          "content": "import assert from \"node:assert/strict\";\nimport { readFile } from \"node:fs/promises\";\nimport test from \"node:test\";\n\ntest(\"clearAuthCookies clears the refresh token using the refresh-token path\", async () => {\n  const source = await readFile(\n    new URL(\"./cookie.helper.ts\", import.meta.url),\n    \"utf8\"\n  );\n\n  assert.match(\n    source,\n    /clearCookie\\(res, \"refreshToken\", \"\\/api\\/v1\\/auth\\/refresh-token\"\\)/\n  );\n  assert.match(\n    source,\n    /res\\.clearCookie\\(cookie, \\{\\s*\\.\\.\\.COOKIE_OPTIONS,\\s*path\\s*\\}\\)/\n  );\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/constants/status-codes.ts",
                          "content": "export const STATUS_CODES = {\n  // 2xx Success\n  OK: 200,\n  CREATED: 201,\n  ACCEPTED: 202,\n  NO_CONTENT: 204,\n\n  // 3xx Redirection\n  MOVED_PERMANENTLY: 301,\n  FOUND: 302,\n  NOT_MODIFIED: 304,\n\n  // 4xx Client Errors\n  BAD_REQUEST: 400,\n  UNAUTHORIZED: 401,\n  FORBIDDEN: 403,\n  NOT_FOUND: 404,\n  CONFLICT: 409,\n  UNPROCESSABLE_ENTITY: 422,\n  TOO_MANY_REQUESTS: 429,\n\n  // 5xx Server Errors\n  INTERNAL_SERVER_ERROR: 500,\n  NOT_IMPLEMENTED: 501,\n  BAD_GATEWAY: 502,\n  SERVICE_UNAVAILABLE: 503,\n  GATEWAY_TIMEOUT: 504\n} as const;\n\nexport type StatusCode = (typeof STATUS_CODES)[keyof typeof STATUS_CODES];\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/swagger.ts",
                          "content": "import swaggerUi from \"swagger-ui-express\";\nimport { Express } from \"express\";\nimport env from \"../configs/env\";\n\nimport swaggerDocument from \"../../docs/swagger.json\";\n\nexport const setupSwagger = (app: Express) => {\n  if (env.NODE_ENV !== \"development\") return;\n\n  app.use(\"/api/docs\", swaggerUi.serve, swaggerUi.setup(swaggerDocument));\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/resend.ts",
                          "content": "import { Resend } from \"resend\";\nimport env from \"./env\";\n\nexport const resend = new Resend(env.RESEND_API_KEY);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/redis.ts",
                          "content": "import { createClient } from \"redis\";\nimport { env } from \"./env\";\n\nconst redisClient = createClient({\n  url: env.REDIS_URL\n});\n\nexport default redisClient;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/passport.ts",
                          "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport passport from \"passport\";\nimport {\n  Strategy as GitHubStrategy,\n  Profile as GithubProfile\n} from \"passport-github2\";\n\nimport {\n  Strategy as GoogleStrategy,\n  Profile as GoogleProfile\n} from \"passport-google-oauth20\";\n\nimport {\n  Strategy as FacebookStrategy,\n  Profile as FacebookProfile\n} from \"passport-facebook\"; // npm i --save-dev @types/passport-facebook\n\nimport env from \"./env\";\n\n//? GITHUB STRATEGY\npassport.use(\n  new GitHubStrategy(\n    {\n      clientID: env.GITHUB_CLIENT_ID,\n      clientSecret: env.GITHUB_CLIENT_SECRET,\n      callbackURL: env.GITHUB_REDIRECT_URI\n    },\n    function (\n      accessToken: string,\n      refreshToken: string,\n      profile: GithubProfile,\n      cb: (error: Error | null, user?: any) => void\n    ) {\n      // console.log({ profile });\n      return cb(null, profile);\n    }\n  )\n);\n\n//? GOOGLE STRATEGY\npassport.use(\n  new GoogleStrategy(\n    {\n      clientID: env.GOOGLE_CLIENT_ID,\n      clientSecret: env.GOOGLE_CLIENT_SECRET,\n      callbackURL: env.GOOGLE_REDIRECT_URI\n    },\n    function (accessToken, refreshToken, profile: GoogleProfile, cb) {\n      return cb(null, profile);\n    }\n  )\n);\n\n//? FACEBOOK STRATEGY\npassport.use(\n  new FacebookStrategy(\n    {\n      clientID: env.FACEBOOK_APP_ID,\n      clientSecret: env.FACEBOOK_APP_SECRET,\n      callbackURL: env.FACEBOOK_REDIRECT_URI\n    },\n    function (accessToken, refreshToken, profile: FacebookProfile, cb) {\n      // console.log({ profile });\n      return cb(null, profile);\n    }\n  )\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/env.ts",
                          "content": "/* eslint-disable no-console */\nimport \"dotenv-flow/config\";\nimport { z } from \"zod\";\n\nexport const envSchema = z.object({\n  NODE_ENV: z\n    .enum([\"development\", \"test\", \"production\"])\n    .default(\"development\"),\n\n  PORT: z.string().regex(/^\\d+$/, \"PORT must be a number\").transform(Number),\n\n  DATABASE_URL: z.url(),\n\n  CORS_ORIGIN: z.string(),\n  CLIENT_URL: z.url(),\n\n  LOG_LEVEL: z\n    .enum([\"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\"])\n    .default(\"info\"),\n\n  JWT_ACCESS_SECRET: z.string().min(32),\n  JWT_REFRESH_SECRET: z.string().min(32),\n\n  CRYPTO_SECRET: z.string().min(32),\n\n  RESEND_API_KEY: z.string(),\n  EMAIL_FROM: z.email(),\n\n  CLOUDINARY_CLOUD_NAME: z.string(),\n  CLOUDINARY_API_KEY: z.string(),\n  CLOUDINARY_API_SECRET: z.string(),\n\n  GOOGLE_CLIENT_ID: z.string(),\n  GOOGLE_CLIENT_SECRET: z.string(),\n  GOOGLE_REDIRECT_URI: z.url(),\n\n  GITHUB_CLIENT_ID: z.string(),\n  GITHUB_CLIENT_SECRET: z.string(),\n  GITHUB_REDIRECT_URI: z.url(),\n\n  FACEBOOK_APP_ID: z.string(),\n  FACEBOOK_APP_SECRET: z.string(),\n  FACEBOOK_REDIRECT_URI: z.url(),\n\n  REDIS_URL: z.url()\n});\n\nexport type Env = z.infer<typeof envSchema>;\n\nconst result = envSchema.safeParse(process.env);\n\nif (!result.success) {\n  console.error(\"❌ Invalid environment configuration\");\n  console.error(z.treeifyError(result.error));\n  process.exit(1);\n}\n\nexport const env: Readonly<Env> = Object.freeze(result.data);\n\nexport default env;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/db.ts",
                          "content": "import { drizzle } from \"drizzle-orm/node-postgres\";\nimport env from \"./env\";\nimport * as schema from \"../../drizzle\";\n\nconst db = drizzle(env.DATABASE_URL!, {\n  schema,\n  logger: env.NODE_ENV === \"development\"\n});\n\nexport default db;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/cloudinary.ts",
                          "content": "import { v2 as cloudinary } from \"cloudinary\";\nimport env from \"./env\";\n\ncloudinary.config({\n  cloud_name: env.CLOUDINARY_CLOUD_NAME,\n  api_key: env.CLOUDINARY_API_KEY,\n  api_secret: env.CLOUDINARY_API_SECRET\n});\n\nexport default cloudinary;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/upload/upload.service.ts",
                          "content": "import { DeleteApiResponse } from \"cloudinary\";\nimport cloudinary from \"../../shared/configs/cloudinary\";\n\nexport interface UploadOptions {\n  folder: string;\n  resource_type?: \"image\" | \"video\" | \"raw\" | \"auto\";\n}\n\nexport interface CloudinaryUploadResult {\n  url: string;\n  public_id: string;\n  size: number;\n}\n\nexport const uploadToCloudinary = (\n  buffer: Buffer,\n  options: UploadOptions\n): Promise<CloudinaryUploadResult> => {\n  return new Promise((resolve, reject) => {\n    const stream = cloudinary.uploader.upload_stream(\n      {\n        folder: options.folder || \"uploads\",\n        resource_type: options.resource_type || \"auto\"\n      },\n      (error, result) => {\n        if (error || !result) {\n          return reject(error);\n        }\n        resolve({\n          url: result.secure_url,\n          public_id: result.public_id,\n          size: result.bytes\n        });\n      }\n    );\n\n    stream.end(buffer);\n  });\n};\n\nexport const deleteFileFromCloudinary = (\n  publicIds: string[]\n): Promise<DeleteApiResponse> => {\n  return new Promise((resolve, reject) => {\n    cloudinary.api.delete_resources(publicIds, (error, result) => {\n      if (error || !result) {\n        return reject(error);\n      }\n      resolve(result);\n    });\n  });\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/otp/otp.service.ts",
                          "content": "import { logger } from \"../../shared/utils/logger\";\nimport redis from \"../../shared/configs/redis\";\nimport {\n  OTP_CODE_LENGTH,\n  OTP_EXPIRES_IN,\n  OTP_MAX_ATTEMPTS,\n  OTP_SPAM_LOCK_TIME,\n  OTP_COOL_DOWN\n} from \"../auth/auth.constants\";\nimport { generateOTP } from \"../../shared/helpers/token.helpers\";\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { sendEmail } from \"../../shared/utils/send-mail\";\n\ntype SendOtpBase = {\n  name: string;\n  email: string;\n  templateName: string;\n  subject: string;\n};\n\ntype SendOtpWithCode = SendOtpBase & {\n  code: string;\n  hashCode: string;\n};\n\ntype SendOtpWithoutCode = SendOtpBase & {\n  code?: never;\n  hashCode?: never;\n};\n\nexport type SendOtpType = SendOtpWithCode | SendOtpWithoutCode;\n\nexport class OtpService {\n  static async checkOtpRestrictions(email: string) {\n    const otpLock = await redis.get(`otp_lock:${email}`);\n    if (otpLock) {\n      throw ApiError.badRequest(\n        \"Your Account is locked due to multiple failed attempts. Please try again after 30 minutes.\"\n      );\n    }\n\n    if (await redis.get(`otp_spam_lock:${email}`)) {\n      throw ApiError.tooManyRequests(\n        \"Too many otp requests. Please try again after 1 hour before requesting again.\"\n      );\n    }\n\n    if (await redis.get(`otp_cooldown:${email}`)) {\n      throw ApiError.tooManyRequests(\n        \"Too many otp requests. Please try again after 1 minute before requesting new otp.\"\n      );\n    }\n  }\n\n  static async trackOtpRequests(email: string) {\n    try {\n      const otpRequestKey = `otp_request_count:${email}`;\n      let otpRequestsCount = parseInt((await redis.get(otpRequestKey)) || \"0\");\n      if (otpRequestsCount >= OTP_MAX_ATTEMPTS) {\n        await redis.set(`otp_spam_lock:${email}`, \"locked\", {\n          expiration: {\n            type: \"EX\",\n            value: 3600\n          }\n        });\n        throw ApiError.tooManyRequests(\n          \"Too many otp requests. Please try again after 1 hour before requesting again.\"\n        );\n      }\n\n      await redis.set(otpRequestKey, otpRequestsCount + 1, {\n        expiration: {\n          type: \"EX\",\n          value: 3600\n        }\n      });\n    } catch (error) {\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to track otp requests!\");\n    }\n  }\n\n  static async sendOtp({\n    name,\n    email,\n    templateName,\n    code,\n    hashCode,\n    subject\n  }: SendOtpType) {\n    try {\n      const newOtp = generateOTP(OTP_CODE_LENGTH);\n      const otpKey = `otp:${email}`;\n      const otpCooldownKey = `otp_cooldown:${email}`;\n      const otpHash = hashCode ? hashCode : newOtp.hashCode;\n\n      logger.info({ email }, \"OTP generated successfully\");\n\n      await redis.set(otpKey, otpHash, {\n        expiration: {\n          type: \"EX\",\n          value: OTP_EXPIRES_IN / 1000\n        }\n      });\n\n      await redis.set(otpCooldownKey, OTP_COOL_DOWN, {\n        expiration: {\n          type: \"EX\",\n          value: OTP_COOL_DOWN\n        }\n      });\n\n      try {\n        await sendEmail({\n          email,\n          subject,\n          data: {\n            code: code ? code : newOtp.code,\n            name\n          },\n          templateName\n        });\n      } catch (error) {\n        await Promise.allSettled([redis.del(otpKey), redis.del(otpCooldownKey)]);\n        throw error;\n      }\n    } catch (error) {\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to send otp!\");\n    }\n  }\n\n  static async verifyOtp(hashCode: string, email: string) {\n    const hashOtpCodeKey = await redis.get(`otp:${email}`);\n\n    if (!hashOtpCodeKey) {\n      throw ApiError.badRequest(\"Invalid or expired otp\");\n    }\n\n    const failedAttemptsKey = `otp_attempts:${email}`;\n    if (hashOtpCodeKey !== hashCode) {\n      const failedAttempts = await redis.incr(failedAttemptsKey);\n\n      if (failedAttempts === 1) {\n        await redis.expire(\n          failedAttemptsKey,\n          Math.floor(OTP_EXPIRES_IN / 1000)\n        );\n      }\n\n      if (failedAttempts >= OTP_MAX_ATTEMPTS) {\n        await redis.set(`otp_lock:${email}`, \"locked\", {\n          EX: OTP_SPAM_LOCK_TIME\n        });\n        throw ApiError.tooManyRequests(\n          \"Too many failed attempts. Please try again after 1 hour.\"\n        );\n      }\n      throw ApiError.badRequest(\n        `Incorrect OTP. ${OTP_MAX_ATTEMPTS - failedAttempts} attempts left.`\n      );\n    }\n\n    await redis.del([`otp:${email}`, failedAttemptsKey]);\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/otp/otp.service.test.ts",
                          "content": "import assert from \"node:assert/strict\";\nimport { readFile } from \"node:fs/promises\";\nimport test from \"node:test\";\n\ntest(\"otp service does not log raw OTP values\", async () => {\n  const source = await readFile(\n    new URL(\"./otp.service.ts\", import.meta.url),\n    \"utf8\"\n  );\n\n  assert.match(source, /logger\\.info\\(\\{ email \\}, \"OTP generated successfully\"\\)/);\n  assert.doesNotMatch(source, /OTP generated successfully: \\$\\{/);\n  assert.doesNotMatch(source, /logger\\.(info|warn|error|debug|trace)\\([^)]*(newOtp\\.code|code \\? code)/);\n});\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/oauth/oauth.service.ts",
                          "content": "import { AuthService, CookieOptionsType } from \"../auth/auth.service\";\nimport db from \"../../shared/configs/db\";\nimport { users } from \"../../drizzle/schemas/user.schema\";\nimport { eq } from \"drizzle-orm\";\n\ntype OAuthProfile = {\n  provider: \"local\" | \"google\" | \"github\" | \"facebook\";\n  providerId: string;\n  name: string;\n  email: string | undefined;\n  isEmailVerified: boolean;\n  avatar: string | undefined;\n  ip: string;\n  userAgent: string;\n};\n\nexport class OAuthService {\n  static async handleOAuthLogin(\n    user: OAuthProfile,\n    context: CookieOptionsType\n  ) {\n    if (!user.email) {\n      throw new Error(\"Email is required for OAuth login\");\n    }\n\n    const existingUser = await db.query.users.findFirst({\n        where: eq(users.email, user.email)\n    });\n\n    if (existingUser) {\n      const [updatedUser] = await db.update(users).set({\n        provider: user.provider,\n        providerId: user.providerId,\n        isEmailVerified: user.isEmailVerified,\n        avatar: user.avatar ? { url: user.avatar } : null\n      }).where(eq(users.id, existingUser.id)).returning();\n\n      await AuthService.handleToken(\n        {\n          _id: updatedUser.id,\n          role: updatedUser.role as \"user\" | \"admin\",\n          ip: user.ip,\n          userAgent: user.userAgent\n        },\n        context\n      );\n      return updatedUser;\n    }\n\n    const [newUser] = await db.insert(users).values({\n      name: user.name,\n      email: user.email,\n      isEmailVerified: user.isEmailVerified,\n      provider: user.provider,\n      providerId: user.providerId,\n      avatar: user.avatar ? { url: user.avatar } : null\n    }).returning();\n\n    await AuthService.handleToken(\n      {\n        _id: newUser.id,\n        role: newUser.role as \"user\" | \"admin\",\n        ip: user.ip,\n        userAgent: user.userAgent\n      },\n      context\n    );\n\n    return newUser;\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/oauth/oauth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport passport from \"passport\";\nimport { facebookOAuth, githubOAuth, googleOAuth } from \"./oauth.controller\";\n\nconst router = Router();\n\nrouter.get(\n  \"/github\",\n  passport.authenticate(\"github\", { scope: [\"user:email\"] })\n);\n\nrouter.get(\n  \"/github/callback\",\n  passport.authenticate(\"github\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed,\n    session: false\n  }),\n  githubOAuth\n);\n\nrouter.get(\n  \"/facebook\",\n  passport.authenticate(\"facebook\", { scope: [\"email\", \"user_location\"] })\n);\n\nrouter.get(\n  \"/facebook/callback\",\n  passport.authenticate(\"facebook\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed,\n    session: false,\n    failureMessage: true\n  }),\n  facebookOAuth\n);\n\nrouter.get(\n  \"/google\",\n  passport.authenticate(\"google\", {\n    scope: [\"email\", \"profile\", \"openid\"],\n    prompt: \"consent\"\n  })\n);\n\nrouter.get(\n  \"/google/callback\",\n  passport.authenticate(\"google\", {\n    failureRedirect: \"/login\", //? redirect route if authenticated is failed\n    session: false\n  }),\n  googleOAuth\n);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/oauth/oauth.controller.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport { Profile as GithubProfile } from \"passport-github2\";\nimport { Profile as GoogleProfile } from \"passport-google-oauth20\";\nimport { Profile as FacebookProfile } from \"passport-facebook\";\n\nimport { ApiResponse } from \"../../shared/utils/api-response\";\nimport { AsyncHandler } from \"../../shared/utils/async-handler\";\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { OAuthService } from \"./oauth.service\";\nimport { setAuthCookies } from \"../../shared/helpers/cookie.helper\";\n\n//? LOGIN WITH GITHUB\nexport const githubOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as GithubProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authentication failed\"));\n    }\n\n    const email = data?.emails?.[0]?.value;\n\n    if (!email?.trim()) {\n      return next(ApiError.badRequest(\"Email is required for OAuth login\"));\n    }\n\n    const user = {\n      provider: data?.provider as \"local\" | \"google\" | \"github\" | \"facebook\",\n      providerId: data.id,\n      name: data.displayName,\n      email,\n      isEmailVerified: false,\n      avatar:\n        data.photos && data.photos.length > 0\n          ? data.photos[0]?.value\n          : undefined,\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(user, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    //? save the data into your databases\n\n    ApiResponse.ok(res, \"Sign in successful\", {\n      user: {\n        id: existingUser.id,\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n\n//? LOGIN WITH GOOGLE\nexport const googleOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as GoogleProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authentication failed\"));\n    }\n\n    const email = data?.emails?.[0]?.value;\n\n    if (!email?.trim()) {\n      return next(ApiError.badRequest(\"Email is required for OAuth login\"));\n    }\n\n    const userInfo = {\n      provider: data?.provider as \"local\" | \"google\" | \"github\" | \"facebook\",\n      providerId: data.id,\n      name: data.displayName,\n      email,\n      isEmailVerified: data?.emails?.[0]?.verified === true,\n      avatar:\n        data.photos && data.photos.length > 0\n          ? data.photos[0]?.value\n          : undefined,\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    ApiResponse.ok(res, \"Sign in successful\", {\n      user: {\n        id: existingUser.id,\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n\n//? LOGIN WITH FACEBOOK\nexport const facebookOAuth = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const data = req.user as FacebookProfile | undefined;\n\n    if (!data) {\n      return next(ApiError.unauthorized(\"Authentication failed\"));\n    }\n\n    const email = data?.emails?.[0]?.value;\n\n    if (!email?.trim()) {\n      return next(ApiError.badRequest(\"Email is required for OAuth login\"));\n    }\n\n    const userInfo = {\n      provider: data?.provider as \"local\" | \"google\" | \"github\" | \"facebook\",\n      providerId: data.id,\n      name: data.displayName,\n      email,\n      isEmailVerified: false,\n      avatar:\n        data.profileUrl ||\n        (data.photos && data.photos.length > 0\n          ? data.photos[0]?.value\n          : undefined),\n      ip: req.ip || \"Unknown\",\n      userAgent: req.get(\"user-agent\") || req.headers[\"user-agent\"] || \"Unknown\"\n    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (\n        accessToken: string,\n        refreshToken: string,\n        sessionId: string\n      ) => {\n        setAuthCookies(res, accessToken, refreshToken, sessionId);\n      }\n    });\n\n    ApiResponse.ok(res, \"Sign in successful\", {\n      user: {\n        id: existingUser.id,\n        name: existingUser.name,\n        email: existingUser.email,\n        role: existingUser.role,\n        avatar: existingUser.avatar,\n        isEmailVerified: existingUser.isEmailVerified,\n        lastLoginAt: existingUser.lastLoginAt,\n        provider: existingUser.provider\n      }\n    });\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.validator.ts",
                          "content": "import * as z from \"zod\";\nimport { OTP_TYPES } from \"./auth.constants\";\n\nexport const nameSchema = z\n  .string({ error: \"Name must be a string\" })\n  .trim()\n  .min(3, {\n    message: \"Name must be at least 3 characters long\"\n  })\n  .max(50, {\n    message: \"Name must be at most 50 characters long\"\n  });\n\nexport const passwordSchema = z\n  .string({ error: \"Password must be a string\" })\n  .trim()\n  .min(6, {\n    message: \"Password must be at least 6 characters long\"\n  })\n  .max(80, {\n    message: \"Password must be at most 80 characters long\"\n  });\n\nexport const emailSchema = z\n  .email({ message: \"Please enter a valid email address.\" })\n  .max(100, { message: \"Email must be no more than 100 characters.\" });\n\nexport const roleSchema = z\n  .enum([\"user\", \"admin\"], {\n    error: \"Role must be either user or admin\"\n  })\n  .default(\"user\");\n\nexport const SigninSchema = z.object({\n  email: emailSchema,\n  password: z.string({ error: \"Password must be a string\" }).trim().min(1, {\n    message: \"Password is required\"\n  })\n});\n\nexport const SignupSchema = z\n  .object({\n    name: nameSchema,\n    email: emailSchema,\n    password: passwordSchema,\n    confirmPassword: passwordSchema\n  })\n  .refine(\n    data => {\n      return data.password === data.confirmPassword;\n    },\n    {\n      message: \"Passwords do not match\",\n      path: [\"confirmPassword\"]\n    }\n  );\n\nexport const RequestOtpSchema = z.object({\n  email: emailSchema,\n  otpType: z.enum(OTP_TYPES, { error: \"Invalid otp type\" })\n});\n\nexport const VerifyOtpSchema = z.object({\n  otpCode: z.string().min(6, \"Please enter a valid OTP\"),\n  email: emailSchema\n});\n\nexport const ResetPasswordSchema = z.object({\n  email: emailSchema,\n  newPassword: passwordSchema\n});\n\nexport const ChangePasswordSchema = z.object({\n  oldPassword: z.string({ error: \"Password must be a string\" }).min(1, {\n    message: \"Old password is required\"\n  }),\n  newPassword: passwordSchema\n});\n\nexport const UpdateProfileSchema = z.object({\n  name: nameSchema.optional(),\n  avatar: z.string().optional()\n});\n\nexport const GoogleSigninSchema = z.object({\n  name: nameSchema,\n  email: emailSchema,\n  provider: z.enum([\"google\", \"github\", \"facebook\"]).default(\"google\"),\n  providerId: z.string({ error: \"Provider id must be a string\" }).min(1, {\n    message: \"Provider id is required\"\n  }),\n  avatar: z.string().optional(),\n  isEmailVerified: z.boolean().default(false)\n});\n\nexport const DeleteAccountSchema = z.object({\n  userId: z.string({ error: \"User id must be a string\" }).min(1, {\n    message: \"User id is required\"\n  }),\n  type: z\n    .enum([\"soft\", \"hard\"], { error: \"Type must be either soft or hard\" })\n    .default(\"soft\")\n});\n\nexport type SignupUserType = z.infer<typeof SignupSchema>;\nexport type SigninUserType = z.infer<typeof SigninSchema>;\nexport type RequestOtpType = z.infer<typeof RequestOtpSchema>;\nexport type VerifyOtpType = z.infer<typeof VerifyOtpSchema>;\nexport type ResetPasswordType = z.infer<typeof ResetPasswordSchema>;\nexport type ChangePasswordType = z.infer<typeof ChangePasswordSchema>;\nexport type UpdateProfileType = z.infer<typeof UpdateProfileSchema>;\nexport type GoogleSigninType = z.infer<typeof GoogleSigninSchema>;\nexport type DeleteAccountType = z.infer<typeof DeleteAccountSchema>;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.types.ts",
                          "content": "import { OTP_TYPES } from \"@/modules/auth/auth.constants\";\n\nexport type OTPType = (typeof OTP_TYPES)[number];\n\nexport interface AvatarData {\n  public_id: string;\n  url: string;\n  size: number;\n}\n\nexport interface IUser {\n  id: string;\n  name: string;\n  email: string;\n  password?: string;\n  role: \"user\" | \"admin\";\n  isEmailVerified: boolean;\n  lastLoginAt?: Date;\n  failedLoginAttempts: number;\n  lockUntil?: Date;\n  avatar?: AvatarData | string | null;\n  provider: \"local\" | \"google\" | \"github\" | \"facebook\";\n  providerId?: string;\n  isDeleted: boolean;\n  deletedAt?: Date;\n  reActivateAvailableAt?: Date;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nexport type RefreshTokenData = {\n  userId: string;\n  tokenHash: string;\n  expiresAt: Date;\n};\n\nexport type SessionData = {\n  userId: string;\n  sessionId: string;\n  refreshTokenHash: string;\n  userAgent: string;\n  ip: string;\n  createdAt: Date;\n  expiresAt: Date;\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.service.ts",
                          "content": "import { NextFunction } from \"express\";\nimport db from \"../../shared/configs/db\";\nimport { users } from \"../../drizzle/schemas/user.schema\";\nimport { eq } from \"drizzle-orm\";\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { hashPassword, verifyPassword } from \"./auth.helpers\";\nimport { SignupUserType, VerifyOtpType } from \"./auth.validator\";\nimport {\n  DELETE_ACCOUNT_TOKEN_EXPIRY,\n  LOCK_TIME_MS,\n  LOGIN_MAX_ATTEMPTS,\n  OTP_CODE_LENGTH,\n  OTP_EXPIRES_IN,\n  REACTIVATION_AVAILABLE_AT,\n  REFRESH_TOKEN_EXPIRY,\n  RESET_PASSWORD_TOKEN_EXPIRY,\n  SESSION_EXPIRY\n} from \"./auth.constants\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../../shared/utils/jwt\";\nimport {\n  createTokenLogFingerprint,\n  generateHashedToken,\n  generateOTP,\n  generateSecureToken,\n  generateUUID\n} from \"../../shared/helpers/token.helpers\";\nimport { AvatarData, RefreshTokenData, SessionData } from \"./auth.types\";\nimport { OtpService } from \"../otp/otp.service\";\nimport { deleteFileFromCloudinary } from \"../upload/upload.service\";\nimport redisClient from \"../../shared/configs/redis\";\nimport { logger } from \"../../shared/utils/logger\";\nimport env from \"../../shared/configs/env\";\nimport { sendEmail } from \"../../shared/utils/send-mail\";\nimport { getRemainingTime } from \"../../shared/utils/date\";\n\nexport type CookieOptionsType = {\n  setAuthCookie?: (\n    accessToken: string,\n    refreshToken: string,\n    sessionId: string\n  ) => void;\n};\n\nexport class AuthService {\n  static async registerUser(user: Omit<SignupUserType, \"confirmPassword\">) {\n    try {\n      const { name, email, password } = user;\n      const enforcedRole = \"user\" as const;\n      const existingUser = await db.query.users.findFirst({\n        where: eq(users.email, email)\n      });\n\n      if (existingUser) {\n        throw ApiError.conflict(\"User with this email already exists\");\n      }\n\n      const pending = await redisClient.get(`user:pending:${email}`);\n\n      if (pending) {\n        throw ApiError.conflict(\n          \"Signup already in progress. Check your email for OTP.\"\n        );\n      }\n\n      const hashedPassword = await hashPassword(password);\n\n      await OtpService.checkOtpRestrictions(email);\n      await OtpService.trackOtpRequests(email);\n\n      const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n      const redisKey = `user:${email}:${hashCode}`;\n      const indexKey = `user:pending:${email}`;\n      await redisClient.set(indexKey, hashCode, {\n        expiration: {\n          type: \"PX\",\n          value: OTP_EXPIRES_IN\n        }\n      });\n      const userData = JSON.stringify({\n        name,\n        email,\n        role: enforcedRole,\n        password: hashedPassword\n      });\n\n      await OtpService.sendOtp({\n        name,\n        email,\n        templateName: \"email-verification\",\n        code,\n        hashCode,\n        subject: \"Email Verification\"\n      });\n\n      await redisClient.set(redisKey, userData, {\n        expiration: {\n          type: \"PX\",\n          value: OTP_EXPIRES_IN\n        }\n      });\n    } catch (error) {\n      logger.error(error, \"Failed to register user\");\n      if (error instanceof ApiError) {\n        throw error;\n      }\n      throw ApiError.server(\"Failed to register user\");\n    }\n  }\n\n  static async verifyUser({ email, otpCode }: VerifyOtpType) {\n    const hashCode = generateHashedToken(otpCode);\n\n    await OtpService.verifyOtp(hashCode, email);\n\n    const userData = await redisClient.get(`user:${email}:${hashCode}`);\n\n    if (!userData) {\n      throw ApiError.badRequest(\"Invalid or expired otp\");\n    }\n\n    const { name, email: userEmail, role, password } = JSON.parse(userData);\n    const enforcedRole = \"user\" as const;\n\n    if (role && role !== enforcedRole) {\n      throw ApiError.forbidden(\"Invalid signup role\");\n    }\n\n    const [user] = await db.insert(users).values({\n      name,\n      email: userEmail,\n      role: enforcedRole,\n      password,\n      isEmailVerified: true\n    }).returning();\n\n    await redisClient.del(`user:${email}:${hashCode}`);\n    await redisClient.del(`user:pending:${email}`);\n\n    return {\n      _id: user.id,\n      name,\n      email,\n      role: enforcedRole,\n      isEmailVerified: true\n    };\n  }\n\n  static async signinUser(\n    {\n      email,\n      password,\n      ip,\n      userAgent\n    }: {\n      email: string;\n      password: string;\n      ip: string;\n      userAgent: string;\n    },\n    setCookie: CookieOptionsType\n  ) {\n    try {\n      const user = await db.query.users.findFirst({\n        where: eq(users.email, email)\n      });\n      if (!user) {\n        throw ApiError.unauthorized(\"Invalid credentials\");\n      }\n\n      if (!user.isEmailVerified) {\n        throw ApiError.unauthorized(\"Email not verified\");\n      }\n\n      if (user.lockUntil && new Date() < user.lockUntil) {\n        throw ApiError.forbidden(\n          `Your account has been locked. Please try again after ${getRemainingTime(user.lockUntil).minutes} minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        );\n      }\n\n      const isPasswordValid = await verifyPassword(\n        password,\n        user.password || \"\"\n      );\n      if (!isPasswordValid) {\n        let lockUntil = null;\n\n        let newAttempts = user.failedLoginAttempts + 1;\n\n        if (newAttempts >= LOGIN_MAX_ATTEMPTS) {\n          lockUntil = new Date(Date.now() + LOCK_TIME_MS);\n        }\n\n        await db.update(users).set({\n          failedLoginAttempts: newAttempts,\n          lockUntil\n        }).where(eq(users.id, user.id));\n\n        throw ApiError.unauthorized(\"Invalid credentials\");\n      }\n\n      await db.update(users).set({\n        failedLoginAttempts: 0,\n        lockUntil: null\n      }).where(eq(users.id, user.id));\n\n      await AuthService.handleToken(\n        {\n          _id: user.id,\n          role: user.role as \"user\" | \"admin\",\n          ip,\n          userAgent\n        },\n        setCookie\n      );\n\n      return {\n        id: user.id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        isEmailVerified: user.isEmailVerified\n      };\n    } catch (err) {\n      if (err instanceof ApiError) {\n        throw err;\n      }\n      throw ApiError.server(\"Signin failed\");\n    }\n  }\n\n  static async handleToken(\n    user: { _id: string; role: \"user\" | \"admin\" } & {\n      ip: string;\n      userAgent: string;\n    },\n    context: CookieOptionsType\n  ) {\n    const sessionId = generateUUID();\n\n    const accessToken = generateAccessToken({\n      _id: user._id,\n      role: user.role,\n      sessionId\n    });\n\n    const refreshToken = generateRefreshToken({\n      _id: user._id,\n      sessionId\n    });\n\n    const hashedRefreshToken = generateHashedToken(refreshToken);\n\n    const refreshTokenData: RefreshTokenData = {\n      userId: user._id,\n      tokenHash: hashedRefreshToken,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    };\n\n    const sessionData: SessionData = {\n      userId: user._id,\n      sessionId,\n      refreshTokenHash: hashedRefreshToken,\n      userAgent: user.userAgent,\n      ip: user.ip,\n      createdAt: new Date(),\n      expiresAt: new Date(Date.now() + SESSION_EXPIRY)\n    };\n\n    const refreshTokenKey = `refreshToken:${hashedRefreshToken}`;\n\n    await redisClient.set(refreshTokenKey, JSON.stringify(refreshTokenData), {\n      expiration: {\n        type: \"PX\",\n        value: REFRESH_TOKEN_EXPIRY\n      }\n    });\n\n    const sessionKey = `session:${sessionId}`;\n\n    const userSessionsKey = `user_sessions:${user._id}`;\n\n    await redisClient.set(sessionKey, JSON.stringify(sessionData), {\n      expiration: {\n        type: \"PX\",\n        value: SESSION_EXPIRY\n      }\n    });\n\n    // add sessionId to user's set\n    await redisClient.sAdd(userSessionsKey, sessionId);\n\n    context.setAuthCookie &&\n      context.setAuthCookie(accessToken, refreshToken, sessionId);\n\n    await db.update(users).set({\n      lastLoginAt: new Date(),\n      failedLoginAttempts: 0,\n      lockUntil: null\n    }).where(eq(users.id, user._id));\n  }\n\n  static async getUserProfile(userId: string) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.id, userId)\n    });\n    return user;\n  }\n\n  static async refreshTokens(accessToken: string | null, refreshToken: string) {\n    if (!refreshToken) {\n      throw ApiError.unauthorized(\"Unauthorized, please login.\");\n    }\n\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n\n    if (!decodedRefresh?._id) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const refreshTokenKey = `refreshToken:${refreshTokenHash}`;\n\n    const storedToken = await redisClient.get(refreshTokenKey);\n    if (!storedToken) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    const { userId, tokenHash, expiresAt } = JSON.parse(\n      storedToken\n    ) as RefreshTokenData;\n\n    if (userId !== decodedRefresh._id) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    if (new Date(expiresAt) < new Date()) {\n      throw ApiError.unauthorized(\"Refresh token expired.\");\n    }\n\n    const session = await redisClient.get(\n      `session:${decodedRefresh.sessionId}`\n    );\n\n    if (!session) {\n      throw ApiError.unauthorized(\"Session not found.\");\n    }\n\n    const storedSessionData = JSON.parse(session) as SessionData;\n\n    if (\n      decodedRefresh.sessionId !== storedSessionData.sessionId ||\n      decodedRefresh._id !== storedSessionData.userId\n    ) {\n      throw ApiError.unauthorized(\"Token-session mismatch\");\n    }\n\n    if (accessToken) {\n      try {\n        const decodedAccess = verifyAccessToken(accessToken);\n        if (decodedAccess._id !== decodedRefresh._id) {\n          throw ApiError.unauthorized(\"Token mismatch.\");\n        }\n      } catch (e) {\n          // Normal for access token to be expired during refresh\n      }\n    }\n\n    const user = await db.query.users.findFirst({ where: eq(users.id, decodedRefresh._id) });\n    if (!user) {\n      throw ApiError.unauthorized(\"User not found.\");\n    }\n\n    const newAccessToken = generateAccessToken({\n      _id: user.id,\n      role: user.role as \"user\" | \"admin\",\n      sessionId: storedSessionData.sessionId\n    });\n\n    const newRefreshToken = generateRefreshToken({\n      _id: user.id,\n      sessionId: storedSessionData.sessionId\n    });\n    const newRefreshTokenHash = generateHashedToken(newRefreshToken);\n\n    //? Rotate token\n    await Promise.all([\n      redisClient.del(`refreshToken:${tokenHash}`),\n      redisClient.del(`session:${storedSessionData.sessionId}`)\n    ]);\n\n    const refreshTokenData: RefreshTokenData = {\n      userId: user.id,\n      tokenHash: newRefreshTokenHash,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    };\n    const sessionData: SessionData = {\n      userId: user.id,\n      sessionId: storedSessionData.sessionId,\n      refreshTokenHash: newRefreshTokenHash,\n      userAgent: storedSessionData.userAgent,\n      ip: storedSessionData.ip,\n      createdAt: new Date(),\n      expiresAt: new Date(Date.now() + SESSION_EXPIRY)\n    };\n\n    const newRefreshTokenKey = `refreshToken:${newRefreshTokenHash}`;\n    const newSessionKey = `session:${storedSessionData.sessionId}`;\n\n    await Promise.all([\n      redisClient.set(newRefreshTokenKey, JSON.stringify(refreshTokenData), {\n        expiration: {\n          type: \"PX\",\n          value: REFRESH_TOKEN_EXPIRY\n        }\n      }),\n      redisClient.set(newSessionKey, JSON.stringify(sessionData), {\n        expiration: {\n          type: \"PX\",\n          value: SESSION_EXPIRY\n        }\n      })\n    ]);\n\n    return {\n      accessToken: newAccessToken,\n      refreshToken: newRefreshToken,\n      sessionId: storedSessionData.sessionId\n    };\n  }\n\n  static async logoutUser(userId: string, sessionId: string) {\n    const sessionKey = `session:${sessionId}`;\n    const sessionData = await redisClient.get(sessionKey);\n    const userSessionsKey = `user_sessions:${userId}`;\n    if (!sessionData) {\n      throw ApiError.unauthorized(\"Session not found.\");\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.userId !== userId) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const refreshTokenKey = `refreshToken:${session.refreshTokenHash}`;\n\n    await redisClient.del(sessionKey);\n    await redisClient.del(refreshTokenKey);\n    await redisClient.sRem(userSessionsKey, sessionId);\n  }\n\n  static async forgotPassword(email: string) {\n    const user = await db.query.users.findFirst({\n      where: eq(users.email, email)\n    });\n\n    if (!user) {\n      return;\n    }\n\n    const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n    await OtpService.checkOtpRestrictions(email);\n    await OtpService.trackOtpRequests(email);\n\n    const redisKey = `reset_password:${email}:${hashCode}`;\n\n    await redisClient.set(redisKey, hashCode, {\n      expiration: {\n        type: \"PX\",\n        value: RESET_PASSWORD_TOKEN_EXPIRY\n      }\n    });\n\n    await OtpService.sendOtp({\n      email,\n      subject: \"Password Reset\",\n      templateName: \"forgot-password\",\n      name: user.name,\n      code,\n      hashCode\n    });\n  }\n\n  static async verifyResetPasswordOtp(otpCode: string, email: string) {\n    const hashedCode = generateHashedToken(otpCode);\n\n    const redisKey = `reset_password:${email}:${hashedCode}`;\n    const storedHashCode = await redisClient.get(redisKey);\n    if (!storedHashCode) {\n      throw ApiError.unauthorized(\"Invalid or expired otp\");\n    }\n    await OtpService.verifyOtp(storedHashCode, email);\n\n    await redisClient.del(`reset_password:${email}:${hashedCode}`);\n    await redisClient.set(`reset_password:status:${email}`, \"pending\", {\n      expiration: {\n        type: \"PX\",\n        value: RESET_PASSWORD_TOKEN_EXPIRY\n      }\n    });\n  }\n\n  static async getUserSessions(userId: string, currentSessionId: string) {\n    const userSessionsKey = `user_sessions:${userId}`;\n    const sessionIds = await redisClient.sMembers(userSessionsKey);\n\n    const sessions = [];\n    for (const sessionId of sessionIds) {\n      const sessionKey = `session:${sessionId}`;\n      const sessionData = await redisClient.get(sessionKey);\n      if (sessionData) {\n        const session = JSON.parse(sessionData) as SessionData;\n        sessions.push({\n          ...session,\n          isCurrent: sessionId === currentSessionId\n        });\n      }\n    }\n\n    return sessions;\n  }\n\n  static async deleteSession(userId: string, sessionId: string) {\n    const sessionKey = `session:${sessionId}`;\n    const sessionData = await redisClient.get(sessionKey);\n    const userSessionsKey = `user_sessions:${userId}`;\n\n    if (!sessionData) {\n      throw ApiError.notFound(\"Session not found.\");\n    }\n\n    const session = JSON.parse(sessionData) as SessionData;\n\n    if (session.userId !== userId) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const refreshTokenKey = `refreshToken:${session.refreshTokenHash}`;\n\n    await redisClient.del(sessionKey);\n    await redisClient.del(refreshTokenKey);\n    await redisClient.sRem(userSessionsKey, sessionId);\n  }\n\n  static async deleteAllUserSessions(userId: string) {\n    const userSessionsKey = `user_sessions:${userId}`;\n    const sessionIds = await redisClient.sMembers(userSessionsKey);\n\n    if (sessionIds.length === 0) {\n      return;\n    }\n\n    const sessions = await Promise.all(\n      sessionIds.map(async sessionId => {\n        const sessionKey = `session:${sessionId}`;\n        const sessionData = await redisClient.get(sessionKey);\n\n        return {\n          sessionKey,\n          session: sessionData ? (JSON.parse(sessionData) as SessionData) : null\n        };\n      })\n    );\n\n    await Promise.all(\n      sessions.flatMap(({ sessionKey, session }) => {\n        const deletions = [redisClient.del(sessionKey)];\n\n        if (session?.refreshTokenHash) {\n          deletions.push(\n            redisClient.del(`refreshToken:${session.refreshTokenHash}`)\n          );\n        }\n\n        return deletions;\n      })\n    );\n\n    await redisClient.del(userSessionsKey);\n  }\n\n  static async resetPassword(\n    next: NextFunction,\n    email: string,\n    newPassword: string\n  ) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.email, email)\n    });\n\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${\n            getRemainingTime(user.lockUntil).minutes\n          } minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        )\n      );\n    }\n\n    if (user.failedLoginAttempts >= LOGIN_MAX_ATTEMPTS && user.lockUntil) {\n      return next(\n        ApiError.forbidden(\n          `You have exceeded the maximum number of login attempts. Please try again after ${\n            getRemainingTime(user.lockUntil).minutes\n          } minutes and ${getRemainingTime(user.lockUntil).seconds} seconds.`\n        )\n      );\n    }\n\n    if (!user.isEmailVerified) {\n      return next(ApiError.unauthorized(\"Please verify your email first.\"));\n    }\n\n    const redisKey = `reset_password:status:${email}`;\n    const status = await redisClient.get(redisKey);\n    if (status !== \"pending\") {\n      return next(\n        ApiError.unauthorized(\n          \"Please request a password reset before attempting to set a new password.\"\n        )\n      );\n    }\n\n    const oldPassword = user.password;\n\n    const isOldPassword = await verifyPassword(\n      newPassword,\n      oldPassword as string\n    );\n\n    if (isOldPassword) {\n      return next(ApiError.badRequest(\"New password should be different!\"));\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n    await db.update(users).set({ password: hashedPassword }).where(eq(users.email, email));\n    await redisClient.del(`reset_password:status:${email}`);\n\n    //? Delete all user sessions\n    await this.deleteAllUserSessions(user.id);\n\n    return {\n      message: \"Password reset successfully. Please login!\"\n    };\n  }\n\n  static async changePassword(\n    next: NextFunction,\n    {\n      newPassword,\n      oldPassword,\n      userId\n    }: {\n      userId: string;\n      newPassword: string;\n      oldPassword: string;\n    }\n  ) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.id, userId)\n    });\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    if (!user.isEmailVerified) {\n      return next(ApiError.unauthorized(\"Please verify your email first.\"));\n    }\n\n    const isOldPassword = await verifyPassword(\n      oldPassword,\n      user.password || \"\"\n    );\n\n    if (!isOldPassword) {\n      return next(ApiError.unauthorized(\"Invalid credentials\"));\n    }\n\n    if (newPassword === oldPassword) {\n      return next(ApiError.badRequest(\"New password should be different!\"));\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n    await db.update(users).set({ password: hashedPassword }).where(eq(users.id, userId));\n\n    await this.deleteAllUserSessions(userId);\n\n    return {\n      message: \"Password changed successfully. Please login again!\"\n    };\n  }\n\n  static async requestDeleteAccount(userId: string, password: string) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.id, userId)\n    });\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const isPasswordValid = await verifyPassword(password, user.password || \"\");\n\n    if (!isPasswordValid) {\n      let lockUntil = null;\n\n      let newAttempts = user.failedLoginAttempts + 1;\n\n      if (newAttempts >= LOGIN_MAX_ATTEMPTS) {\n        lockUntil = new Date(Date.now() + LOCK_TIME_MS);\n      }\n\n      await db.update(users).set({\n        failedLoginAttempts: newAttempts,\n        lockUntil\n      }).where(eq(users.id, user.id));\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    const token = generateSecureToken();\n    const hashedToken = generateHashedToken(token);\n\n    const redisKey = `delete_account:token:${userId}`;\n\n    if (await redisClient.get(redisKey)) {\n      throw ApiError.badRequest(\"Delete account token already requested!\");\n    }\n\n    await redisClient.set(redisKey, hashedToken, {\n      expiration: {\n        type: \"PX\",\n        value: DELETE_ACCOUNT_TOKEN_EXPIRY\n      }\n    });\n\n    const deleteAccountUrl = `${env.CLIENT_URL}/account/delete?token=${token}`;\n    logger.warn(\n      {\n        userId,\n        tokenFingerprint: createTokenLogFingerprint(token)\n      },\n      \"Delete account token generated\"\n    );\n    await sendEmail({\n      email: user.email,\n      subject: \"Delete Account Request\",\n      templateName: \"delete-account\",\n      data: {\n        name: user.name,\n        deleteAccountUrl\n      }\n    });\n  }\n\n  static async deleteOrDeactiveAccount({\n    userId,\n    type,\n    token\n  }: {\n    userId: string;\n    type: \"soft\" | \"hard\";\n    token: string;\n  }) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.id, userId)\n    });\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    const redisKey = `delete_account:token:${userId}`;\n    const storedToken = await redisClient.get(redisKey);\n    if (!storedToken) {\n      throw ApiError.badRequest(\"Invalid or expired token!\");\n    }\n\n    const isTokenValid = generateHashedToken(token) === storedToken;\n    if (!isTokenValid) {\n      throw ApiError.badRequest(\"Invalid or expired token!\");\n    }\n\n    await redisClient.del(redisKey);\n\n    if (type === \"soft\") {\n      await db.update(users).set({\n        isDeleted: true,\n        deletedAt: new Date(),\n        reActivateAvailableAt: new Date(Date.now() + REACTIVATION_AVAILABLE_AT)\n      }).where(eq(users.id, userId));\n      await AuthService.deleteAllUserSessions(userId);\n    } else if (type === \"hard\") {\n      const avatar = user.avatar as AvatarData | string | null | undefined;\n\n      if (avatar && typeof avatar !== \"string\" && avatar.public_id) {\n        await deleteFileFromCloudinary([avatar.public_id]);\n      }\n      await db.delete(users).where(eq(users.id, userId));\n      await AuthService.deleteAllUserSessions(userId);\n    }\n  }\n\n  static async reactivateAccount(userId: string) {\n    const user = await db.query.users.findFirst({\n        where: eq(users.id, userId)\n    });\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized access\");\n    }\n\n    if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n      const remainingTime = getRemainingTime(user.lockUntil);\n      throw ApiError.badRequest(\n        `Your account has been locked. Please try again after ${remainingTime.minutes} minutes and ${remainingTime.seconds} seconds.`\n      );\n    }\n\n    if (!user?.isDeleted || !user?.deletedAt) {\n      throw ApiError.badRequest(\"Your account is already active!\");\n    }\n\n    if (\n      user?.reActivateAvailableAt &&\n      new Date(user?.reActivateAvailableAt) > new Date()\n    ) {\n      throw ApiError.forbidden(\n        `Your account has been locked. Please try again after ${\n          getRemainingTime(user.reActivateAvailableAt).minutes\n        } minutes and ${getRemainingTime(user.reActivateAvailableAt).seconds} seconds.`\n      );\n    }\n\n    await db.update(users).set({\n      isDeleted: false,\n      deletedAt: null,\n      reActivateAvailableAt: null\n    }).where(eq(users.id, userId));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport {\n  ChangePasswordSchema,\n  DeleteAccountSchema,\n  RequestOtpSchema,\n  ResetPasswordSchema,\n  SigninSchema,\n  SignupSchema,\n  UpdateProfileSchema,\n  VerifyOtpSchema\n} from \"./auth.validator\";\nimport {\n  changePassword,\n  deleteAccount,\n  deleteAllUserSessions,\n  deleteUserSession,\n  forgotPassword,\n  getUserProfile,\n  getUserSessions,\n  logoutUser,\n  reactivateAccount,\n  refreshToken,\n  requestDeleteAccount,\n  resetPassword,\n  signinUser,\n  signupUser,\n  updateProfile,\n  verifyResetPasswordOtp,\n  verifyUser\n} from \"./auth.controller\";\nimport { verifyAuthentication } from \"@/shared/middlewares/verify-auth\";\nimport { checkUserAccountRestriction } from \"@/shared/middlewares/user-account-restriction\";\nimport {\n  changePasswordLimiter,\n  deleteAccountLimiter,\n  otpRequestLimiter,\n  resetPasswordLimiter,\n  signinRateLimiter,\n  signupRateLimiter\n} from \"@/shared/middlewares/rate-limiter\";\nimport upload from \"@/shared/middlewares/upload-file\";\nimport { validateRequest } from \"@/shared/middlewares/validate-request\";\n\nconst router = Router();\n\nrouter.post(\n  \"/signup\",\n  validateRequest(SignupSchema),\n  signupRateLimiter,\n  signupUser\n);\n\nrouter.post(\"/verify-user\", validateRequest(VerifyOtpSchema), verifyUser);\n\nrouter.post(\n  \"/signin\",\n  validateRequest(SigninSchema),\n  signinRateLimiter,\n  signinUser\n);\n\nrouter.get(\"/profile\", verifyAuthentication, getUserProfile);\n\nrouter.patch(\n  \"/profile\",\n  upload.single(\"avatar\"),\n  validateRequest(UpdateProfileSchema),\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  updateProfile\n);\n\nrouter.get(\"/sessions\", verifyAuthentication, getUserSessions);\n\nrouter.delete(\n  \"/sessions\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  deleteAllUserSessions\n);\n\nrouter.delete(\n  \"/sessions/:sessionId\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  deleteUserSession\n);\n\nrouter.post(\"/refresh-token\", refreshToken);\n\nrouter.post(\n  \"/logout\",\n  verifyAuthentication,\n  checkUserAccountRestriction,\n  logoutUser\n);\n\nrouter.post(\n  \"/forgot-password\",\n  validateRequest(RequestOtpSchema.pick({ email: true })),\n  otpRequestLimiter,\n  forgotPassword\n);\n\nrouter.post(\n  \"/verify-reset-otp\",\n  validateRequest(VerifyOtpSchema),\n  otpRequestLimiter,\n  verifyResetPasswordOtp\n);\n\nrouter.post(\n  \"/reset-password\",\n  validateRequest(ResetPasswordSchema),\n  resetPasswordLimiter,\n  resetPassword\n);\n\nrouter.post(\n  \"/change-password\",\n  verifyAuthentication,\n  validateRequest(ChangePasswordSchema),\n  checkUserAccountRestriction,\n  changePasswordLimiter,\n  changePassword\n);\n\nrouter.post(\n  \"/account/request-delete\",\n  verifyAuthentication,\n  validateRequest(SigninSchema.pick({ password: true })),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  requestDeleteAccount\n);\n\nrouter.delete(\n  \"/account/delete\",\n  verifyAuthentication,\n  validateRequest(DeleteAccountSchema),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  deleteAccount\n);\n\nrouter.put(\"/account/reactivate\", verifyAuthentication, reactivateAccount);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.helpers.ts",
                          "content": "import argon2 from \"argon2\";\n\nexport async function hashPassword(password: string): Promise<string> {\n  return argon2.hash(password);\n}\n\nexport async function verifyPassword(\n  password: string,\n  hash: string\n): Promise<boolean> {\n  return argon2.verify(hash, password);\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.controller.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport { ApiResponse } from \"../../shared/utils/api-response\";\nimport { AsyncHandler } from \"../../shared/utils/async-handler\";\n\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { AuthService } from \"./auth.service\";\nimport {\n  clearAuthCookies,\n  clearCookie,\n  setAuthCookies\n} from \"../../shared/helpers/cookie.helper\";\nimport { UserRequest } from \"../../types/global\";\nimport {\n  deleteFileFromCloudinary,\n  uploadToCloudinary\n} from \"../upload/upload.service\";\nimport { DeleteAccountType, VerifyOtpType } from \"./auth.validator\";\nimport db from \"../../shared/configs/db\";\nimport { users } from \"../../drizzle/schemas/user.schema\";\nimport { eq } from \"drizzle-orm\";\nimport { AvatarData } from \"./auth.types\";\n\n//? SIGNUP USER\nexport const signupUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { name, email, password } = req.body;\n    if (!name || !email || !password) {\n      return next(ApiError.badRequest(\"Name, email and password are required\"));\n    }\n\n    await AuthService.registerUser({\n      name,\n      email,\n      password\n    });\n\n    return ApiResponse.Success(\n      res,\n      \"User registered successfully. Please check your email for verification.\"\n    );\n  }\n);\n\n//? VERIFY USER\nexport const verifyUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, otpCode }: VerifyOtpType = req.body;\n\n    if (!email || !otpCode) {\n      return next(ApiError.badRequest(\"Email and code are required\"));\n    }\n\n    await AuthService.verifyUser({ email, otpCode });\n\n    return ApiResponse.ok(res, \"User verified successfully\");\n  }\n);\n\n//? SIGNIN USER\nexport const signinUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, password } = req.body;\n    if (!email || !password) {\n      return next(ApiError.badRequest(\"Email and password are required\"));\n    }\n\n    const ip = req.ip || \"Unknown\";\n    const userAgent = req.headers[\"user-agent\"] || \"Unknown\";\n\n    await AuthService.signinUser(\n      { email, password, ip, userAgent },\n      {\n        setAuthCookie: (\n          accessToken: string,\n          refreshToken: string,\n          sessionId: string\n        ) => {\n          setAuthCookies(res, accessToken, refreshToken, sessionId);\n        }\n      }\n    );\n\n    return ApiResponse.ok(res, \"User signed in successfully!\");\n  }\n);\n\n//? GET USER PROFILE\nexport const getUserProfile = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(userId.toString());\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    if (user.isDeleted) {\n      return next(ApiError.notFound(\"This account has been deactivated.\"));\n    }\n\n    const result = await AuthService.getUserSessions(\n      userId.toString(),\n      currentSessionId\n    );\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to get user sessions!\"));\n    }\n\n    return ApiResponse.ok(res, \"User profile fetched successfully\", {\n      user: {\n        id: user.id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        avatar: user.avatar,\n        isEmailVerified: user.isEmailVerified,\n        lastLoginAt: user.lastLoginAt,\n        sessions: result\n      }\n    });\n  }\n);\n\n//? UPDATE PROFILE\nexport const updateProfile = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const data = req.body;\n    const { name } = data;\n\n    if (!req.user?.id) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(req.user?.id.toString());\n\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    const avatar = user.avatar as AvatarData | null;\n\n    if (req?.file && avatar?.public_id) {\n      await deleteFileFromCloudinary([avatar.public_id]);\n    }\n\n    let updatedAvatar: AvatarData | null = avatar;\n\n    if (req?.file) {\n      const file = await uploadToCloudinary(req.file.buffer, {\n        folder: \"uploads/files\",\n        resource_type: \"auto\"\n      });\n      updatedAvatar = {\n        public_id: file.public_id,\n        url: file.url,\n        size: file.size\n      };\n    }\n\n    const updateData: Record<string, unknown> = {};\n    if (name) updateData.name = name;\n    if (updatedAvatar !== avatar) updateData.avatar = updatedAvatar;\n\n    if (Object.keys(updateData).length > 0) {\n      await db.update(users).set(updateData).where(eq(users.id, user.id));\n    }\n\n    const updatedUser = await AuthService.getUserProfile(user.id);\n\n    return ApiResponse.Success(res, \"Profile updated successfully!\", {\n      user: {\n        id: updatedUser?.id,\n        name: updatedUser?.name,\n        email: updatedUser?.email,\n        role: updatedUser?.role,\n        avatar: updatedUser?.avatar,\n        isEmailVerified: updatedUser?.isEmailVerified,\n        lastLoginAt: updatedUser?.lastLoginAt\n      }\n    });\n  }\n);\n\n//? REFRESH TOKENS\nexport const refreshToken = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const accessToken = req.cookies?.accessToken;\n    const refreshToken = req.cookies?.refreshToken;\n\n    const token = await AuthService.refreshTokens(accessToken, refreshToken);\n\n    if (!token) {\n      return next(ApiError.server(\"Failed to refresh tokens!\"));\n    }\n\n    const newAccessToken = token.accessToken;\n    const newRefreshToken = token.refreshToken;\n    setAuthCookies(res, newAccessToken, newRefreshToken, token.sessionId);\n    clearCookie(res, \"refreshToken\");\n\n    return ApiResponse.Success(res, \"Tokens refreshed successfully!\");\n  }\n);\n\n//? LOGOUT\nexport const logoutUser = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req.user?.id;\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const currentSessionId = req.user?.sessionId;\n    if (!currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.logoutUser(userId.toString(), currentSessionId);\n\n    clearAuthCookies(res);\n\n    return ApiResponse.Success(res, \"Logged out successfully!\");\n  }\n);\n\n//? FORGOT PASSWORD\nexport const forgotPassword = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email } = req.body;\n    if (!email) {\n      return next(ApiError.badRequest(\"Email is required!\"));\n    }\n\n    await AuthService.forgotPassword(email);\n\n    return ApiResponse.ok(\n      res,\n      \"If an account exists, a reset code has been sent to your email.\"\n    );\n  }\n);\n\n//? VERIFY RESET PASSWORD TOKEN\nexport const verifyResetPasswordOtp = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { otpCode, email } = req.body;\n    if (!otpCode || !email) {\n      return next(ApiError.badRequest(\"OtpCode and email are required!\"));\n    }\n\n    await AuthService.verifyResetPasswordOtp(otpCode, email);\n\n    return ApiResponse.ok(\n      res,\n      \"Password reset otp verified successfully. You can now reset your password.\"\n    );\n  }\n);\n\n//? RESET PASSWORD\nexport const resetPassword = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { newPassword, email } = req.body;\n    if (!email || !newPassword) {\n      return next(ApiError.badRequest(\"Newpassword and email are required!\"));\n    }\n\n    const result = await AuthService.resetPassword(next, email, newPassword);\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to reset password!\"));\n    }\n\n    clearAuthCookies(res);\n\n    return ApiResponse.ok(\n      res,\n      result.message || \"Password reset successfully!\"\n    );\n  }\n);\n\n//? CHANGE PASSWORD\nexport const changePassword = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const { oldPassword, newPassword } = req.body;\n\n    if (!oldPassword || !newPassword) {\n      return next(\n        ApiError.badRequest(\"Old password and new password are required\")\n      );\n    }\n\n    const result = await AuthService.changePassword(next, {\n      userId: userId.toString(),\n      oldPassword,\n      newPassword\n    });\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to change password!\"));\n    }\n\n    clearAuthCookies(res);\n\n    return ApiResponse.ok(\n      res,\n      result.message || \"Password changed successfully!\"\n    );\n  }\n);\n\n//? REQUEST DELETE ACCOUNT\nexport const requestDeleteAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n    const { password } = req.body;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    if (!password) {\n      return next(ApiError.badRequest(\"Password is required!\"));\n    }\n\n    await AuthService.requestDeleteAccount(userId, password);\n\n    return ApiResponse.ok(\n      res,\n      \"Account deletion request sent successfully. Please check your email to confirm.\"\n    );\n  }\n);\n\n//? DELETE/DEACTIVATE ACCOUNT\nexport const deleteAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { userId, type }: DeleteAccountType = req.body;\n\n    if (!userId || !type) {\n      return next(ApiError.badRequest(\"User id and type are required!\"));\n    }\n\n    const reqUserId = req?.user?.id;\n\n    if (!reqUserId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n    const token = req.query.token as string;\n    if (!token) {\n      return next(\n        ApiError.badRequest(\n          `${type === \"hard\" ? \"Delete\" : \"Deactivate\"} account token is required!`\n        )\n      );\n    }\n\n    if (userId !== reqUserId.toString()) {\n      return next(\n        ApiError.unauthorized(\"You are not authorized to perform this action\")\n      );\n    }\n\n    await AuthService.deleteOrDeactiveAccount({ userId, type, token });\n\n    clearAuthCookies(res);\n\n    return ApiResponse.Success(\n      res,\n      `Account ${type === \"soft\" ? \"deactivated\" : \"deleted\"} successfully!`\n    );\n  }\n);\n\n//? REACTIVATE ACCOUNT\nexport const reactivateAccount = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.reactivateAccount(userId);\n\n    return ApiResponse.Success(res, \"Account reactivated successfully!\");\n  }\n);\n\n//? GET USER SESSIONS\nexport const getUserSessions = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const result = await AuthService.getUserSessions(\n      userId.toString(),\n      currentSessionId\n    );\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to get user sessions!\"));\n    }\n\n    return ApiResponse.ok(res, \"User sessions fetched successfully\", result);\n  }\n);\n\n//? DELETE SESSION\nexport const deleteUserSession = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n    const { sessionId } = req.params;\n\n    if (!userId || !sessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.deleteSession(userId, sessionId as string);\n\n    const reqSId = req.cookies?.sid;\n\n    const isCurrentSession = sessionId === reqSId;\n    if (isCurrentSession) {\n      clearAuthCookies(res);\n    }\n\n    return ApiResponse.Success(res, \"User session deleted successfully!\");\n  }\n);\n\n//? DELETE ALL SESSIONS\nexport const deleteAllUserSessions = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const userId = req?.user?.id;\n    const currentSessionId = req.user?.sessionId;\n\n    if (!userId || !currentSessionId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.deleteAllUserSessions(userId);\n\n    clearAuthCookies(res);\n    // clearCookie(res, \"sid\");\n\n    return ApiResponse.Success(res, \"User sessions deleted successfully!\");\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.constants.ts",
                          "content": "export const OTP_MAX_ATTEMPTS = 5;\nexport const OTP_TYPES = [\n  \"signin\",\n  \"email-verification\",\n  \"password-reset\",\n  \"password-change\"\n] as const;\n\nexport const NEXT_OTP_DELAY = 1 * 60 * 1000; // 1 minute\n\nexport const LOGIN_MAX_ATTEMPTS = 5 as const;\n\nexport const OTP_CODE_LENGTH = 6 as const;\n\nexport const OTP_COOL_DOWN = 60;\n\nexport const OTP_EXPIRES_IN = 5 * 60 * 1000; // 5 minutes\n\nexport const OTP_SPAM_LOCK_TIME = 3600; // 1 hour\n\nexport const LOCK_TIME_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport const ACCESS_TOKEN_EXPIRY = 15 * 60 * 1000; // 15 minutes\n\nexport const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days\n\nexport const SESSION_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days\n\nexport const RESET_PASSWORD_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n\nexport const REACTIVATION_AVAILABLE_AT = 24 * 60 * 60 * 1000; // 24 hours\n\nexport const DELETE_ACCOUNT_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/health/health.routes.ts",
                          "content": "import { Router } from \"express\";\nimport { healthCheck, detailedHealthCheck } from \"./health.controller\";\n\nconst router = Router();\n\nrouter.get(\"/\", healthCheck);\nrouter.get(\"/detailed\", detailedHealthCheck);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/health/health.controller.ts",
                          "content": "import { Request, Response } from \"express\";\nimport { ApiResponse } from \"../../shared/utils/api-response\";\nimport { AsyncHandler } from \"../../shared/utils/async-handler\";\n\n/**\n * Basic health check endpoint\n * GET /api/health\n */\nexport const healthCheck = AsyncHandler(\n  async (_req: Request, res: Response) => {\n    return ApiResponse.Success(res, \"Service is healthy\", {\n      status: \"healthy\",\n      timestamp: new Date().toISOString(),\n      uptime: process.uptime()\n    });\n  }\n);\n\n/**\n * Detailed health check with system information\n * GET /api/health/detailed\n */\nexport const detailedHealthCheck = AsyncHandler(\n  async (_req: Request, res: Response) => {\n    const healthData = {\n      status: \"healthy\",\n      timestamp: new Date().toISOString(),\n      uptime: process.uptime(),\n      environment: process.env.NODE_ENV || \"development\",\n      version: process.env.npm_package_version || \"1.0.0\",\n      memory: {\n        used:\n          Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) /\n          100,\n        total:\n          Math.round((process.memoryUsage().heapTotal / 1024 / 1024) * 100) /\n          100,\n        unit: \"MB\"\n      },\n      cpu: {\n        usage: process.cpuUsage()\n      }\n    };\n\n    return ApiResponse.Success(res, \"Service is healthy\", healthData);\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/schemas/user.schema.ts",
                          "content": "import { \n  pgTable, \n  text, \n  timestamp, \n  boolean, \n  integer,\n  json\n} from \"drizzle-orm/pg-core\";\nimport { createId } from \"@paralleldrive/cuid2\";\n\nexport const users = pgTable(\"users\", {\n  id: text(\"id\").primaryKey().$defaultFn(() => createId()),\n  name: text(\"name\").notNull(),\n  email: text(\"email\").notNull().unique(),\n  password: text(\"password\"),\n  role: text(\"role\", { enum: [\"user\", \"admin\"] }).default(\"user\").notNull(),\n  isEmailVerified: boolean(\"is_email_verified\").default(false).notNull(),\n  lastLoginAt: timestamp(\"last_login_at\"),\n  failedLoginAttempts: integer(\"failed_login_attempts\").default(0).notNull(),\n  lockUntil: timestamp(\"lock_until\"),\n  avatar: json(\"avatar\"), // { public_id: string, url: string, size: number }\n  \n  provider: text(\"provider\", { enum: [\"local\", \"google\", \"github\", \"facebook\"] }).default(\"local\").notNull(),\n  providerId: text(\"provider_id\"),\n  \n  isDeleted: boolean(\"is_deleted\").default(false).notNull(),\n  deletedAt: timestamp(\"deleted_at\"),\n  reActivateAvailableAt: timestamp(\"re_activate_available_at\"),\n  \n  createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n  updatedAt: timestamp(\"updated_at\").defaultNow().notNull(),\n});\n\nexport type User = typeof users.$inferSelect;\nexport type NewUser = typeof users.$inferInsert;\n"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
