{
  "slug": "stateless-auth",
  "runtimes": {
    "node": {
      "frameworks": {
        "express": {
          "databases": {
            "mysql": {
              "orms": {
                "drizzle": {
                  "dependencies": {
                    "runtime": [
                      "express",
                      "mysql2",
                      "ejs",
                      "node-cron",
                      "redis",
                      "drizzle-orm",
                      "argon2",
                      "cloudinary",
                      "cookie-parser",
                      "cors",
                      "express-rate-limit",
                      "helmet",
                      "jsonwebtoken",
                      "multer",
                      "nodemailer",
                      "passport",
                      "passport-github2",
                      "passport-google-oauth20",
                      "pino",
                      "pino-pretty",
                      "zod",
                      "dotenv-flow",
                      "cross-env",
                      "source-map-support",
                      "swagger-autogen",
                      "swagger-ui-express"
                    ],
                    "dev": [
                      "@types/express",
                      "drizzle-kit",
                      "@types/ejs",
                      "@types/cookie-parser",
                      "@types/cors",
                      "@types/jsonwebtoken",
                      "@types/morgan",
                      "@types/multer",
                      "@types/nodemailer",
                      "@types/passport",
                      "@types/passport-github2",
                      "@types/passport-google-oauth20",
                      "morgan",
                      "@types/source-map-support",
                      "@types/swagger-ui-express"
                    ]
                  },
                  "env": [
                    "PORT",
                    "NODE_ENV",
                    "LOG_LEVEL",
                    "CORS_ORIGIN",
                    "CRYPTO_SECRET",
                    "DATABASE_URL",
                    "JWT_ACCESS_SECRET",
                    "JWT_REFRESH_SECRET",
                    "REDIS_URL",
                    "SMTP_HOST",
                    "SMTP_PORT",
                    "SMTP_USER",
                    "SMTP_PASS",
                    "EMAIL_FROM",
                    "CLOUDINARY_CLOUD_NAME",
                    "CLOUDINARY_API_KEY",
                    "CLOUDINARY_API_SECRET",
                    "GOOGLE_CLIENT_ID",
                    "GOOGLE_CLIENT_SECRET",
                    "GOOGLE_REDIRECT_URI",
                    "GITHUB_CLIENT_ID",
                    "GITHUB_CLIENT_SECRET",
                    "GITHUB_REDIRECT_URI"
                  ],
                  "architectures": {
                    "mvc": {
                      "files": [
                        {
                          "type": "file",
                          "path": "swagger.config.ts",
                          "content": "import swaggerAutoGen from \"swagger-autogen\";\n\nconst doc = {\n  info: {\n    title: \"Stateless Auth API\",\n    description: \"Stateless Auth API\",\n    version: \"1.0.0\"\n  },\n  host: `localhost:3000`,\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": "package.json",
                          "content": "{\n  \"name\": \"servercn-stateless-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    \"db:generate\": \"drizzle-kit generate\",\n    \"db:migrate\": \"drizzle-kit migrate\",\n    \"db:studio\": \"drizzle-kit studio\",\n    \"lint:check\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"format:check\": \"npx prettier . --check\",\n    \"format:fix\": \"npx prettier . --write\"\n  },\n  \"devDependencies\": {},\n  \"dependencies\": {}\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "drizzle.config.ts",
                          "content": "import { Config, defineConfig } from \"drizzle-kit\";\n\nimport env from \"./src/configs/env\";\n\nexport default defineConfig({\n  out: \"./src/drizzle/migrations\",\n  schema: \"./src/drizzle/index.ts\",\n  dialect: \"mysql\",\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 { connectRedis } from \"./configs/redis\";\nimport { logger } from \"./utils/logger\";\nimport { configureGracefulShutdown } from \"./utils/shutdown\";\n\nconst port = env.PORT || 9000;\n\nconnectRedis();\n\nconst 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\nconfigureGracefulShutdown(server);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/app.ts",
                          "content": "import express, { Express, Request, Response } from \"express\";\nimport cors from \"cors\";\nimport helmet from \"helmet\";\nimport cookieParser from \"cookie-parser\";\nimport morgan from \"morgan\";\nimport { notFoundHandler } from \"./middlewares/not-found-handler\";\nimport { errorHandler } from \"./middlewares/error-handler\";\nimport Routes from \"./routes/index\";\nimport { startRefreshTokenCleanupJob } from \"./cron/cleanup-refresh-tokens.cron\";\nimport swaggerUi from \"swagger-ui-express\";\nimport swaggerDocument from \"./docs/swagger.json\";\nimport { rateLimiter } from \"./middlewares/rate-limiter\";\nimport sourceMapSupport from \"source-map-support\";\nsourceMapSupport.install();\n\nconst app: Express = express();\n\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\napp.use(\n  cors({\n    origin: \"*\",\n    credentials: true\n  })\n);\napp.use(helmet());\napp.use(cookieParser());\napp.use(morgan(process.env.NODE_ENV === \"development\" ? \"dev\" : \"combined\"));\n\n// Routes\n\napp.get(\"/\", (req: Request, res: Response) => {\n  res.redirect(\"/api/v1/health\");\n});\n\napp.use(\"/api\", Routes);\n\napp.use(\"/api/docs\", swaggerUi.serve, swaggerUi.setup(swaggerDocument));\nstartRefreshTokenCleanupJob();\n\napp.get([\"/docs-json\", \"/api-docs-json\"], (req: Request, res: Response) => {\n  res.json(swaggerDocument);\n});\n\napp.use(rateLimiter);\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"
                        },
                        {
                          "type": "file",
                          "path": "src/types/user.d.ts",
                          "content": "import { Request } from \"express\";\nimport { OTP_TYPES } from \"../constants/auth\";\nimport { IAvatar } from \"../drizzle\";\n\nexport type OTPType = (typeof OTP_TYPES)[number];\n\nexport interface UserRequest extends Request {\n  user?: {\n    id?: number | undefined;\n    role?: \"user\" | \"admin\" | undefined;\n  };\n}\n\nexport interface IUser {\n  id: number;\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  provider: \"local\" | \"google\" | \"github\";\n  providerId?: string;\n  isDeleted: boolean;\n  deletedAt?: Date;\n  reActivateAvailableAt?: Date;\n  createdAt: Date;\n  updatedAt: Date;\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/validators/auth.ts",
                          "content": "import * as z from \"zod\";\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.object({\n  name: nameSchema,\n  email: emailSchema,\n  password: passwordSchema,\n  role: roleSchema\n});\n\nexport const VerifyOtpSchema = z.object({\n  code: 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.number().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 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\nexport type VerifyOtpType = z.infer<typeof VerifyOtpSchema>;\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": "import env from \"../configs/env\";\nimport { getTransporter } from \"../configs/nodemailer\";\nimport { ApiError } from \"./api-error\";\nimport { renderEmailTemplates } from \"./render-template\";\n\ntype sendMail = {\n  from?: string;\n  subject: string;\n  data: Record<string, any>;\n  email: string;\n  templateName: string;\n};\n\nexport async function sendEmail({\n  from,\n  email,\n  subject,\n  data,\n  templateName\n}: sendMail) {\n  const transporter = getTransporter();\n\n  const html = await renderEmailTemplates(templateName, data);\n  return transporter\n    .sendMail({\n      from: from || `<${env.EMAIL_FROM}>`,\n      to: email,\n      subject,\n      html\n    })\n    .catch(err => {\n      throw ApiError.badRequest(\"Failed to send email\");\n    });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/utils/render-template.ts",
                          "content": "import ejs from \"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 ACCESS_TOKEN_EXPIRY = \"15m\";\nconst REFRESH_TOKEN_EXPIRY = \"7d\";\n\nexport function generateAccessToken(user: { id: number; role: string }) {\n  return jwt.sign({ id: user.id, role: user.role }, env.JWT_ACCESS_SECRET!, {\n    expiresIn: ACCESS_TOKEN_EXPIRY\n  });\n}\n\nexport function generateRefreshToken(userId: number) {\n  return jwt.sign({ userId }, env.JWT_REFRESH_SECRET!, {\n    expiresIn: REFRESH_TOKEN_EXPIRY\n  });\n}\n\nexport function verifyAccessToken(token: string) {\n  return jwt.verify(token, env.JWT_ACCESS_SECRET!) as {\n    id: number;\n    role: \"user\" | \"admin\";\n  };\n}\n\nexport function verifyRefreshToken(token: string) {\n  return jwt.verify(token, env.JWT_REFRESH_SECRET!) as {\n    userId: number;\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 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/routes/oauth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport passport from \"passport\";\nimport { 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  \"/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 authRoutes from \"./auth.routes\";\nimport healthRoutes from \"./health.routes\";\nimport oauthRoutes from \"./oauth.routes\";\n\nconst router = Router();\n\nrouter.use(\"/v1/health\", healthRoutes);\n\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 = \"/api/auth/google/callback\"\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 {\n  changePassword,\n  deleteAccount,\n  forgotPassword,\n  getUserProfile,\n  loginUser,\n  logoutUser,\n  reactivateAccount,\n  refreshToken,\n  resetPassword,\n  signupUser,\n  updateProfile,\n  verifyForgotPasswordOtp,\n  verifyUser\n} from \"../controllers/auth.controller\";\nimport { validateRequest } from \"../middlewares/validate-request\";\nimport {\n  ChangePasswordSchema,\n  DeleteAccountSchema,\n  ResetPasswordSchema,\n  SigninSchema,\n  SignupSchema,\n  UpdateProfileSchema,\n  VerifyOtpSchema\n} from \"../validators/auth\";\nimport { verifyAuthentication } from \"../middlewares/verify-auth\";\nimport upload from \"../middlewares/upload-file\";\nimport {\n  changePasswordLimiter,\n  deleteAccountLimiter,\n  resetPasswordLimiter,\n  signinRateLimiter,\n  signupRateLimiter\n} from \"../middlewares/rate-limiter\";\nimport { checkUserAccountRestriction } from \"../middlewares/user-account-restriction\";\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  loginUser\n);\n\nrouter.get(\"/profile\", verifyAuthentication, getUserProfile);\n\nrouter.patch(\n  \"/profile\",\n  verifyAuthentication,\n  upload.single(\"avatar\"),\n  validateRequest(UpdateProfileSchema),\n  checkUserAccountRestriction,\n  updateProfile\n);\n\nrouter.post(\"/refresh-token\", refreshToken);\n\nrouter.post(\n  \"/forgot-password\",\n  validateRequest(VerifyOtpSchema.pick({ email: true })),\n  forgotPassword\n);\n\nrouter.post(\n  \"/verify-forgot-password\",\n  validateRequest(VerifyOtpSchema),\n  verifyForgotPasswordOtp\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  changePasswordLimiter,\n  checkUserAccountRestriction,\n  changePassword\n);\n\nrouter.post(\"/logout\", verifyAuthentication, logoutUser);\n\nrouter.delete(\n  \"/delete-account\",\n  verifyAuthentication,\n  validateRequest(DeleteAccountSchema),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  deleteAccount\n);\n\nrouter.put(\"/reactivate-account\", verifyAuthentication, reactivateAccount);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/otp.service.ts",
                          "content": "import redis from \"../configs/redis\";\nimport {\n  OTP_CODE_LENGTH,\n  OTP_COOL_DOWN,\n  OTP_EXPIRES_IN,\n  OTP_MAX_COUNTS,\n  OTP_SPAM_LOCK\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};\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    const otpRequestKey = `otp_request_count:${email}`;\n    let otpRequestsCount = parseInt((await redis.get(otpRequestKey)) || \"0\");\n    if (otpRequestsCount >= OTP_MAX_COUNTS) {\n      await redis.set(`otp_spam_lock:${email}`, \"locked\", {\n        EX: OTP_SPAM_LOCK / 1000\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      EX: OTP_SPAM_LOCK / 1000\n    });\n  }\n\n  static async sendOtp({\n    name,\n    email,\n    templateName,\n    code,\n    hashCode\n  }: SendOtpType) {\n    const newOtp = generateOTP(OTP_CODE_LENGTH);\n\n    await sendEmail({\n      email,\n      subject:\n        templateName === \"email-verification\"\n          ? \"Verify your email\"\n          : \"Verify your OTP\",\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      EX: OTP_EXPIRES_IN / 1000\n    });\n\n    await redis.set(`otp_cooldown:${email}`, OTP_COOL_DOWN, {\n      EX: OTP_COOL_DOWN\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_COUNTS) {\n        await redis.set(`otp_lock:${email}`, \"locked\", {\n          EX: OTP_SPAM_LOCK / 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_COUNTS - 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 { eq } from \"drizzle-orm\";\nimport db from \"../configs/db\";\nimport { NewUser, users } from \"../drizzle\";\nimport { AuthService, CookieOptionsType } from \"./auth.service\";\n\nexport class OAuthService {\n  static async handleOAuthLogin(user: NewUser, context: CookieOptionsType) {\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.email, user.email));\n    if (existingUser) {\n      await db\n        .update(users)\n        .set({\n          provider: user.provider,\n          providerId: user.providerId,\n          avatar: {\n            url: user.avatar?.url || existingUser.avatar?.url\n          }\n        })\n        .where(eq(users.id, existingUser.id));\n      await AuthService.handleUserToken(\n        {\n          id: existingUser.id,\n          role: existingUser.role\n        },\n        context\n      );\n      return existingUser;\n    }\n\n    const [newUser] = await db\n      .insert(users)\n      .values({\n        name: user.name,\n        email: user.email,\n        isEmailVerified: user.isEmailVerified,\n        provider: user.provider,\n        providerId: user.providerId\n      })\n      .$returningId();\n\n    await AuthService.handleUserToken(\n      {\n        id: newUser.id,\n        role: \"user\"\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 { eq } from \"drizzle-orm\";\nimport db from \"../configs/db\";\nimport {\n  ChangePasswordType,\n  DeleteAccountType,\n  ResetPasswordType,\n  SigninUserType,\n  SignupUserType,\n  VerifyOtpType\n} from \"../validators/auth\";\nimport { ApiError } from \"../utils/api-error\";\nimport { refreshTokens, users } from \"../drizzle\";\nimport { hashPassword, verifyPassword } from \"../helpers/auth.helpers\";\nimport {\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} from \"../constants/auth\";\nimport { OtpService } from \"./otp.service\";\nimport { generateHashedToken, generateOTP } from \"../helpers/token.helpers\";\nimport redis from \"../configs/redis\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../utils/jwt\";\nimport {\n  deleteFileFromCloudinary,\n  uploadToCloudinary\n} from \"./cloudinary.service\";\nimport { IUser } from \"../types/user\";\n\nexport type CookieOptionsType = {\n  setAuthCookie?: (accessToken: string, refreshToken: string) => void;\n};\n\nexport class AuthService {\n  static async signupUser(user: SignupUserType) {\n    const { name, email, password, role } = user;\n    const existingUser = await db\n      .select()\n      .from(users)\n      .where(eq(users.email, email));\n\n    if (existingUser.length > 0) {\n      throw ApiError.conflict(\"User already exists with this email\");\n    }\n\n    await OtpService.checkOtpRestrictions(email);\n    await OtpService.trackOtpRequests(email);\n    const hashedPassword = await hashPassword(password);\n    const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n    const redisKey = `user:${email}:${hashCode}`;\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    });\n\n    await redis.set(redisKey, userData, {\n      EX: OTP_EXPIRES_IN / 1000\n    });\n\n    return;\n  }\n\n  static async verifyUser({ email, code }: VerifyOtpType) {\n    const hashCode = generateHashedToken(code);\n\n    await OtpService.verifyOtp(hashCode, email);\n\n    const userData = await redis.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 [existingUser] = await db\n      .insert(users)\n      .values({\n        name,\n        email: userEmail,\n        role,\n        password,\n        isEmailVerified: true\n      })\n      .$returningId();\n\n    await redis.del(`user:${email}:${hashCode}`);\n\n    return {\n      id: existingUser.id,\n      name,\n      email,\n      role: role || \"user\",\n      isEmailVerified: true\n    };\n  }\n\n  static async loginUser(user: SigninUserType, setCookie: CookieOptionsType) {\n    const { email, password } = user;\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.email, email));\n\n    if (!existingUser) {\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    if (!existingUser.isEmailVerified) {\n      throw ApiError.unauthorized(\"Email not verified\");\n    }\n\n    if (existingUser.lockUntil && new Date() < existingUser.lockUntil) {\n      throw ApiError.forbidden(\n        `Your account has been locked. Please try again after ${Math.ceil((existingUser.lockUntil.getTime() - Date.now()) / (1000 * 60))} minutes.`\n      );\n    }\n\n    const isPasswordValid = await verifyPassword(\n      password,\n      existingUser.password || \"\"\n    );\n\n    if (!isPasswordValid) {\n      let lockUntil = null;\n\n      let newAttempts = existingUser.failedLoginAttempts + 1;\n\n      if (newAttempts >= LOGIN_MAX_ATTEMPTS) {\n        lockUntil = new Date(Date.now() + LOCK_TIME_MS);\n      }\n\n      await db\n        .update(users)\n        .set({\n          failedLoginAttempts: newAttempts,\n          lockUntil\n        })\n        .where(eq(users.id, existingUser.id));\n\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    await db\n      .update(users)\n      .set({\n        failedLoginAttempts: 0,\n        lockUntil: null,\n        lastLoginAt: new Date()\n      })\n      .where(eq(users.id, existingUser.id));\n\n    await AuthService.handleUserToken(\n      {\n        id: existingUser.id,\n        role: existingUser.role\n      },\n      setCookie\n    );\n\n    return {\n      id: existingUser.id,\n      name: existingUser.name,\n      email: existingUser.email,\n      role: existingUser.role,\n      isEmailVerified: existingUser.isEmailVerified\n    };\n  }\n\n  static async getUserProfile(userId: number) {\n    const [user] = await db.select().from(users).where(eq(users.id, userId));\n    return {\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  static async updateUserProfile(\n    userId: number,\n    { name, avatar }: { name: string; avatar?: Express.Multer.File | undefined }\n  ) {\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.id, userId));\n    if (!existingUser) {\n      throw ApiError.unauthorized(\"Unauthorized\");\n    }\n\n    if (existingUser?.avatar?.public_id) {\n      await deleteFileFromCloudinary([existingUser.avatar.public_id]);\n    }\n\n    let avatarUrl;\n\n    if (avatar?.buffer) {\n      const file = await uploadToCloudinary(avatar.buffer, {\n        folder: \"uploads/files\",\n        resource_type: \"auto\"\n      });\n      avatarUrl = file.url;\n\n      await db\n        .update(users)\n        .set({\n          avatar: {\n            public_id: file.public_id,\n            url: file.url,\n            size: file.size\n          }\n        })\n        .where(eq(users.id, userId));\n    }\n\n    if (name) {\n      await db.update(users).set({ name }).where(eq(users.id, userId));\n    }\n\n    return {\n      name,\n      email: existingUser.email,\n      role: existingUser.role,\n      avatar: avatarUrl,\n      isEmailVerified: existingUser.isEmailVerified,\n      lastLoginAt: existingUser.lastLoginAt\n    };\n  }\n\n  static async handleUserToken(\n    user: Pick<IUser, \"id\" | \"role\">,\n    context: CookieOptionsType\n  ) {\n    const accessToken = generateAccessToken({\n      id: user.id,\n      role: user.role\n    });\n\n    const newRefreshToken = generateRefreshToken(user.id);\n\n    const hashedNewRefreshToken = generateHashedToken(newRefreshToken);\n\n    await db.insert(refreshTokens).values({\n      userId: user.id,\n      tokenHash: hashedNewRefreshToken,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    context.setAuthCookie &&\n      context.setAuthCookie(accessToken, newRefreshToken);\n  }\n\n  static async refreshTokens(accessToken: string, refreshToken: string) {\n    if (!refreshToken) {\n      throw ApiError.unauthorized(\"Unauthorized, please login.\");\n    }\n\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n    if (!decodedRefresh?.userId) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const [storedToken] = await db\n      .select()\n      .from(refreshTokens)\n      .where(eq(refreshTokens.tokenHash, refreshTokenHash));\n\n    // Reuse detection\n    if (!storedToken) {\n      await db\n        .update(refreshTokens)\n        .set({ isRevoked: true, revokedAt: new Date() })\n        .where(eq(refreshTokens.tokenHash, refreshTokenHash));\n      throw ApiError.unauthorized(\"Token reuse detected. Please login again.\");\n    }\n    if (storedToken.isRevoked) {\n      throw ApiError.unauthorized(\"Refresh token revoked.\");\n    }\n\n    if (storedToken.expiresAt < new Date()) {\n      throw ApiError.unauthorized(\"Refresh token expired.\");\n    }\n\n    if (accessToken) {\n      const decodedAccess = verifyAccessToken(accessToken);\n      if (decodedAccess.id !== decodedRefresh.userId) {\n        throw ApiError.unauthorized(\"Token mismatch.\");\n      }\n    }\n\n    const [user] = await db\n      .select()\n      .from(users)\n      .where(eq(users.id, decodedRefresh.userId));\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\n    });\n\n    const newRefreshToken = generateRefreshToken(user.id);\n\n    const hashedNewRefreshToken = generateHashedToken(newRefreshToken);\n\n    await db\n      .update(refreshTokens)\n      .set({\n        isRevoked: true,\n        revokedAt: new Date(),\n        replacedByTokenHash: hashedNewRefreshToken\n      })\n      .where(eq(refreshTokens.tokenHash, refreshTokenHash));\n\n    await db.insert(refreshTokens).values({\n      userId: user.id,\n      tokenHash: hashedNewRefreshToken,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    return {\n      accessToken: newAccessToken,\n      refreshToken: newRefreshToken\n    };\n  }\n\n  static async forgotPassword(email: string) {\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.email, email));\n\n    if (!existingUser) {\n      throw ApiError.badRequest(\n        \"If this email is registered, check your inbox.\"\n      );\n    }\n\n    await OtpService.checkOtpRestrictions(email);\n    await OtpService.trackOtpRequests(email);\n\n    const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n    await OtpService.sendOtp({\n      name: existingUser.name,\n      email,\n      templateName: \"forgot-password\",\n      code,\n      hashCode\n    });\n  }\n\n  static async verifyForgotPasswordOtp({ code, email }: VerifyOtpType) {\n    const hashCode = generateHashedToken(code);\n    await OtpService.verifyOtp(hashCode, email);\n    const redisKey = `forgot-password:${email}`;\n    await redis.set(redisKey, \"true\", {\n      EX: 60 * 5 // 5 minutes\n    });\n  }\n\n  static async resetPassword({ email, newPassword }: ResetPasswordType) {\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.email, email));\n\n    if (!existingUser) {\n      throw ApiError.badRequest(\n        \"If this email is registered, check your inbox.\"\n      );\n    }\n\n    const redisKey = `forgot-password:${email}`;\n    const isResetTokenValid = await redis.get(redisKey);\n    if (!isResetTokenValid) {\n      throw ApiError.badRequest(\"Invalid or expired reset token.\");\n    }\n\n    const isOldPassword = await verifyPassword(\n      newPassword,\n      existingUser.password || \"\"\n    );\n\n    if (isOldPassword) {\n      throw ApiError.badRequest(`New password cannot be same as old password.`);\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n\n    await db\n      .update(users)\n      .set({\n        password: hashedPassword\n      })\n      .where(eq(users.email, email));\n\n    await redis.del(redisKey);\n  }\n\n  static async logoutUser(userId: number) {\n    await db.delete(refreshTokens).where(eq(refreshTokens.userId, userId));\n  }\n\n  static async changePassword(\n    userId: number,\n    { oldPassword, newPassword }: ChangePasswordType\n  ) {\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.id, userId));\n\n    if (!existingUser) {\n      throw ApiError.unauthorized(\"Unauthorized\");\n    }\n\n    if (!existingUser.isEmailVerified) {\n      throw ApiError.unauthorized(\"Please verify your email first.\");\n    }\n\n    const isPasswordValid = await verifyPassword(\n      oldPassword,\n      existingUser.password || \"\"\n    );\n\n    if (!isPasswordValid) {\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    const isOldPassword = await verifyPassword(\n      newPassword,\n      existingUser.password || \"\"\n    );\n\n    if (isOldPassword) {\n      throw ApiError.badRequest(`New password cannot be same as old password.`);\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n\n    await db\n      .update(users)\n      .set({\n        password: hashedPassword\n      })\n      .where(eq(users.id, userId));\n  }\n\n  static async deleteAccount({ userId, type }: DeleteAccountType) {\n    if (type === \"soft\") {\n      await db\n        .update(users)\n        .set({\n          isDeleted: true,\n          deletedAt: new Date(),\n          reActivateAvailableAt: new Date(\n            Date.now() + REACTIVATION_AVAILABLE_AT\n          )\n        })\n        .where(eq(users.id, userId));\n    } else {\n      await db.delete(users).where(eq(users.id, userId));\n    }\n  }\n\n  static async reactivateAccount(userId: number) {\n    const [user] = await db.select().from(users).where(eq(users.id, userId));\n\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized, user not found\");\n    }\n\n    if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n      throw ApiError.badRequest(\n        `Your account has been locked. Please try again after ${Math.ceil(\n          (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n        )} minutes.`\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.unauthorized(\n        `Reactivation not available yet. Please try again after ${Math.ceil(\n          (user.reActivateAvailableAt.getTime() - Date.now()) / (1000 * 60)\n        )} minutes.`\n      );\n    }\n\n    await db\n      .update(users)\n      .set({\n        isDeleted: false,\n        deletedAt: null,\n        reActivateAvailableAt: null\n      })\n      .where(eq(users.id, userId));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/verify-auth.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../utils/jwt\";\nimport { ApiError } from \"../utils/api-error\";\nimport { logger } from \"../utils/logger\";\nimport env from \"../configs/env\";\nimport { UserRequest } from \"../types/user\";\nimport { setAuthCookies } from \"../helpers/cookie.helper\";\nimport { generateHashedToken } from \"../helpers/token.helpers\";\nimport db from \"../configs/db\";\nimport { refreshTokens, users } from \"../drizzle\";\nimport { eq } from \"drizzle-orm\";\n\nconst REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000;\n\nexport async function verifyAuthentication(\n  req: UserRequest,\n  res: Response,\n  next: NextFunction\n): Promise<void> {\n  const accessToken = req.cookies?.accessToken;\n  const refreshToken = req.cookies?.refreshToken;\n\n  //? 1. Try access token\n  if (accessToken) {\n    try {\n      const decoded = verifyAccessToken(accessToken);\n      req.user = decoded;\n      return next();\n    } catch {\n      logger.warn(\"Access token expired or invalid, attempting refresh\");\n    }\n  }\n\n  //? 2. Refresh token required\n  if (!refreshToken) {\n    return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n  }\n\n  try {\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n    if (!decodedRefresh?.userId) {\n      return next(ApiError.unauthorized(\"Invalid refresh token.\"));\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const [storedToken] = await db\n      .select()\n      .from(refreshTokens)\n      .where(eq(refreshTokens.tokenHash, refreshTokenHash));\n\n    //? Reuse detection\n    if (!storedToken) {\n      await db\n        .update(refreshTokens)\n        .set({\n          isRevoked: true,\n          revokedAt: new Date()\n        })\n        .where(eq(refreshTokens.userId, decodedRefresh.userId));\n\n      return next(\n        ApiError.unauthorized(\"Token reuse detected. Please login again.\")\n      );\n    }\n\n    if (storedToken.isRevoked) {\n      return next(ApiError.unauthorized(\"Refresh token revoked.\"));\n    }\n\n    if (storedToken.expiresAt < new Date()) {\n      return next(ApiError.unauthorized(\"Refresh token expired.\"));\n    }\n\n    const [user] = await db\n      .select({ id: users.id, role: users.role })\n      .from(users)\n      .where(eq(users.id, decodedRefresh.userId));\n    if (!user) {\n      return next(ApiError.unauthorized(\"User not found.\"));\n    }\n\n    //? 3. Rotate tokens\n    const newAccessToken = generateAccessToken({\n      id: user.id,\n      role: user.role\n    });\n\n    const newRefreshToken = generateRefreshToken(user.id);\n    const newRefreshTokenHash = generateHashedToken(newRefreshToken);\n\n    await db\n      .update(refreshTokens)\n      .set({\n        isRevoked: true,\n        revokedAt: new Date(),\n        replacedByTokenHash: newRefreshTokenHash\n      })\n      .where(eq(refreshTokens.id, storedToken.id));\n\n    await db.insert(refreshTokens).values({\n      userId: user.id,\n      tokenHash: newRefreshTokenHash,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    setAuthCookies(res, newAccessToken, newRefreshToken);\n\n    req.user = {\n      id: user.id,\n      role: user.role\n    };\n\n    return next();\n  } catch (err) {\n    logger.warn(\"Refresh token verification failed\");\n    return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\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\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/user-account-restriction.ts",
                          "content": "import { NextFunction, Response } from \"express\";\nimport { UserRequest } from \"../types/user\";\nimport { ApiError } from \"../utils/api-error\";\nimport { logger } from \"../utils/logger\";\nimport db from \"../configs/db\";\nimport { users } from \"../drizzle\";\nimport { eq } from \"drizzle-orm\";\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\n      .select()\n      .from(users)\n      .where(eq(users.id, req.user.id))\n      .limit(1);\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 minutesLeft = Math.ceil(\n        (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n      );\n\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${minutesLeft} minutes.`\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  console.log({ file });\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/rate-limiter.ts",
                          "content": "import { 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    statusCode: 429\n  },\n  handler: (req, res, next, options) => {\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() %> ServerCN. 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() %> ServerCN. 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/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 { ACCESS_TOKEN_EXPIRY, REFRESH_TOKEN_EXPIRY } 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) {\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    }\n  ]);\n}\n\nexport function clearAuthCookies(res: Response) {\n  res.clearCookie(\"accessToken\", COOKIE_OPTIONS);\n  res.clearCookie(\"refreshToken\", COOKIE_OPTIONS);\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};\n\nexport function setCookies(res: Response, cookies: Cookie[]) {\n  cookies.forEach(({ cookie, value, maxAge }) => {\n    res.cookie(cookie, value, {\n      ...COOKIE_OPTIONS,\n      maxAge\n    });\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/helpers/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/docs/swagger.json",
                          "content": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"Stateless Auth API\",\n    \"description\": \"Stateless Auth API\",\n    \"version\": \"1.0.0\"\n  },\n  \"host\": \"localhost:8000\",\n  \"basePath\": \"/\",\n  \"schemes\": [\"http\"],\n  \"paths\": {\n    \"/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/verify-user\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/signin\": {\n      \"post\": {\n        \"description\": \"\",\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        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\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    \"/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/verify-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    \"/logout\": {\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/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/verify-user\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/signin\": {\n      \"post\": {\n        \"description\": \"\",\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        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ],\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/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/verify-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/logout\": {\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\";\nexport * from \"./schemas/refresh-token.schema\";\n"
                        },
                        {
                          "type": "file",
                          "path": "src/cron/cleanup-refresh-tokens.cron.ts",
                          "content": "import cron from \"node-cron\";\nimport { lt, or, eq } from \"drizzle-orm\";\nimport { refreshTokens } from \"../drizzle\";\nimport db from \"../configs/db\";\nimport { logger } from \"../utils/logger\";\n\nexport function startRefreshTokenCleanupJob() {\n  cron.schedule(\n    \"0 2 * * *\", // daily at 2 am\n    async () => {\n      try {\n        const now = new Date();\n\n        const [result] = await db\n          .delete(refreshTokens)\n          .where(\n            or(\n              lt(refreshTokens.expiresAt, now),\n              eq(refreshTokens.isRevoked, true)\n            )\n          );\n\n        logger.info(\n          `Refresh token cleanup completed. Deleted ${result.affectedRows} records`\n        );\n      } catch (error) {\n        logger.error(error, \"Refresh token cleanup failed\");\n      }\n    },\n    {\n      timezone: \"Asia/Kathmandu\"\n    }\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_MAX_COUNTS = 3;\n\nexport const OTP_TYPES = [\n  \"signin\",\n  \"email-verification\",\n  \"password-reset\",\n  \"password-change\"\n] as const;\n\nexport const OTP_COOL_DOWN = 60;\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_EXPIRES_IN = 5 * 60 * 1000; // 5 minutes\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 RESET_PASSWORD_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n\nexport const REACTIVATION_AVAILABLE_AT = 24 * 60 * 60 * 1000; // 24 hours\n\nexport const OTP_SPAM_LOCK = 1 * 60 * 60 * 1000; // 1 hour\n"
                        },
                        {
                          "type": "file",
                          "path": "src/controllers/oauth.controller.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport { Profile as GithubProfile } from \"passport-github2\";\n\nimport { Profile as GoogleProfile } from \"passport-google-oauth20\";\n\nimport { ApiResponse } from \"../utils/api-response\";\nimport { AsyncHandler } from \"../utils/async-handler\";\nimport { ApiError } from \"../utils/api-error\";\nimport { setAuthCookies } from \"../helpers/cookie.helper\";\nimport { OAuthService } from \"../services/oauth.service\";\nimport { NewUser } from \"../drizzle\";\n\nconst getProvider: Record<string, \"github\" | \"google\" | \"local\"> = {\n  github: \"github\",\n  google: \"google\"\n};\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: NewUser = {\n      provider: getProvider[data?.provider],\n      providerId: data.id,\n      name: data.displayName,\n      email: data.emails ? (data.emails[0].value as string) : \"\",\n      isEmailVerified: true,\n      avatar: {\n        url: data.photos ? (data.photos[0].value as string) : \"\"\n      }\n    };\n\n    const result = await OAuthService.handleOAuthLogin(user, {\n      setAuthCookie: (accessToken: string, refreshToken: string) => {\n        setAuthCookies(res, accessToken, refreshToken);\n      }\n    });\n\n    ApiResponse.ok(res, \"Auth Successfull\", {\n      user: {\n        ...user,\n        id: result.id\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: NewUser = {\n      provider: getProvider[data?.provider],\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails ? (data.emails[0].value as string) : \"\",\n      isEmailVerified: data?.emails\n        ? (data.emails[0].verified as boolean)\n        : false,\n      avatar: {\n        url: data.profileUrl || (data.photos ? data.photos[0].value : \"\")\n      }\n    };\n\n    const result = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (accessToken: string, refreshToken: string) => {\n        setAuthCookies(res, accessToken, refreshToken);\n      }\n    });\n\n    ApiResponse.ok(res, \"Auth Successfull\", {\n      user: {\n        ...userInfo,\n        id: result.id\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\";\n\n/**\n * Basic health check endpoint\n * GET /api/health\n */\nexport const healthCheck = 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 * Detailed health check with system information\n * GET /api/health/detailed\n */\nexport const detailedHealthCheck = 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) / 100,\n      total:\n        Math.round((process.memoryUsage().heapTotal / 1024 / 1024) * 100) / 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"
                        },
                        {
                          "type": "file",
                          "path": "src/controllers/auth.controller.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport { AsyncHandler } from \"../utils/async-handler\";\nimport {\n  ChangePasswordType,\n  DeleteAccountType,\n  ResetPasswordType,\n  SigninUserType,\n  SignupUserType,\n  VerifyOtpType\n} from \"../validators/auth\";\nimport { ApiResponse } from \"../utils/api-response\";\nimport { ApiError } from \"../utils/api-error\";\nimport { AuthService } from \"../services/auth.service\";\nimport { clearAuthCookies, setAuthCookies } from \"../helpers/cookie.helper\";\nimport { UserRequest } from \"../types/user\";\n\n//? SIGNUP USER\nexport const signupUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, password, role, name }: SignupUserType = req.body;\n\n    if (!name || !email || !password) {\n      return next(ApiError.badRequest(\"Name, email and password are required\"));\n    }\n\n    await AuthService.signupUser({\n      email,\n      password,\n      role,\n      name\n    });\n\n    return ApiResponse.created(\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, code }: VerifyOtpType = req.body;\n\n    if (!email || !code) {\n      return next(ApiError.badRequest(\"Email and code are required\"));\n    }\n\n    await AuthService.verifyUser({ email, code });\n\n    return ApiResponse.ok(res, \"User verified successfully\");\n  }\n);\n\n//? LOGIN USER\nexport const loginUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, password }: SigninUserType = req.body;\n\n    if (!email || !password) {\n      return next(ApiError.badRequest(\"Email and password are required\"));\n    }\n\n    const result = await AuthService.loginUser(\n      {\n        email,\n        password\n      },\n      {\n        setAuthCookie(accessToken, refreshToken) {\n          setAuthCookies(res, accessToken, refreshToken);\n        }\n      }\n    );\n\n    if (!result) {\n      return next(ApiError.server(\"User login failed\"));\n    }\n\n    return ApiResponse.ok(res, \"User logged in successfully\", result);\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    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(userId);\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    return ApiResponse.ok(res, \"User profile fetched successfully\", {\n      user: {\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//? UPDATE PROFILE\nexport const updateProfile = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { name } = req.body;\n\n    if (!req.user?.id) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(req.user.id);\n\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    const updatedUser = await AuthService.updateUserProfile(req.user.id, {\n      name,\n      avatar: req.file\n    });\n\n    if (!updatedUser) {\n      return next(ApiError.server(\"Failed to update profile\"));\n    }\n\n    return ApiResponse.Success(\n      res,\n      \"Profile updated successfully!\",\n      updatedUser\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);\n\n    return ApiResponse.Success(res, \"Tokens refreshed 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(res, `Otp sent to ${email} successfully!`);\n  }\n);\n\n//? VERIFY FORGOT PASSWORD OTP\nexport const verifyForgotPasswordOtp = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, code }: VerifyOtpType = req.body;\n    if (!email || !code) {\n      return next(ApiError.badRequest(\"Email and code are required!\"));\n    }\n\n    await AuthService.verifyForgotPasswordOtp({ email, code });\n\n    return ApiResponse.ok(res, `Otp verified successfully!`);\n  }\n);\n\n//? RESET PASSWORD\nexport const resetPassword = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, newPassword }: ResetPasswordType = req.body;\n    if (!email || !newPassword) {\n      return next(ApiError.badRequest(\"Email and password are required!\"));\n    }\n\n    await AuthService.resetPassword({ email, newPassword });\n\n    return ApiResponse.ok(res, `Password reset successfully!`);\n  }\n);\n\n//? CHANGE PASSWORD\nexport const changePassword = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { oldPassword, newPassword }: ChangePasswordType = req.body;\n    if (!oldPassword || !newPassword) {\n      return next(\n        ApiError.badRequest(\"Old password and new password are required!\")\n      );\n    }\n\n    const userId = req.user?.id;\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.changePassword(userId, {\n      oldPassword,\n      newPassword\n    });\n\n    return ApiResponse.ok(res, `Password changed 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    await AuthService.logoutUser(userId);\n    clearAuthCookies(res);\n\n    return ApiResponse.ok(res, \"User logged out successfully!\");\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\n    if (userId !== reqUserId) {\n      return next(\n        ApiError.unauthorized(\"you are not authorized to perform this action\")\n      );\n    }\n\n    await AuthService.deleteAccount({ userId, type });\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 reqUserId = req?.user?.id;\n\n    if (!reqUserId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.reactivateAccount(reqUserId);\n\n    return ApiResponse.Success(res, `Account reactivated successfully!`);\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/redis.ts",
                          "content": "import { createClient } from \"redis\";\nimport env from \"./env\";\nimport { logger } from \"../utils/logger\";\n\nlet redisUrl: string = env.REDIS_URL!;\n\nif (!redisUrl) {\n  throw new Error(\"REDIS_URL is not defined\");\n}\n\nconst redis = createClient({\n  url: redisUrl\n});\n\nexport default redis;\n\nexport function connectRedis() {\n  redis\n    .connect()\n    .then(() => {\n      logger.info(\"Redis connected\");\n    })\n    .catch(err => {\n      logger.error(err, \"Redis connection error!\");\n    });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/passport.ts",
                          "content": "import 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 env from \"./env\";\n\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\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"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/nodemailer.ts",
                          "content": "import nodemailer from \"nodemailer\";\nimport \"dotenv/config\";\nimport env from \"./env\";\n\nlet transporter: nodemailer.Transporter | null = null;\n\nexport function getTransporter() {\n  if (transporter) return transporter;\n  const host = env.SMTP_HOST;\n  const port = Number(env.SMTP_PORT || 465);\n  const user = env.SMTP_USER;\n  const pass = env.SMTP_PASS;\n  const from = env.EMAIL_FROM;\n  if (!host || !user || !pass || !from) {\n    throw new Error(\"SMTP/EMAIL env not configured\");\n  }\n\n  transporter = nodemailer.createTransport({\n    host,\n    port,\n    secure: port === 465,\n    auth: { user, pass }\n  });\n  return transporter;\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/env.ts",
                          "content": "import \"dotenv-flow/config\";\nimport { z } from \"zod\";\n\nconst envSchema = z.object({\n  NODE_ENV: z\n    .enum([\"development\", \"test\", \"production\"])\n    .default(\"development\"),\n  PORT: z.string().default(\"9000\"),\n  DATABASE_URL: z.string(),\n\n  CORS_ORIGIN: z.string(),\n  LOG_LEVEL: z.string().default(\"info\"),\n\n  JWT_REFRESH_SECRET: z.string(),\n  JWT_ACCESS_SECRET: z.string(),\n\n  CRYPTO_SECRET: z.string(),\n\n  REDIS_URL: z.string(),\n\n  SMTP_HOST: z.string(),\n  SMTP_PORT: z.string(),\n  SMTP_USER: z.string(),\n  SMTP_PASS: z.string(),\n  EMAIL_FROM: z.string(),\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.string(),\n\n  GITHUB_CLIENT_ID: z.string(),\n  GITHUB_CLIENT_SECRET: z.string(),\n  GITHUB_REDIRECT_URI: z.string()\n});\nconst parsed = envSchema.safeParse(process.env);\n\nif (!parsed.success) {\n  console.error(\"Invalid environment variables\", z.treeifyError(parsed.error));\n  process.exit(1);\n}\n\nconst env = parsed.data;\n\nexport default env;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/db.ts",
                          "content": "import { drizzle } from \"drizzle-orm/mysql2\";\nimport env from \"./env\";\n\nconst db = drizzle(env.DATABASE_URL!, {\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  mysqlTable,\n  serial,\n  varchar,\n  boolean,\n  timestamp,\n  int,\n  json,\n  uniqueIndex,\n  index,\n  mysqlEnum\n} from \"drizzle-orm/mysql-core\";\nimport { timestamps } from \"./schema.helper\";\nimport { relations } from \"drizzle-orm\";\nimport { refreshTokens } from \"./refresh-token.schema\";\n\nexport interface IAvatar {\n  public_id?: string;\n  url: string;\n  size?: number;\n}\n\nexport const users = mysqlTable(\n  \"users\",\n  {\n    id: serial(\"id\").primaryKey(),\n    name: varchar(\"name\", { length: 100 }).notNull(),\n    email: varchar(\"email\", { length: 255 }).notNull().unique(),\n    password: varchar(\"password\", { length: 255 }),\n    role: mysqlEnum(\"role\", [\"user\", \"admin\"]).default(\"user\").notNull(),\n\n    provider: mysqlEnum(\"provider\", [\"local\", \"google\", \"github\"])\n      .default(\"local\")\n      .notNull(),\n    providerId: varchar(\"provider_id\", { length: 255 }),\n\n    avatar: json(\"avatar\").$type<IAvatar>(),\n\n    isEmailVerified: boolean(\"is_email_verified\").default(false).notNull(),\n    lastLoginAt: timestamp(\"last_login_at\"),\n    failedLoginAttempts: int(\"failed_login_attempts\").default(0).notNull(),\n    lockUntil: timestamp(\"lock_until\"),\n\n    isDeleted: boolean(\"is_deleted\").default(false).notNull(),\n    deletedAt: timestamp(\"deleted_at\"),\n    reActivateAvailableAt: timestamp(\"re_activate_available_at\"),\n\n    ...timestamps\n  },\n  table => [\n    uniqueIndex(\"email_idx\").on(table.email),\n    index(\"role_idx\").on(table.role),\n    index(\"is_deleted_idx\").on(table.isDeleted)\n  ]\n);\n\nexport const usersRelations = relations(users, ({ many }) => ({\n  refreshTokens: many(refreshTokens)\n}));\n\nexport type User = typeof users.$inferSelect;\nexport type NewUser = typeof users.$inferInsert;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/schemas/schema.helper.ts",
                          "content": "import { timestamp } from \"drizzle-orm/mysql-core\";\n\nexport const timestamps = {\n  createdAt: timestamp(\"created_at\", { mode: \"date\" }).defaultNow().notNull(),\n  updatedAt: timestamp(\"updated_at\", { mode: \"date\" })\n    .defaultNow()\n    .onUpdateNow()\n    .notNull()\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/schemas/refresh-token.schema.ts",
                          "content": "import {\n  mysqlTable,\n  serial,\n  varchar,\n  boolean,\n  timestamp,\n  index,\n  bigint,\n  text\n} from \"drizzle-orm/mysql-core\";\nimport { relations } from \"drizzle-orm\";\nimport { users } from \"./user.schema\";\nimport { timestamps } from \"./schema.helper\";\n\nexport const refreshTokens = mysqlTable(\n  \"refresh_tokens\",\n  {\n    id: serial(\"id\").primaryKey(),\n    userId: bigint(\"user_id\", { mode: \"number\", unsigned: true })\n      .references(() => users.id, { onDelete: \"cascade\" })\n      .notNull(),\n    tokenHash: text(\"token_hash\").notNull(),\n    expiresAt: timestamp(\"expires_at\").notNull(),\n    isRevoked: boolean(\"is_revoked\").default(false).notNull(),\n    revokedAt: timestamp(\"revoked_at\"),\n    replacedByTokenHash: varchar(\"replaced_by_token_hash\", { length: 255 }),\n    ...timestamps\n  },\n  table => [\n    index(\"user_id_idx\").on(table.userId),\n    index(\"token_hash_idx\").on(table.tokenHash),\n    index(\"is_revoked_idx\").on(table.isRevoked),\n    index(\"expires_at_idx\").on(table.expiresAt)\n  ]\n);\n\nexport const refreshTokensRelations = relations(\n  refreshTokens,\n  ({ one, many }) => {\n    return {\n      user: one(users, {\n        fields: [refreshTokens.userId],\n        references: [users.id]\n      })\n    };\n  }\n);\n\nexport type RefreshToken = typeof refreshTokens.$inferSelect;\nexport type NewRefreshToken = typeof refreshTokens.$inferInsert;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/migrations/0000_organic_arachne.sql",
                          "content": "CREATE TABLE `users` (\n\t`id` serial AUTO_INCREMENT NOT NULL,\n\t`name` varchar(100) NOT NULL,\n\t`email` varchar(255) NOT NULL,\n\t`password` varchar(255),\n\t`role` enum('user','admin') NOT NULL DEFAULT 'user',\n\t`provider` enum('local','google','github') NOT NULL DEFAULT 'local',\n\t`provider_id` varchar(255),\n\t`avatar` json,\n\t`is_email_verified` boolean NOT NULL DEFAULT false,\n\t`last_login_at` timestamp,\n\t`failed_login_attempts` int NOT NULL DEFAULT 0,\n\t`lock_until` timestamp,\n\t`is_deleted` boolean NOT NULL DEFAULT false,\n\t`deleted_at` timestamp,\n\t`re_activate_available_at` timestamp,\n\t`created_at` timestamp NOT NULL DEFAULT (now()),\n\t`updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,\n\tCONSTRAINT `users_id` PRIMARY KEY(`id`),\n\tCONSTRAINT `users_email_unique` UNIQUE(`email`),\n\tCONSTRAINT `email_idx` UNIQUE(`email`)\n);\n--> statement-breakpoint\nCREATE TABLE `refresh_tokens` (\n\t`id` serial AUTO_INCREMENT NOT NULL,\n\t`user_id` bigint unsigned NOT NULL,\n\t`token_hash` text NOT NULL,\n\t`expires_at` timestamp NOT NULL,\n\t`is_revoked` boolean NOT NULL DEFAULT false,\n\t`revoked_at` timestamp,\n\t`replaced_by_token_hash` varchar(255),\n\t`created_at` timestamp NOT NULL DEFAULT (now()),\n\t`updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,\n\tCONSTRAINT `refresh_tokens_id` PRIMARY KEY(`id`)\n);\n--> statement-breakpoint\nALTER TABLE `refresh_tokens` ADD CONSTRAINT `refresh_tokens_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint\nCREATE INDEX `role_idx` ON `users` (`role`);--> statement-breakpoint\nCREATE INDEX `is_deleted_idx` ON `users` (`is_deleted`);--> statement-breakpoint\nCREATE INDEX `user_id_idx` ON `refresh_tokens` (`user_id`);--> statement-breakpoint\nCREATE INDEX `token_hash_idx` ON `refresh_tokens` (`token_hash`);--> statement-breakpoint\nCREATE INDEX `is_revoked_idx` ON `refresh_tokens` (`is_revoked`);--> statement-breakpoint\nCREATE INDEX `expires_at_idx` ON `refresh_tokens` (`expires_at`);"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/migrations/meta/_journal.json",
                          "content": "{\n  \"version\": \"7\",\n  \"dialect\": \"mysql\",\n  \"entries\": [\n    {\n      \"idx\": 0,\n      \"version\": \"5\",\n      \"when\": 1768828194715,\n      \"tag\": \"0000_organic_arachne\",\n      \"breakpoints\": true\n    }\n  ]\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/migrations/meta/0000_snapshot.json",
                          "content": "{\n  \"version\": \"5\",\n  \"dialect\": \"mysql\",\n  \"id\": \"9e7c0bb7-17d0-4e0b-bc69-980208b82787\",\n  \"prevId\": \"00000000-0000-0000-0000-000000000000\",\n  \"tables\": {\n    \"users\": {\n      \"name\": \"users\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"serial\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"varchar(100)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"varchar(255)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"varchar(255)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"enum('user','admin')\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"enum('local','google','github')\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'local'\"\n        },\n        \"provider_id\": {\n          \"name\": \"provider_id\",\n          \"type\": \"varchar(255)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"avatar\": {\n          \"name\": \"avatar\",\n          \"type\": \"json\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"is_email_verified\": {\n          \"name\": \"is_email_verified\",\n          \"type\": \"boolean\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"last_login_at\": {\n          \"name\": \"last_login_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"failed_login_attempts\": {\n          \"name\": \"failed_login_attempts\",\n          \"type\": \"int\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"lock_until\": {\n          \"name\": \"lock_until\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"is_deleted\": {\n          \"name\": \"is_deleted\",\n          \"type\": \"boolean\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"re_activate_available_at\": {\n          \"name\": \"re_activate_available_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(now())\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"onUpdate\": true,\n          \"default\": \"(now())\"\n        }\n      },\n      \"indexes\": {\n        \"email_idx\": {\n          \"name\": \"email_idx\",\n          \"columns\": [\"email\"],\n          \"isUnique\": true\n        },\n        \"role_idx\": {\n          \"name\": \"role_idx\",\n          \"columns\": [\"role\"],\n          \"isUnique\": false\n        },\n        \"is_deleted_idx\": {\n          \"name\": \"is_deleted_idx\",\n          \"columns\": [\"is_deleted\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"users_id\": {\n          \"name\": \"users_id\",\n          \"columns\": [\"id\"]\n        }\n      },\n      \"uniqueConstraints\": {\n        \"users_email_unique\": {\n          \"name\": \"users_email_unique\",\n          \"columns\": [\"email\"]\n        }\n      },\n      \"checkConstraint\": {}\n    },\n    \"refresh_tokens\": {\n      \"name\": \"refresh_tokens\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"serial\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"bigint unsigned\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token_hash\": {\n          \"name\": \"token_hash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"is_revoked\": {\n          \"name\": \"is_revoked\",\n          \"type\": \"boolean\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"revoked_at\": {\n          \"name\": \"revoked_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"replaced_by_token_hash\": {\n          \"name\": \"replaced_by_token_hash\",\n          \"type\": \"varchar(255)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(now())\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"onUpdate\": true,\n          \"default\": \"(now())\"\n        }\n      },\n      \"indexes\": {\n        \"user_id_idx\": {\n          \"name\": \"user_id_idx\",\n          \"columns\": [\"user_id\"],\n          \"isUnique\": false\n        },\n        \"token_hash_idx\": {\n          \"name\": \"token_hash_idx\",\n          \"columns\": [\"token_hash\"],\n          \"isUnique\": false\n        },\n        \"is_revoked_idx\": {\n          \"name\": \"is_revoked_idx\",\n          \"columns\": [\"is_revoked\"],\n          \"isUnique\": false\n        },\n        \"expires_at_idx\": {\n          \"name\": \"expires_at_idx\",\n          \"columns\": [\"expires_at\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"refresh_tokens_user_id_users_id_fk\": {\n          \"name\": \"refresh_tokens_user_id_users_id_fk\",\n          \"tableFrom\": \"refresh_tokens\",\n          \"tableTo\": \"users\",\n          \"columnsFrom\": [\"user_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"refresh_tokens_id\": {\n          \"name\": \"refresh_tokens_id\",\n          \"columns\": [\"id\"]\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraint\": {}\n    }\n  },\n  \"views\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"tables\": {},\n    \"indexes\": {}\n  }\n}\n"
                        }
                      ]
                    },
                    "feature": {
                      "files": [
                        {
                          "type": "file",
                          "path": "swagger.config.ts",
                          "content": "import swaggerAutoGen from \"swagger-autogen\";\n\nconst doc = {\n  info: {\n    title: \"Stateless Auth API\",\n    description: \"Stateless Auth API\",\n    version: \"1.0.0\"\n  },\n  host: `localhost:3000`,\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": "package.json",
                          "content": "{\n  \"name\": \"servercn-stateless-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    \"db:generate\": \"drizzle-kit generate\",\n    \"db:migrate\": \"drizzle-kit migrate\",\n    \"db:studio\": \"drizzle-kit studio\",\n    \"lint:check\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"format:check\": \"npx prettier . --check\",\n    \"format:fix\": \"npx prettier . --write\"\n  },\n  \"devDependencies\": {},\n  \"dependencies\": {}\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "drizzle.config.ts",
                          "content": "import { Config, defineConfig } from \"drizzle-kit\";\n\nimport env from \"./src/shared/configs/env\";\n\nexport default defineConfig({\n  out: \"./src/drizzle/migrations\",\n  schema: \"./src/drizzle/index.ts\",\n  dialect: \"mysql\",\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 { connectRedis } from \"./shared/configs/redis\";\nimport { logger } from \"./shared/utils/logger\";\nimport { configureGracefulShutdown } from \"./shared/utils/shutdown\";\n\nconst port = env.PORT || 9000;\n\nconnectRedis();\n\nconst 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\nconfigureGracefulShutdown(server);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/app.ts",
                          "content": "import express, { Express, Request, Response } from \"express\";\nimport cors from \"cors\";\nimport helmet from \"helmet\";\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 Routes from \"./routes/index\";\nimport { startRefreshTokenCleanupJob } from \"./cron/cleanup-refresh-tokens.cron\";\n\nimport { rateLimiter } from \"./shared/middlewares/rate-limiter\";\nimport { setupSwagger } from \"./shared/configs/swagger\";\nimport sourceMapSupport from \"source-map-support\";\nsourceMapSupport.install();\n\nconst app: Express = express();\n\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\napp.use(\n  cors({\n    origin: \"*\",\n    credentials: true\n  })\n);\napp.use(helmet());\napp.use(cookieParser());\napp.use(morgan(process.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\nstartRefreshTokenCleanupJob();\n\napp.use(rateLimiter);\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": "src/types/global.d.ts",
                          "content": "import { Request } from \"express\";\n\nexport interface UserRequest extends Request {\n  user?: {\n    id?: number | undefined;\n    role?: \"user\" | \"admin\" | undefined;\n  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": ".husky/pre-commit",
                          "content": "npx lint-staged"
                        },
                        {
                          "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() %> ServerCN. 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() %> ServerCN. 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/cron/cleanup-refresh-tokens.cron.ts",
                          "content": "import cron from \"node-cron\";\nimport { lt, or, eq } from \"drizzle-orm\";\nimport { refreshTokens } from \"../drizzle\";\nimport db from \"../shared/configs/db\";\nimport { logger } from \"../shared/utils/logger\";\n\nexport function startRefreshTokenCleanupJob() {\n  cron.schedule(\n    \"0 2 * * *\", // daily at 2 am\n    async () => {\n      try {\n        const now = new Date();\n\n        const [result] = await db\n          .delete(refreshTokens)\n          .where(\n            or(\n              lt(refreshTokens.expiresAt, now),\n              eq(refreshTokens.isRevoked, true)\n            )\n          );\n\n        logger.info(\n          `Refresh token cleanup completed. Deleted ${result.affectedRows} records`\n        );\n      } catch (error) {\n        logger.error(error, \"Refresh token cleanup failed\");\n      }\n    },\n    {\n      timezone: \"Asia/Kathmandu\"\n    }\n  );\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/docs/swagger.json",
                          "content": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"Stateless Auth API\",\n    \"description\": \"Stateless Auth API\",\n    \"version\": \"1.0.0\"\n  },\n  \"host\": \"localhost:8000\",\n  \"basePath\": \"/api\",\n  \"schemes\": [\"http\"],\n  \"paths\": {\n    \"/v1/health/\": {\n      \"get\": {\n        \"description\": \"Simple endpoint to verify service is running\",\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/signup\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        },\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              }\n            }\n          }\n        ]\n      }\n    },\n    \"/v1/auth/verify-user\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        },\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"email\": {\n                  \"example\": \"any\"\n                },\n                \"code\": {\n                  \"example\": \"any\"\n                }\n              }\n            }\n          }\n        ]\n      }\n    },\n    \"/v1/auth/signin\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        },\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      }\n    },\n    \"/v1/auth/profile\": {\n      \"get\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      },\n      \"patch\": {\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              }\n            }\n          }\n        ],\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/forgot-password\": {\n      \"post\": {\n        \"description\": \"\",\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\"\n          }\n        }\n      }\n    },\n    \"/v1/auth/verify-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/logout\": {\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  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/index.ts",
                          "content": "import { Router } from \"express\";\nimport authRoutes from \"../modules/auth/auth.routes\";\nimport healthRoutes from \"../modules/health/health.routes\";\nimport oauthRoutes from \"../modules/oauth/oauth.routes\";\n\nconst router = Router();\n\nrouter.use(\"/v1/health\", healthRoutes);\n\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 = \"/api/auth/google/callback\"\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/index.ts",
                          "content": "export * from \"./schemas/user.schema\";\nexport * from \"./schemas/refresh-token.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": "import env from \"../configs/env\";\nimport { getTransporter } from \"../configs/nodemailer\";\nimport { ApiError } from \"../utils/api-error\";\nimport { renderEmailTemplates } from \"./render-template\";\n\ntype sendMail = {\n  from?: string;\n  subject: string;\n  data: Record<string, any>;\n  email: string;\n  templateName: string;\n};\n\nexport async function sendEmail({\n  from,\n  email,\n  subject,\n  data,\n  templateName\n}: sendMail) {\n  const transporter = getTransporter();\n\n  const html = await renderEmailTemplates(templateName, data);\n  return transporter\n    .sendMail({\n      from: from || `<${env.EMAIL_FROM}>`,\n      to: email,\n      subject,\n      html\n    })\n    .catch(err => {\n      throw ApiError.badRequest(\"Failed to send email\");\n    });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/utils/render-template.ts",
                          "content": "import ejs from \"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 ACCESS_TOKEN_EXPIRY = \"15m\";\nconst REFRESH_TOKEN_EXPIRY = \"7d\";\n\nexport function generateAccessToken(user: { id: number; role: string }) {\n  return jwt.sign({ id: user.id, role: user.role }, env.JWT_ACCESS_SECRET!, {\n    expiresIn: ACCESS_TOKEN_EXPIRY\n  });\n}\n\nexport function generateRefreshToken(userId: number) {\n  return jwt.sign({ userId }, env.JWT_REFRESH_SECRET!, {\n    expiresIn: REFRESH_TOKEN_EXPIRY\n  });\n}\n\nexport function verifyAccessToken(token: string) {\n  return jwt.verify(token, env.JWT_ACCESS_SECRET!) as {\n    id: number;\n    role: \"user\" | \"admin\";\n  };\n}\n\nexport function verifyRefreshToken(token: string) {\n  return jwt.verify(token, env.JWT_REFRESH_SECRET!) as {\n    userId: number;\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 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": "import { NextFunction, Request, Response } from \"express\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../utils/jwt\";\nimport { ApiError } from \"../utils/api-error\";\nimport { logger } from \"../utils/logger\";\nimport { setAuthCookies } from \"../helpers/cookie.helper\";\nimport { generateHashedToken } from \"../helpers/token.helpers\";\nimport db from \"../configs/db\";\nimport { eq } from \"drizzle-orm\";\nimport { UserRequest } from \"../../types/global\";\nimport { refreshTokens, users } from \"../../drizzle\";\n\nconst REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000;\n\nexport async function verifyAuthentication(\n  req: UserRequest,\n  res: Response,\n  next: NextFunction\n): Promise<void> {\n  const accessToken = req.cookies?.accessToken;\n  const refreshToken = req.cookies?.refreshToken;\n\n  //? 1. Try access token\n  if (accessToken) {\n    try {\n      const decoded = verifyAccessToken(accessToken);\n      req.user = decoded;\n      return next();\n    } catch {\n      logger.warn(\"Access token expired or invalid, attempting refresh\");\n    }\n  }\n\n  //? 2. Refresh token required\n  if (!refreshToken) {\n    return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n  }\n\n  try {\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n    if (!decodedRefresh?.userId) {\n      return next(ApiError.unauthorized(\"Invalid refresh token.\"));\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const [storedToken] = await db\n      .select()\n      .from(refreshTokens)\n      .where(eq(refreshTokens.tokenHash, refreshTokenHash));\n\n    //? Reuse detection\n    if (!storedToken) {\n      await db\n        .update(refreshTokens)\n        .set({\n          isRevoked: true,\n          revokedAt: new Date()\n        })\n        .where(eq(refreshTokens.userId, decodedRefresh.userId));\n\n      return next(\n        ApiError.unauthorized(\"Token reuse detected. Please login again.\")\n      );\n    }\n\n    if (storedToken.isRevoked) {\n      return next(ApiError.unauthorized(\"Refresh token revoked.\"));\n    }\n\n    if (storedToken.expiresAt < new Date()) {\n      return next(ApiError.unauthorized(\"Refresh token expired.\"));\n    }\n\n    const [user] = await db\n      .select({ id: users.id, role: users.role })\n      .from(users)\n      .where(eq(users.id, decodedRefresh.userId));\n    if (!user) {\n      return next(ApiError.unauthorized(\"User not found.\"));\n    }\n\n    //? 3. Rotate tokens\n    const newAccessToken = generateAccessToken({\n      id: user.id,\n      role: user.role\n    });\n\n    const newRefreshToken = generateRefreshToken(user.id);\n    const newRefreshTokenHash = generateHashedToken(newRefreshToken);\n\n    await db\n      .update(refreshTokens)\n      .set({\n        isRevoked: true,\n        revokedAt: new Date(),\n        replacedByTokenHash: newRefreshTokenHash\n      })\n      .where(eq(refreshTokens.id, storedToken.id));\n\n    await db.insert(refreshTokens).values({\n      userId: user.id,\n      tokenHash: newRefreshTokenHash,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    setAuthCookies(res, newAccessToken, newRefreshToken);\n\n    req.user = {\n      id: user.id,\n      role: user.role\n    };\n\n    return next();\n  } catch (err) {\n    logger.warn(\"Refresh token verification failed\");\n    return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/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\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/user-account-restriction.ts",
                          "content": "import { NextFunction, Response } from \"express\";\nimport { ApiError } from \"../utils/api-error\";\nimport { logger } from \"../utils/logger\";\nimport db from \"../configs/db\";\nimport { eq } from \"drizzle-orm\";\nimport { users } from \"../../drizzle\";\nimport { UserRequest } from \"../../types/global\";\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\n      .select()\n      .from(users)\n      .where(eq(users.id, req.user.id))\n      .limit(1);\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 minutesLeft = Math.ceil(\n        (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n      );\n\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${minutesLeft} minutes.`\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  console.log({ file });\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/rate-limiter.ts",
                          "content": "import { 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    statusCode: 429\n  },\n  handler: (req, res, next, options) => {\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/configs/swagger.ts",
                          "content": "import swaggerUi from \"swagger-ui-express\";\nimport { Express } from \"express\";\nimport env from \"./env\";\nimport swaggerDocument from \"../../docs/swagger.json\";\n\nexport const setupSwagger = (app: Express) => {\n  if (env.NODE_ENV !== \"development\") return;\n  app.use(\"/api/docs\", swaggerUi.serve, swaggerUi.setup(swaggerDocument));\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/redis.ts",
                          "content": "import { createClient } from \"redis\";\nimport env from \"./env\";\nimport { logger } from \"../utils/logger\";\n\nlet redisUrl: string = env.REDIS_URL!;\n\nif (!redisUrl) {\n  throw new Error(\"REDIS_URL is not defined\");\n}\n\nconst redis = createClient({\n  url: redisUrl\n});\n\nexport default redis;\n\nexport function connectRedis() {\n  redis\n    .connect()\n    .then(() => {\n      logger.info(\"Redis connected\");\n    })\n    .catch(err => {\n      logger.error(err, \"Redis connection error!\");\n    });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/passport.ts",
                          "content": "import 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 env from \"./env\";\n\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\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"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/nodemailer.ts",
                          "content": "import nodemailer from \"nodemailer\";\nimport \"dotenv/config\";\nimport env from \"./env\";\n\nlet transporter: nodemailer.Transporter | null = null;\n\nexport function getTransporter() {\n  if (transporter) return transporter;\n  const host = env.SMTP_HOST;\n  const port = Number(env.SMTP_PORT || 465);\n  const user = env.SMTP_USER;\n  const pass = env.SMTP_PASS;\n  const from = env.EMAIL_FROM;\n  if (!host || !user || !pass || !from) {\n    throw new Error(\"SMTP/EMAIL env not configured\");\n  }\n\n  transporter = nodemailer.createTransport({\n    host,\n    port,\n    secure: port === 465,\n    auth: { user, pass }\n  });\n  return transporter;\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/env.ts",
                          "content": "import \"dotenv-flow/config\";\nimport { z } from \"zod\";\n\nconst envSchema = z.object({\n  NODE_ENV: z\n    .enum([\"development\", \"test\", \"production\"])\n    .default(\"development\"),\n  PORT: z.string().default(\"9000\"),\n  DATABASE_URL: z.string(),\n\n  CORS_ORIGIN: z.string(),\n  LOG_LEVEL: z.string().default(\"info\"),\n\n  JWT_REFRESH_SECRET: z.string(),\n  JWT_ACCESS_SECRET: z.string(),\n\n  CRYPTO_SECRET: z.string(),\n\n  REDIS_URL: z.string(),\n\n  SMTP_HOST: z.string(),\n  SMTP_PORT: z.string(),\n  SMTP_USER: z.string(),\n  SMTP_PASS: z.string(),\n  EMAIL_FROM: z.string(),\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.string(),\n\n  GITHUB_CLIENT_ID: z.string(),\n  GITHUB_CLIENT_SECRET: z.string(),\n  GITHUB_REDIRECT_URI: z.string()\n});\nconst parsed = envSchema.safeParse(process.env);\n\nif (!parsed.success) {\n  console.error(\"Invalid environment variables\", z.treeifyError(parsed.error));\n  process.exit(1);\n}\n\nconst env = parsed.data;\n\nexport default env;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/db.ts",
                          "content": "import { drizzle } from \"drizzle-orm/mysql2\";\nimport env from \"./env\";\n\nconst db = drizzle(env.DATABASE_URL!, {\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/otp/otp.service.ts",
                          "content": "import redis from \"../../shared/configs/redis\";\nimport {\n  OTP_CODE_LENGTH,\n  OTP_COOL_DOWN,\n  OTP_EXPIRES_IN,\n  OTP_MAX_COUNTS,\n  OTP_SPAM_LOCK\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};\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    const otpRequestKey = `otp_request_count:${email}`;\n    let otpRequestsCount = parseInt((await redis.get(otpRequestKey)) || \"0\");\n    if (otpRequestsCount >= OTP_MAX_COUNTS) {\n      await redis.set(`otp_spam_lock:${email}`, \"locked\", {\n        EX: OTP_SPAM_LOCK / 1000\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      EX: OTP_SPAM_LOCK / 1000\n    });\n  }\n\n  static async sendOtp({\n    name,\n    email,\n    templateName,\n    code,\n    hashCode\n  }: SendOtpType) {\n    const newOtp = generateOTP(OTP_CODE_LENGTH);\n\n    await sendEmail({\n      email,\n      subject:\n        templateName === \"email-verification\"\n          ? \"Verify your email\"\n          : \"Verify your OTP\",\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      EX: OTP_EXPIRES_IN / 1000\n    });\n\n    await redis.set(`otp_cooldown:${email}`, OTP_COOL_DOWN, {\n      EX: OTP_COOL_DOWN\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_COUNTS) {\n        await redis.set(`otp_lock:${email}`, \"locked\", {\n          EX: OTP_SPAM_LOCK / 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_COUNTS - failedAttempts} attempts left.`\n      );\n    }\n\n    await redis.del([`otp:${email}`, failedAttemptsKey]);\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/otp/oauth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport passport from \"passport\";\nimport { githubOAuth, googleOAuth } from \"../oauth/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  \"/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/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/modules/upload/cloudinary.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/oauth/oauth.service.ts",
                          "content": "import { eq } from \"drizzle-orm\";\nimport db from \"../../shared/configs/db\";\nimport { NewUser, users } from \"../../drizzle\";\nimport { AuthService, CookieOptionsType } from \"../auth/auth.service\";\n\nexport class OAuthService {\n  static async handleOAuthLogin(user: NewUser, context: CookieOptionsType) {\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.email, user.email));\n    if (existingUser) {\n      await db\n        .update(users)\n        .set({\n          provider: user.provider,\n          providerId: user.providerId,\n          avatar: {\n            url: user.avatar?.url || existingUser.avatar?.url\n          }\n        })\n        .where(eq(users.id, existingUser.id));\n      await AuthService.handleUserToken(\n        {\n          id: existingUser.id,\n          role: existingUser.role\n        },\n        context\n      );\n      return existingUser;\n    }\n\n    const [newUser] = await db\n      .insert(users)\n      .values({\n        name: user.name,\n        email: user.email,\n        isEmailVerified: user.isEmailVerified,\n        provider: user.provider,\n        providerId: user.providerId\n      })\n      .$returningId();\n\n    await AuthService.handleUserToken(\n      {\n        id: newUser.id,\n        role: \"user\"\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 { 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  \"/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\";\n\nimport { Profile as GoogleProfile } from \"passport-google-oauth20\";\n\nimport { ApiResponse } from \"../../shared/utils/api-response\";\nimport { AsyncHandler } from \"../../shared/utils/async-handler\";\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { setAuthCookies } from \"../../shared/helpers/cookie.helper\";\nimport { OAuthService } from \"./oauth.service\";\nimport { NewUser } from \"../../drizzle\";\n\nconst getProvider: Record<string, \"github\" | \"google\" | \"local\"> = {\n  github: \"github\",\n  google: \"google\"\n};\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: NewUser = {\n      provider: getProvider[data?.provider],\n      providerId: data.id,\n      name: data.displayName,\n      email: data.emails ? (data.emails[0].value as string) : \"\",\n      isEmailVerified: true,\n      avatar: {\n        url: data.photos ? (data.photos[0].value as string) : \"\"\n      }\n    };\n\n    const result = await OAuthService.handleOAuthLogin(user, {\n      setAuthCookie: (accessToken: string, refreshToken: string) => {\n        setAuthCookies(res, accessToken, refreshToken);\n      }\n    });\n\n    ApiResponse.ok(res, \"Auth Successfull\", {\n      user: {\n        ...user,\n        id: result.id\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: NewUser = {\n      provider: getProvider[data?.provider],\n      providerId: data.id,\n      name: data.displayName,\n      email: data?.emails ? (data.emails[0].value as string) : \"\",\n      isEmailVerified: data?.emails\n        ? (data.emails[0].verified as boolean)\n        : false,\n      avatar: {\n        url: data.profileUrl || (data.photos ? data.photos[0].value : \"\")\n      }\n    };\n\n    const result = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (accessToken: string, refreshToken: string) => {\n        setAuthCookies(res, accessToken, refreshToken);\n      }\n    });\n\n    ApiResponse.ok(res, \"Auth Successfull\", {\n      user: {\n        ...userInfo,\n        id: result.id\n      }\n    });\n  }\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} 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) {\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    }\n  ]);\n}\n\nexport function clearAuthCookies(res: Response) {\n  res.clearCookie(\"accessToken\", COOKIE_OPTIONS);\n  res.clearCookie(\"refreshToken\", COOKIE_OPTIONS);\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};\n\nexport function setCookies(res: Response, cookies: Cookie[]) {\n  cookies.forEach(({ cookie, value, maxAge }) => {\n    res.cookie(cookie, value, {\n      ...COOKIE_OPTIONS,\n      maxAge\n    });\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.validator.ts",
                          "content": "import * as z from \"zod\";\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.object({\n  name: nameSchema,\n  email: emailSchema,\n  password: passwordSchema,\n  role: roleSchema\n});\n\nexport const VerifyOtpSchema = z.object({\n  code: 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.number().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 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\nexport type VerifyOtpType = z.infer<typeof VerifyOtpSchema>;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.types.ts",
                          "content": "import { OTP_TYPES } from \"./auth.constants\";\n\nexport type OTPType = (typeof OTP_TYPES)[number];\n\nexport interface IAvatar {\n  public_id?: string;\n  url: string;\n  size?: number;\n}\n\nexport interface IUser {\n  id: number;\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  provider: \"local\" | \"google\" | \"github\";\n  providerId?: string;\n  isDeleted: boolean;\n  deletedAt?: Date;\n  reActivateAvailableAt?: Date;\n  createdAt: Date;\n  updatedAt: Date;\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.service.ts",
                          "content": "import { eq } from \"drizzle-orm\";\nimport {\n  ChangePasswordType,\n  DeleteAccountType,\n  ResetPasswordType,\n  SigninUserType,\n  SignupUserType,\n  VerifyOtpType\n} from \"./auth.validator\";\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { hashPassword, verifyPassword } from \"./auth.helpers\";\nimport {\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} from \"./auth.constants\";\nimport { refreshTokens, users } from \"../../drizzle\";\nimport { OtpService } from \"../otp/otp.service\";\nimport {\n  generateHashedToken,\n  generateOTP\n} from \"../../shared/helpers/token.helpers\";\nimport redis from \"../../shared/configs/redis\";\nimport db from \"../../shared/configs/db\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../../shared/utils/jwt\";\nimport { IUser } from \"./auth.types\";\nimport {\n  deleteFileFromCloudinary,\n  uploadToCloudinary\n} from \"../upload/cloudinary.service\";\n\nexport type CookieOptionsType = {\n  setAuthCookie?: (accessToken: string, refreshToken: string) => void;\n};\n\nexport class AuthService {\n  static async signupUser(user: SignupUserType) {\n    const { name, email, password, role } = user;\n    const existingUser = await db\n      .select()\n      .from(users)\n      .where(eq(users.email, email));\n\n    if (existingUser.length > 0) {\n      throw ApiError.conflict(\"User already exists with this email\");\n    }\n\n    await OtpService.checkOtpRestrictions(email);\n    await OtpService.trackOtpRequests(email);\n    const hashedPassword = await hashPassword(password);\n    const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n    const redisKey = `user:${email}:${hashCode}`;\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    });\n\n    await redis.set(redisKey, userData, {\n      EX: OTP_EXPIRES_IN / 1000\n    });\n\n    return;\n  }\n\n  static async verifyUser({ email, code }: VerifyOtpType) {\n    const hashCode = generateHashedToken(code);\n\n    await OtpService.verifyOtp(hashCode, email);\n\n    const userData = await redis.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 [existingUser] = await db\n      .insert(users)\n      .values({\n        name,\n        email: userEmail,\n        role,\n        password,\n        isEmailVerified: true\n      })\n      .$returningId();\n\n    await redis.del(`user:${email}:${hashCode}`);\n\n    return {\n      id: existingUser.id,\n      name,\n      email,\n      role: role || \"user\",\n      isEmailVerified: true\n    };\n  }\n\n  static async loginUser(user: SigninUserType, setCookie: CookieOptionsType) {\n    const { email, password } = user;\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.email, email));\n\n    if (!existingUser) {\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    if (!existingUser.isEmailVerified) {\n      throw ApiError.unauthorized(\"Email not verified\");\n    }\n\n    if (existingUser.lockUntil && new Date() < existingUser.lockUntil) {\n      throw ApiError.forbidden(\n        `Your account has been locked. Please try again after ${Math.ceil((existingUser.lockUntil.getTime() - Date.now()) / (1000 * 60))} minutes.`\n      );\n    }\n\n    const isPasswordValid = await verifyPassword(\n      password,\n      existingUser.password || \"\"\n    );\n\n    if (!isPasswordValid) {\n      let lockUntil = null;\n\n      let newAttempts = existingUser.failedLoginAttempts + 1;\n\n      if (newAttempts >= LOGIN_MAX_ATTEMPTS) {\n        lockUntil = new Date(Date.now() + LOCK_TIME_MS);\n      }\n\n      await db\n        .update(users)\n        .set({\n          failedLoginAttempts: newAttempts,\n          lockUntil\n        })\n        .where(eq(users.id, existingUser.id));\n\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    await db\n      .update(users)\n      .set({\n        failedLoginAttempts: 0,\n        lockUntil: null,\n        lastLoginAt: new Date()\n      })\n      .where(eq(users.id, existingUser.id));\n\n    await AuthService.handleUserToken(\n      {\n        id: existingUser.id,\n        role: existingUser.role\n      },\n      setCookie\n    );\n\n    return {\n      id: existingUser.id,\n      name: existingUser.name,\n      email: existingUser.email,\n      role: existingUser.role,\n      isEmailVerified: existingUser.isEmailVerified\n    };\n  }\n\n  static async getUserProfile(userId: number) {\n    const [user] = await db.select().from(users).where(eq(users.id, userId));\n    return {\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  static async updateUserProfile(\n    userId: number,\n    { name, avatar }: { name: string; avatar?: Express.Multer.File | undefined }\n  ) {\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.id, userId));\n    if (!existingUser) {\n      throw ApiError.unauthorized(\"Unauthorized\");\n    }\n\n    if (existingUser?.avatar?.public_id) {\n      await deleteFileFromCloudinary([existingUser.avatar.public_id]);\n    }\n\n    let avatarUrl;\n\n    if (avatar?.buffer) {\n      const file = await uploadToCloudinary(avatar.buffer, {\n        folder: \"uploads/files\",\n        resource_type: \"auto\"\n      });\n      avatarUrl = file.url;\n\n      await db\n        .update(users)\n        .set({\n          avatar: {\n            public_id: file.public_id,\n            url: file.url,\n            size: file.size\n          }\n        })\n        .where(eq(users.id, userId));\n    }\n\n    if (name) {\n      await db.update(users).set({ name }).where(eq(users.id, userId));\n    }\n\n    return {\n      name,\n      email: existingUser.email,\n      role: existingUser.role,\n      avatar: avatarUrl,\n      isEmailVerified: existingUser.isEmailVerified,\n      lastLoginAt: existingUser.lastLoginAt\n    };\n  }\n\n  static async handleUserToken(\n    user: Pick<IUser, \"id\" | \"role\">,\n    context: CookieOptionsType\n  ) {\n    const accessToken = generateAccessToken({\n      id: user.id,\n      role: user.role\n    });\n\n    const newRefreshToken = generateRefreshToken(user.id);\n\n    const hashedNewRefreshToken = generateHashedToken(newRefreshToken);\n\n    await db.insert(refreshTokens).values({\n      userId: user.id,\n      tokenHash: hashedNewRefreshToken,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    context.setAuthCookie &&\n      context.setAuthCookie(accessToken, newRefreshToken);\n  }\n\n  static async refreshTokens(accessToken: string, refreshToken: string) {\n    if (!refreshToken) {\n      throw ApiError.unauthorized(\"Unauthorized, please login.\");\n    }\n\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n    if (!decodedRefresh?.userId) {\n      throw ApiError.unauthorized(\"Invalid refresh token.\");\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const [storedToken] = await db\n      .select()\n      .from(refreshTokens)\n      .where(eq(refreshTokens.tokenHash, refreshTokenHash));\n\n    // Reuse detection\n    if (!storedToken) {\n      await db\n        .update(refreshTokens)\n        .set({ isRevoked: true, revokedAt: new Date() })\n        .where(eq(refreshTokens.tokenHash, refreshTokenHash));\n      throw ApiError.unauthorized(\"Token reuse detected. Please login again.\");\n    }\n    if (storedToken.isRevoked) {\n      throw ApiError.unauthorized(\"Refresh token revoked.\");\n    }\n\n    if (storedToken.expiresAt < new Date()) {\n      throw ApiError.unauthorized(\"Refresh token expired.\");\n    }\n\n    if (accessToken) {\n      const decodedAccess = verifyAccessToken(accessToken);\n      if (decodedAccess.id !== decodedRefresh.userId) {\n        throw ApiError.unauthorized(\"Token mismatch.\");\n      }\n    }\n\n    const [user] = await db\n      .select()\n      .from(users)\n      .where(eq(users.id, decodedRefresh.userId));\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\n    });\n\n    const newRefreshToken = generateRefreshToken(user.id);\n\n    const hashedNewRefreshToken = generateHashedToken(newRefreshToken);\n\n    await db\n      .update(refreshTokens)\n      .set({\n        isRevoked: true,\n        revokedAt: new Date(),\n        replacedByTokenHash: hashedNewRefreshToken\n      })\n      .where(eq(refreshTokens.tokenHash, refreshTokenHash));\n\n    await db.insert(refreshTokens).values({\n      userId: user.id,\n      tokenHash: hashedNewRefreshToken,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    return {\n      accessToken: newAccessToken,\n      refreshToken: newRefreshToken\n    };\n  }\n\n  static async forgotPassword(email: string) {\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.email, email));\n\n    if (!existingUser) {\n      throw ApiError.badRequest(\n        \"If this email is registered, check your inbox.\"\n      );\n    }\n\n    await OtpService.checkOtpRestrictions(email);\n    await OtpService.trackOtpRequests(email);\n\n    const { code, hashCode } = generateOTP(OTP_CODE_LENGTH);\n\n    await OtpService.sendOtp({\n      name: existingUser.name,\n      email,\n      templateName: \"forgot-password\",\n      code,\n      hashCode\n    });\n  }\n\n  static async verifyForgotPasswordOtp({ code, email }: VerifyOtpType) {\n    const hashCode = generateHashedToken(code);\n    await OtpService.verifyOtp(hashCode, email);\n    const redisKey = `forgot-password:${email}`;\n    await redis.set(redisKey, \"true\", {\n      EX: 60 * 5 // 5 minutes\n    });\n  }\n\n  static async resetPassword({ email, newPassword }: ResetPasswordType) {\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.email, email));\n\n    if (!existingUser) {\n      throw ApiError.badRequest(\n        \"If this email is registered, check your inbox.\"\n      );\n    }\n\n    const redisKey = `forgot-password:${email}`;\n    const isResetTokenValid = await redis.get(redisKey);\n    if (!isResetTokenValid) {\n      throw ApiError.badRequest(\"Invalid or expired reset token.\");\n    }\n\n    const isOldPassword = await verifyPassword(\n      newPassword,\n      existingUser.password || \"\"\n    );\n\n    if (isOldPassword) {\n      throw ApiError.badRequest(`New password cannot be same as old password.`);\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n\n    await db\n      .update(users)\n      .set({\n        password: hashedPassword\n      })\n      .where(eq(users.email, email));\n\n    await redis.del(redisKey);\n  }\n\n  static async logoutUser(userId: number) {\n    await db.delete(refreshTokens).where(eq(refreshTokens.userId, userId));\n  }\n\n  static async changePassword(\n    userId: number,\n    { oldPassword, newPassword }: ChangePasswordType\n  ) {\n    const [existingUser] = await db\n      .select()\n      .from(users)\n      .where(eq(users.id, userId));\n\n    if (!existingUser) {\n      throw ApiError.unauthorized(\"Unauthorized\");\n    }\n\n    if (!existingUser.isEmailVerified) {\n      throw ApiError.unauthorized(\"Please verify your email first.\");\n    }\n\n    const isPasswordValid = await verifyPassword(\n      oldPassword,\n      existingUser.password || \"\"\n    );\n\n    if (!isPasswordValid) {\n      throw ApiError.unauthorized(\"Invalid credentials\");\n    }\n\n    const isOldPassword = await verifyPassword(\n      newPassword,\n      existingUser.password || \"\"\n    );\n\n    if (isOldPassword) {\n      throw ApiError.badRequest(`New password cannot be same as old password.`);\n    }\n\n    const hashedPassword = await hashPassword(newPassword);\n\n    await db\n      .update(users)\n      .set({\n        password: hashedPassword\n      })\n      .where(eq(users.id, userId));\n  }\n\n  static async deleteAccount({ userId, type }: DeleteAccountType) {\n    if (type === \"soft\") {\n      await db\n        .update(users)\n        .set({\n          isDeleted: true,\n          deletedAt: new Date(),\n          reActivateAvailableAt: new Date(\n            Date.now() + REACTIVATION_AVAILABLE_AT\n          )\n        })\n        .where(eq(users.id, userId));\n    } else {\n      await db.delete(users).where(eq(users.id, userId));\n    }\n  }\n\n  static async reactivateAccount(userId: number) {\n    const [user] = await db.select().from(users).where(eq(users.id, userId));\n\n    if (!user) {\n      throw ApiError.unauthorized(\"Unauthorized, user not found\");\n    }\n\n    if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n      throw ApiError.badRequest(\n        `Your account has been locked. Please try again after ${Math.ceil(\n          (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n        )} minutes.`\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.unauthorized(\n        `Reactivation not available yet. Please try again after ${Math.ceil(\n          (user.reActivateAvailableAt.getTime() - Date.now()) / (1000 * 60)\n        )} minutes.`\n      );\n    }\n\n    await db\n      .update(users)\n      .set({\n        isDeleted: false,\n        deletedAt: null,\n        reActivateAvailableAt: null\n      })\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  changePassword,\n  deleteAccount,\n  forgotPassword,\n  getUserProfile,\n  loginUser,\n  logoutUser,\n  reactivateAccount,\n  refreshToken,\n  resetPassword,\n  signupUser,\n  updateProfile,\n  verifyForgotPasswordOtp,\n  verifyUser\n} from \"./auth.controller\";\nimport { validateRequest } from \"../../shared/middlewares/validate-request\";\nimport {\n  ChangePasswordSchema,\n  DeleteAccountSchema,\n  ResetPasswordSchema,\n  SigninSchema,\n  SignupSchema,\n  UpdateProfileSchema,\n  VerifyOtpSchema\n} from \"./auth.validator\";\nimport { verifyAuthentication } from \"../../shared/middlewares/verify-auth\";\nimport upload from \"../../shared/middlewares/upload-file\";\nimport {\n  changePasswordLimiter,\n  deleteAccountLimiter,\n  resetPasswordLimiter,\n  signinRateLimiter,\n  signupRateLimiter\n} from \"../../shared/middlewares/rate-limiter\";\nimport { checkUserAccountRestriction } from \"../../shared/middlewares/user-account-restriction\";\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  loginUser\n);\n\nrouter.get(\"/profile\", verifyAuthentication, getUserProfile);\n\nrouter.patch(\n  \"/profile\",\n  verifyAuthentication,\n  upload.single(\"avatar\"),\n  validateRequest(UpdateProfileSchema),\n  checkUserAccountRestriction,\n  updateProfile\n);\n\nrouter.post(\"/refresh-token\", refreshToken);\n\nrouter.post(\n  \"/forgot-password\",\n  validateRequest(VerifyOtpSchema.pick({ email: true })),\n  forgotPassword\n);\n\nrouter.post(\n  \"/verify-forgot-password\",\n  validateRequest(VerifyOtpSchema),\n  verifyForgotPasswordOtp\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  changePasswordLimiter,\n  checkUserAccountRestriction,\n  changePassword\n);\n\nrouter.post(\"/logout\", verifyAuthentication, logoutUser);\n\nrouter.delete(\n  \"/delete-account\",\n  verifyAuthentication,\n  validateRequest(DeleteAccountSchema),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  deleteAccount\n);\n\nrouter.put(\"/reactivate-account\", 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 { AsyncHandler } from \"../../shared/utils/async-handler\";\nimport {\n  ChangePasswordType,\n  DeleteAccountType,\n  ResetPasswordType,\n  SigninUserType,\n  SignupUserType,\n  VerifyOtpType\n} from \"./auth.validator\";\nimport { ApiResponse } from \"../../shared/utils/api-response\";\nimport { ApiError } from \"../../shared/utils/api-error\";\nimport { AuthService } from \"./auth.service\";\nimport {\n  clearAuthCookies,\n  setAuthCookies\n} from \"../../shared/helpers/cookie.helper\";\nimport { UserRequest } from \"../../types/global\";\n\n//? SIGNUP USER\nexport const signupUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, password, role, name }: SignupUserType = req.body;\n\n    if (!name || !email || !password) {\n      return next(ApiError.badRequest(\"Name, email and password are required\"));\n    }\n\n    await AuthService.signupUser({\n      email,\n      password,\n      role,\n      name\n    });\n\n    return ApiResponse.created(\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, code }: VerifyOtpType = req.body;\n\n    if (!email || !code) {\n      return next(ApiError.badRequest(\"Email and code are required\"));\n    }\n\n    await AuthService.verifyUser({ email, code });\n\n    return ApiResponse.ok(res, \"User verified successfully\");\n  }\n);\n\n//? LOGIN USER\nexport const loginUser = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, password }: SigninUserType = req.body;\n\n    if (!email || !password) {\n      return next(ApiError.badRequest(\"Email and password are required\"));\n    }\n\n    const result = await AuthService.loginUser(\n      {\n        email,\n        password\n      },\n      {\n        setAuthCookie(accessToken, refreshToken) {\n          setAuthCookies(res, accessToken, refreshToken);\n        }\n      }\n    );\n\n    if (!result) {\n      return next(ApiError.server(\"User login failed\"));\n    }\n\n    return ApiResponse.ok(res, \"User logged in successfully\", result);\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    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(userId);\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    return ApiResponse.ok(res, \"User profile fetched successfully\", {\n      user: {\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//? UPDATE PROFILE\nexport const updateProfile = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { name } = req.body;\n\n    if (!req.user?.id) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    const user = await AuthService.getUserProfile(req.user.id);\n\n    if (!user) {\n      return next(ApiError.notFound(\"User not found\"));\n    }\n\n    const updatedUser = await AuthService.updateUserProfile(req.user.id, {\n      name,\n      avatar: req.file\n    });\n\n    if (!updatedUser) {\n      return next(ApiError.server(\"Failed to update profile\"));\n    }\n\n    return ApiResponse.Success(\n      res,\n      \"Profile updated successfully!\",\n      updatedUser\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);\n\n    return ApiResponse.Success(res, \"Tokens refreshed 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(res, `Otp sent to ${email} successfully!`);\n  }\n);\n\n//? VERIFY FORGOT PASSWORD OTP\nexport const verifyForgotPasswordOtp = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, code }: VerifyOtpType = req.body;\n    if (!email || !code) {\n      return next(ApiError.badRequest(\"Email and code are required!\"));\n    }\n\n    await AuthService.verifyForgotPasswordOtp({ email, code });\n\n    return ApiResponse.ok(res, `Otp verified successfully!`);\n  }\n);\n\n//? RESET PASSWORD\nexport const resetPassword = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, newPassword }: ResetPasswordType = req.body;\n    if (!email || !newPassword) {\n      return next(ApiError.badRequest(\"Email and password are required!\"));\n    }\n\n    await AuthService.resetPassword({ email, newPassword });\n\n    return ApiResponse.ok(res, `Password reset successfully!`);\n  }\n);\n\n//? CHANGE PASSWORD\nexport const changePassword = AsyncHandler(\n  async (req: UserRequest, res: Response, next: NextFunction) => {\n    const { oldPassword, newPassword }: ChangePasswordType = req.body;\n    if (!oldPassword || !newPassword) {\n      return next(\n        ApiError.badRequest(\"Old password and new password are required!\")\n      );\n    }\n\n    const userId = req.user?.id;\n    if (!userId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.changePassword(userId, {\n      oldPassword,\n      newPassword\n    });\n\n    return ApiResponse.ok(res, `Password changed 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    await AuthService.logoutUser(userId);\n    clearAuthCookies(res);\n\n    return ApiResponse.ok(res, \"User logged out successfully!\");\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\n    if (userId !== reqUserId) {\n      return next(\n        ApiError.unauthorized(\"you are not authorized to perform this action\")\n      );\n    }\n\n    await AuthService.deleteAccount({ userId, type });\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 reqUserId = req?.user?.id;\n\n    if (!reqUserId) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\n\n    await AuthService.reactivateAccount(reqUserId);\n\n    return ApiResponse.Success(res, `Account reactivated successfully!`);\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.constants.ts",
                          "content": "export const OTP_MAX_ATTEMPTS = 5;\n\nexport const OTP_MAX_COUNTS = 3;\n\nexport const OTP_TYPES = [\n  \"signin\",\n  \"email-verification\",\n  \"password-reset\",\n  \"password-change\"\n] as const;\n\nexport const OTP_COOL_DOWN = 60;\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_EXPIRES_IN = 5 * 60 * 1000; // 5 minutes\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 RESET_PASSWORD_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n\nexport const REACTIVATION_AVAILABLE_AT = 24 * 60 * 60 * 1000; // 24 hours\n\nexport const OTP_SPAM_LOCK = 1 * 60 * 60 * 1000; // 1 hour\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/schemas/user.schema.ts",
                          "content": "import {\n  mysqlTable,\n  serial,\n  varchar,\n  boolean,\n  timestamp,\n  int,\n  json,\n  uniqueIndex,\n  index,\n  mysqlEnum\n} from \"drizzle-orm/mysql-core\";\nimport { timestamps } from \"./schema.helper\";\nimport { relations } from \"drizzle-orm\";\nimport { refreshTokens } from \"./refresh-token.schema\";\n\nexport interface IAvatar {\n  public_id?: string;\n  url: string;\n  size?: number;\n}\n\nexport const users = mysqlTable(\n  \"users\",\n  {\n    id: serial(\"id\").primaryKey(),\n    name: varchar(\"name\", { length: 100 }).notNull(),\n    email: varchar(\"email\", { length: 255 }).notNull().unique(),\n    password: varchar(\"password\", { length: 255 }),\n    role: mysqlEnum(\"role\", [\"user\", \"admin\"]).default(\"user\").notNull(),\n\n    provider: mysqlEnum(\"provider\", [\"local\", \"google\", \"github\"])\n      .default(\"local\")\n      .notNull(),\n    providerId: varchar(\"provider_id\", { length: 255 }),\n\n    avatar: json(\"avatar\").$type<IAvatar>(),\n\n    isEmailVerified: boolean(\"is_email_verified\").default(false).notNull(),\n    lastLoginAt: timestamp(\"last_login_at\"),\n    failedLoginAttempts: int(\"failed_login_attempts\").default(0).notNull(),\n    lockUntil: timestamp(\"lock_until\"),\n\n    isDeleted: boolean(\"is_deleted\").default(false).notNull(),\n    deletedAt: timestamp(\"deleted_at\"),\n    reActivateAvailableAt: timestamp(\"re_activate_available_at\"),\n\n    ...timestamps\n  },\n  table => [\n    uniqueIndex(\"email_idx\").on(table.email),\n    index(\"role_idx\").on(table.role),\n    index(\"is_deleted_idx\").on(table.isDeleted)\n  ]\n);\n\nexport const usersRelations = relations(users, ({ many }) => ({\n  refreshTokens: many(refreshTokens)\n}));\n\nexport type User = typeof users.$inferSelect;\nexport type NewUser = typeof users.$inferInsert;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/schemas/schema.helper.ts",
                          "content": "import { timestamp } from \"drizzle-orm/mysql-core\";\n\nexport const timestamps = {\n  createdAt: timestamp(\"created_at\", { mode: \"date\" }).defaultNow().notNull(),\n  updatedAt: timestamp(\"updated_at\", { mode: \"date\" })\n    .defaultNow()\n    .onUpdateNow()\n    .notNull()\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/schemas/refresh-token.schema.ts",
                          "content": "import {\n  mysqlTable,\n  serial,\n  varchar,\n  boolean,\n  timestamp,\n  index,\n  bigint,\n  text\n} from \"drizzle-orm/mysql-core\";\nimport { relations } from \"drizzle-orm\";\nimport { users } from \"./user.schema\";\nimport { timestamps } from \"./schema.helper\";\n\nexport const refreshTokens = mysqlTable(\n  \"refresh_tokens\",\n  {\n    id: serial(\"id\").primaryKey(),\n    userId: bigint(\"user_id\", { mode: \"number\", unsigned: true })\n      .references(() => users.id, { onDelete: \"cascade\" })\n      .notNull(),\n    tokenHash: text(\"token_hash\").notNull(),\n    expiresAt: timestamp(\"expires_at\").notNull(),\n    isRevoked: boolean(\"is_revoked\").default(false).notNull(),\n    revokedAt: timestamp(\"revoked_at\"),\n    replacedByTokenHash: varchar(\"replaced_by_token_hash\", { length: 255 }),\n    ...timestamps\n  },\n  table => [\n    index(\"user_id_idx\").on(table.userId),\n    index(\"token_hash_idx\").on(table.tokenHash),\n    index(\"is_revoked_idx\").on(table.isRevoked),\n    index(\"expires_at_idx\").on(table.expiresAt)\n  ]\n);\n\nexport const refreshTokensRelations = relations(\n  refreshTokens,\n  ({ one, many }) => {\n    return {\n      user: one(users, {\n        fields: [refreshTokens.userId],\n        references: [users.id]\n      })\n    };\n  }\n);\n\nexport type RefreshToken = typeof refreshTokens.$inferSelect;\nexport type NewRefreshToken = typeof refreshTokens.$inferInsert;\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\";\n\n/**\n * Basic health check endpoint\n * GET /api/health\n */\nexport const healthCheck = 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 * Detailed health check with system information\n * GET /api/health/detailed\n */\nexport const detailedHealthCheck = 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) / 100,\n      total:\n        Math.round((process.memoryUsage().heapTotal / 1024 / 1024) * 100) / 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"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/migrations/0000_organic_arachne.sql",
                          "content": "CREATE TABLE `users` (\n\t`id` serial AUTO_INCREMENT NOT NULL,\n\t`name` varchar(100) NOT NULL,\n\t`email` varchar(255) NOT NULL,\n\t`password` varchar(255),\n\t`role` enum('user','admin') NOT NULL DEFAULT 'user',\n\t`provider` enum('local','google','github') NOT NULL DEFAULT 'local',\n\t`provider_id` varchar(255),\n\t`avatar` json,\n\t`is_email_verified` boolean NOT NULL DEFAULT false,\n\t`last_login_at` timestamp,\n\t`failed_login_attempts` int NOT NULL DEFAULT 0,\n\t`lock_until` timestamp,\n\t`is_deleted` boolean NOT NULL DEFAULT false,\n\t`deleted_at` timestamp,\n\t`re_activate_available_at` timestamp,\n\t`created_at` timestamp NOT NULL DEFAULT (now()),\n\t`updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,\n\tCONSTRAINT `users_id` PRIMARY KEY(`id`),\n\tCONSTRAINT `users_email_unique` UNIQUE(`email`),\n\tCONSTRAINT `email_idx` UNIQUE(`email`)\n);\n--> statement-breakpoint\nCREATE TABLE `refresh_tokens` (\n\t`id` serial AUTO_INCREMENT NOT NULL,\n\t`user_id` bigint unsigned NOT NULL,\n\t`token_hash` text NOT NULL,\n\t`expires_at` timestamp NOT NULL,\n\t`is_revoked` boolean NOT NULL DEFAULT false,\n\t`revoked_at` timestamp,\n\t`replaced_by_token_hash` varchar(255),\n\t`created_at` timestamp NOT NULL DEFAULT (now()),\n\t`updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,\n\tCONSTRAINT `refresh_tokens_id` PRIMARY KEY(`id`)\n);\n--> statement-breakpoint\nALTER TABLE `refresh_tokens` ADD CONSTRAINT `refresh_tokens_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint\nCREATE INDEX `role_idx` ON `users` (`role`);--> statement-breakpoint\nCREATE INDEX `is_deleted_idx` ON `users` (`is_deleted`);--> statement-breakpoint\nCREATE INDEX `user_id_idx` ON `refresh_tokens` (`user_id`);--> statement-breakpoint\nCREATE INDEX `token_hash_idx` ON `refresh_tokens` (`token_hash`);--> statement-breakpoint\nCREATE INDEX `is_revoked_idx` ON `refresh_tokens` (`is_revoked`);--> statement-breakpoint\nCREATE INDEX `expires_at_idx` ON `refresh_tokens` (`expires_at`);"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/migrations/meta/_journal.json",
                          "content": "{\n  \"version\": \"7\",\n  \"dialect\": \"mysql\",\n  \"entries\": [\n    {\n      \"idx\": 0,\n      \"version\": \"5\",\n      \"when\": 1768828194715,\n      \"tag\": \"0000_organic_arachne\",\n      \"breakpoints\": true\n    }\n  ]\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/drizzle/migrations/meta/0000_snapshot.json",
                          "content": "{\n  \"version\": \"5\",\n  \"dialect\": \"mysql\",\n  \"id\": \"9e7c0bb7-17d0-4e0b-bc69-980208b82787\",\n  \"prevId\": \"00000000-0000-0000-0000-000000000000\",\n  \"tables\": {\n    \"users\": {\n      \"name\": \"users\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"serial\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"varchar(100)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"varchar(255)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"varchar(255)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"enum('user','admin')\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"enum('local','google','github')\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'local'\"\n        },\n        \"provider_id\": {\n          \"name\": \"provider_id\",\n          \"type\": \"varchar(255)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"avatar\": {\n          \"name\": \"avatar\",\n          \"type\": \"json\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"is_email_verified\": {\n          \"name\": \"is_email_verified\",\n          \"type\": \"boolean\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"last_login_at\": {\n          \"name\": \"last_login_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"failed_login_attempts\": {\n          \"name\": \"failed_login_attempts\",\n          \"type\": \"int\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"lock_until\": {\n          \"name\": \"lock_until\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"is_deleted\": {\n          \"name\": \"is_deleted\",\n          \"type\": \"boolean\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"re_activate_available_at\": {\n          \"name\": \"re_activate_available_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(now())\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"onUpdate\": true,\n          \"default\": \"(now())\"\n        }\n      },\n      \"indexes\": {\n        \"email_idx\": {\n          \"name\": \"email_idx\",\n          \"columns\": [\"email\"],\n          \"isUnique\": true\n        },\n        \"role_idx\": {\n          \"name\": \"role_idx\",\n          \"columns\": [\"role\"],\n          \"isUnique\": false\n        },\n        \"is_deleted_idx\": {\n          \"name\": \"is_deleted_idx\",\n          \"columns\": [\"is_deleted\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"users_id\": {\n          \"name\": \"users_id\",\n          \"columns\": [\"id\"]\n        }\n      },\n      \"uniqueConstraints\": {\n        \"users_email_unique\": {\n          \"name\": \"users_email_unique\",\n          \"columns\": [\"email\"]\n        }\n      },\n      \"checkConstraint\": {}\n    },\n    \"refresh_tokens\": {\n      \"name\": \"refresh_tokens\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"serial\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": true\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"bigint unsigned\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token_hash\": {\n          \"name\": \"token_hash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"is_revoked\": {\n          \"name\": \"is_revoked\",\n          \"type\": \"boolean\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"revoked_at\": {\n          \"name\": \"revoked_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"replaced_by_token_hash\": {\n          \"name\": \"replaced_by_token_hash\",\n          \"type\": \"varchar(255)\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"(now())\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"onUpdate\": true,\n          \"default\": \"(now())\"\n        }\n      },\n      \"indexes\": {\n        \"user_id_idx\": {\n          \"name\": \"user_id_idx\",\n          \"columns\": [\"user_id\"],\n          \"isUnique\": false\n        },\n        \"token_hash_idx\": {\n          \"name\": \"token_hash_idx\",\n          \"columns\": [\"token_hash\"],\n          \"isUnique\": false\n        },\n        \"is_revoked_idx\": {\n          \"name\": \"is_revoked_idx\",\n          \"columns\": [\"is_revoked\"],\n          \"isUnique\": false\n        },\n        \"expires_at_idx\": {\n          \"name\": \"expires_at_idx\",\n          \"columns\": [\"expires_at\"],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"refresh_tokens_user_id_users_id_fk\": {\n          \"name\": \"refresh_tokens_user_id_users_id_fk\",\n          \"tableFrom\": \"refresh_tokens\",\n          \"tableTo\": \"users\",\n          \"columnsFrom\": [\"user_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"refresh_tokens_id\": {\n          \"name\": \"refresh_tokens_id\",\n          \"columns\": [\"id\"]\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraint\": {}\n    }\n  },\n  \"views\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"tables\": {},\n    \"indexes\": {}\n  }\n}\n"
                        }
                      ]
                    }
                  }
                }
              }
            },
            "mongodb": {
              "orms": {
                "mongoose": {
                  "dependencies": {
                    "runtime": [
                      "express",
                      "mongoose",
                      "argon2",
                      "cloudinary",
                      "cookie-parser",
                      "cors",
                      "express-rate-limit",
                      "helmet",
                      "jsonwebtoken",
                      "multer",
                      "nodemailer",
                      "passport",
                      "passport-github2",
                      "passport-google-oauth20",
                      "pino",
                      "pino-pretty",
                      "zod",
                      "dotenv-flow",
                      "cross-env",
                      "source-map-support",
                      "swagger-autogen",
                      "swagger-ui-express"
                    ],
                    "dev": [
                      "@types/express",
                      "@types/jsonwebtoken",
                      "@types/bcryptjs",
                      "@types/cookie-parser",
                      "@types/cors",
                      "@types/jsonwebtoken",
                      "@types/morgan",
                      "@types/multer",
                      "@types/nodemailer",
                      "@types/passport",
                      "@types/passport-github2",
                      "@types/passport-google-oauth20",
                      "morgan",
                      "@types/source-map-support",
                      "@types/swagger-ui-express"
                    ]
                  },
                  "env": [
                    "PORT",
                    "NODE_ENV",
                    "LOG_LEVEL",
                    "CORS_ORIGIN",
                    "CRYPTO_SECRET",
                    "DATABASE_URL",
                    "JWT_ACCESS_SECRET",
                    "JWT_REFRESH_SECRET",
                    "SMTP_HOST",
                    "SMTP_PORT",
                    "SMTP_USER",
                    "SMTP_PASS",
                    "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"
                  ],
                  "architectures": {
                    "mvc": {
                      "files": [
                        {
                          "type": "file",
                          "path": "swagger.config.ts",
                          "content": "import swaggerAutoGen from \"swagger-autogen\";\n\nconst doc = {\n  info: {\n    title: \"Stateless Auth API\",\n    description: \"Stateless Auth API\",\n    version: \"1.0.0\"\n  },\n  host: \"localhost:3000\",\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": "package.json",
                          "content": "{\n  \"name\": \"servercn-stateless-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  \"devDependencies\": {},\n  \"dependencies\": {}\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/server.ts",
                          "content": "import app from \"./app\";\nimport { connectDB } from \"./configs/db\";\nimport env from \"./configs/env\";\nimport { logger } from \"./utils/logger\";\nimport { configureGracefulShutdown } from \"./utils/shutdown\";\n\nconst port = env.PORT || 9000;\n\nconnectDB();\n\nconst 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\nconfigureGracefulShutdown(server);\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"
                        },
                        {
                          "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  otpType: z.enum(OTP_TYPES, { error: \"Invalid otp type\" })\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/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": "import env from \"../configs/env\";\nimport { getTransporter } from \"../configs/nodemailer\";\nimport { ApiError } from \"./api-error\";\n\ntype sendMail = {\n  from?: string;\n  subject: string;\n  html: string;\n  email: string;\n};\n\nexport async function sendEmail({ from, email, subject, html }: sendMail) {\n  const transporter = getTransporter();\n  return transporter\n    .sendMail({\n      from: from || `<${env.EMAIL_FROM}>`,\n      to: email,\n      subject,\n      html\n    })\n    .catch(err => {\n      console.error(err);\n      throw ApiError.badRequest(\"Failed to send email\");\n    });\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}) {\n  return jwt.sign({ _id: user._id, role: user.role }, env.JWT_ACCESS_SECRET!, {\n    expiresIn: JWT_ACCESS_TOKEN_EXPIRY\n  });\n}\n\nexport function generateRefreshToken(userId: string) {\n  return jwt.sign({ userId }, env.JWT_REFRESH_SECRET!, {\n    expiresIn: JWT_REFRESH_TOKEN_EXPIRY\n  });\n}\n\nexport function verifyAccessToken(token: string) {\n  return jwt.verify(token, env.JWT_ACCESS_SECRET!) as {\n    _id: string;\n  };\n}\n\nexport function verifyRefreshToken(token: string) {\n  return jwt.verify(token, env.JWT_REFRESH_SECRET!) as {\n    userId: string;\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 { NextFunction } from \"express\";\nimport User from \"../models/user.model\";\nimport { IUser, OTPType } from \"../types/user\";\nimport { ApiError } from \"../utils/api-error\";\nimport Otp from \"../models/otp.model\";\nimport {\n  NEXT_OTP_DELAY,\n  OTP_CODE_LENGTH,\n  OTP_EXPIRES_IN,\n  OTP_MAX_ATTEMPTS,\n  RESET_PASSWORD_TOKEN_EXPIRY\n} from \"../constants/auth\";\nimport { logger } from \"../utils/logger\";\nimport { generateHashedToken, generateOTP } from \"../helpers/token.helpers\";\nimport { AuthService } from \"./auth.service\";\nimport { sendEmail } from \"../utils/send-mail\";\n\ntype VerifyOtpPayload = {\n  email: string;\n  otpCode: string;\n  otpType: OTPType;\n};\n\ntype VerifyOtpContext = {\n  setAuthCookie?: (accessToken: string, refreshToken: string) => void;\n};\n\ntype ResetPassowrdContext = {\n  setCookie?: (token: string) => void;\n};\n\ntype SendOtpPayload = {\n  email: string;\n  otpType: OTPType;\n  subject: string;\n};\n\nexport class OtpService {\n  static async sendOtp(next: NextFunction, payload: SendOtpPayload) {\n    const { email, otpType, subject } = payload;\n\n    const user = await User.findOne({ email });\n    if (!user) {\n      return next(ApiError.badRequest(\"Invalid request\"));\n    }\n\n    if (user.lockUntil && user.lockUntil > new Date()) {\n      return next(ApiError.badRequest(\"Account locked\"));\n    }\n\n    const existingOtp = await Otp.findOne({ email, type: otpType });\n\n    if (existingOtp && existingOtp.nextResendAllowedAt > new Date()) {\n      const remainingSec = Math.ceil(\n        (existingOtp.nextResendAllowedAt.getTime() - Date.now()) / 1000\n      );\n      return next(\n        ApiError.badRequest(\n          `Please wait ${remainingSec} seconds before requesting another OTP`\n        )\n      );\n    }\n\n    const otp = generateOTP(OTP_CODE_LENGTH, OTP_EXPIRES_IN);\n    logger.info(\n      `Sending OTP to ${email} with type ${otpType} and code ${otp.code}`\n    );\n    const nextResendAllowedAt = new Date(Date.now() + NEXT_OTP_DELAY);\n\n    let html = `<p>Your OTP for ${otpType}: ${otp.code}</p>`;\n    await sendEmail({\n      email,\n      subject: subject,\n      html\n    });\n\n    await Otp.create({\n      email: payload.email,\n      type: payload.otpType,\n      otpHashCode: otp.hashCode,\n      attempts: 0,\n      isUsed: false,\n      expiresAt: otp.expiresAt,\n      nextResendAllowedAt\n    });\n\n    return { message: `OTP sent to ${email} successfully` };\n  }\n\n  static async verifyOtp(\n    next: NextFunction,\n    payload: VerifyOtpPayload,\n    context: VerifyOtpContext,\n    resetPasswordContext: ResetPassowrdContext\n  ): Promise<any> {\n    const { email, otpCode, otpType } = payload;\n\n    const user = await User.findOne({ email });\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized, Please login first.\"));\n    }\n\n    if (user.lockUntil && user.lockUntil > new Date()) {\n      const minutes = Math.ceil(\n        (user.lockUntil.getTime() - Date.now()) / 60000\n      );\n      return next(\n        ApiError.badRequest(\n          `Your account has been locked. Try again in ${minutes} minutes.`\n        )\n      );\n    }\n\n    const otp = await Otp.findOne({\n      email,\n      type: otpType,\n      isUsed: false,\n      expiresAt: { $gt: new Date() }\n    })\n      .sort({ createdAt: -1 })\n      .select(\"+otpHashCode\");\n\n    if (!otp) {\n      return next(ApiError.badRequest(\"Invalid or expired OTP\"));\n    }\n\n    if (otp.attempts >= (otp.maxAttempts || OTP_MAX_ATTEMPTS)) {\n      return next(ApiError.badRequest(\"Maximum OTP attempts reached\"));\n    }\n\n    const hashedOtp = generateHashedToken(String(otpCode));\n\n    if (otp.otpHashCode !== hashedOtp) {\n      await Otp.updateOne({ _id: otp._id }, { $inc: { attempts: 1 } });\n      return next(ApiError.badRequest(\"Invalid OTP code\"));\n    }\n\n    otp.isUsed = true;\n    await otp.save();\n\n    if (otp.type === \"signin\") {\n      return await AuthService.handleToken(\n        {\n          _id: user._id.toString(),\n          role: user.role,\n          isEmailVerified: user.isEmailVerified\n        },\n        context\n      );\n    }\n\n    if (otp.type === \"password-reset\") {\n      return this.handlePasswordReset(\n        {\n          _id: user._id.toString()\n        },\n        resetPasswordContext\n      );\n    }\n\n    await Otp.deleteOne({ _id: otp._id });\n    await Otp.deleteMany({\n      expiresAt: { $lt: new Date() },\n      isUsed: true\n    });\n    return { message: \"OTP verified successfully\" };\n  }\n\n  private static handlePasswordReset(\n    user: Pick<IUser, \"_id\">,\n    context: ResetPassowrdContext\n  ) {\n    const hashedResetPasswordToken = generateHashedToken(user._id.toString());\n    const resetPasswordExpiry = new Date(\n      Date.now() + RESET_PASSWORD_TOKEN_EXPIRY\n    );\n\n    context.setCookie && context.setCookie(hashedResetPasswordToken);\n\n    return {\n      hashedResetPasswordToken,\n      resetPasswordExpiry\n    };\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/services/oauth.service.ts",
                          "content": "import { AuthService, Context } from \"./auth.service\";\nimport User, { IUser } 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};\n\nexport class OAuthService {\n  static async handleOAuthLogin(user: OAuthProfile, context: Context) {\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          isEmailVerified: existingUser.isEmailVerified\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        isEmailVerified: newUser.isEmailVerified\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 } from \"../validators/auth\";\nimport {\n  LOGIN_MAX_ATTEMPTS,\n  REACTIVATION_AVAILABLE_AT,\n  REFRESH_TOKEN_EXPIRY\n} from \"../constants/auth\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../utils/jwt\";\nimport RefreshToken from \"../models/refresh-token.model\";\nimport { generateHashedToken } from \"../helpers/token.helpers\";\nimport { IUser } from \"../types/user\";\nimport mongoose from \"mongoose\";\nimport { OtpService } from \"./otp.service\";\nimport { deleteFileFromCloudinary } from \"./cloudinary.service\";\n\nexport type Context = {\n  setAuthCookie?: (accessToken: string, refreshToken: string) => void;\n};\n\nexport class AuthService {\n  static async registerUser(\n    next: NextFunction,\n    user: Omit<SignupUserType, \"confirmPassword\">\n  ) {\n    const { name, email, password, role } = user;\n    const existingUser = await User.findOne({ email }).select(\"+password\");\n\n    if (existingUser) {\n      return next(ApiError.conflict(\"User with this email already exists\"));\n    }\n\n    const hashedPassword = await hashPassword(password);\n\n    const newUser = await User.create({\n      name,\n      email,\n      password: hashedPassword,\n      role\n    });\n\n    return newUser;\n  }\n\n  static async loginAndSendOtp(\n    next: NextFunction,\n    { email, password }: { email: string; password: string }\n  ) {\n    const session = await mongoose.startSession();\n\n    try {\n      session.startTransaction();\n\n      const user = await User.findOne({ email })\n        .session(session)\n        .select(\"+password\");\n      if (!user) {\n        await session.abortTransaction();\n        return next(ApiError.unauthorized(\"Invalid credentials\"));\n      }\n\n      const isPasswordValid = await verifyPassword(\n        password,\n        user.password || \"\"\n      );\n      if (!isPasswordValid) {\n        await session.abortTransaction();\n        return next(ApiError.unauthorized(\"Invalid credentials\"));\n      }\n\n      const otp = await OtpService.sendOtp(next, {\n        email,\n        otpType: \"signin\",\n        subject: \"Signin\"\n      });\n\n      if (!otp) {\n        await session.abortTransaction();\n        return next(ApiError.server(\"Failed to generate OTP\"));\n      }\n\n      await session.commitTransaction();\n      session.endSession();\n\n      return {\n        message: otp.message\n      };\n    } catch (err) {\n      await session.abortTransaction();\n      session.endSession();\n      return next(ApiError.server(\"Signin failed\"));\n    }\n  }\n\n  static async handleToken(\n    user: Pick<IUser, \"isEmailVerified\" | \"_id\" | \"role\">,\n    context: Context\n  ) {\n    if (!user.isEmailVerified) {\n      await User.updateOne(\n        { _id: user._id },\n        { $set: { isEmailVerified: true } }\n      );\n    }\n\n    const accessToken = generateAccessToken({\n      _id: user._id,\n      role: user.role\n    });\n\n    const refreshToken = generateRefreshToken(user._id);\n\n    const hashedRefreshToken = generateHashedToken(refreshToken);\n\n    await RefreshToken.create({\n      userId: new mongoose.Types.ObjectId(user._id),\n      tokenHash: hashedRefreshToken,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    context.setAuthCookie && context.setAuthCookie(accessToken, refreshToken);\n\n    await User.updateOne(\n      { _id: user._id },\n      {\n        $set: { lastLogin: new Date(), failedLoginAttempts: 0 },\n        $unset: { lockUntil: 1 }\n      }\n    );\n    return { message: \"OTP verified and user signed in successfully\" };\n  }\n\n  static async getUserProfile(userId: string) {\n    const user = await User.findById(userId);\n    return user;\n  }\n\n  static async refreshTokens(\n    next: NextFunction,\n    accessToken: string | null,\n    refreshToken: string\n  ) {\n    if (!refreshToken) {\n      return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n    }\n\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n    if (!decodedRefresh?.userId) {\n      return next(ApiError.unauthorized(\"Invalid refresh token.\"));\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const storedToken = await RefreshToken.findOne({\n      userId: decodedRefresh.userId,\n      tokenHash: refreshTokenHash\n    });\n\n    // Reuse detection\n    if (!storedToken) {\n      await RefreshToken.updateMany(\n        { userId: decodedRefresh.userId },\n        { isRevoked: true, revokedAt: new Date() }\n      );\n      return next(\n        ApiError.unauthorized(\"Token reuse detected. Please login again.\")\n      );\n    }\n\n    if (storedToken.isRevoked) {\n      return next(ApiError.unauthorized(\"Refresh token revoked.\"));\n    }\n\n    if (storedToken.expiresAt < new Date()) {\n      return next(ApiError.unauthorized(\"Refresh token expired.\"));\n    }\n\n    if (accessToken) {\n      const decodedAccess = verifyAccessToken(accessToken);\n      if (decodedAccess._id !== decodedRefresh.userId) {\n        return next(ApiError.unauthorized(\"Token mismatch.\"));\n      }\n    }\n\n    const user = await User.findById(decodedRefresh.userId);\n    if (!user) {\n      return next(ApiError.unauthorized(\"User not found.\"));\n    }\n\n    const newAccessToken = generateAccessToken({\n      _id: user._id.toString(),\n      role: user.role\n    });\n\n    const newRefreshToken = generateRefreshToken(user._id.toString());\n    const newRefreshTokenHash = generateHashedToken(newRefreshToken);\n\n    //? Rotate token\n    storedToken.isRevoked = true;\n    storedToken.revokedAt = new Date();\n    storedToken.replacedByTokenHash = newRefreshTokenHash;\n    await storedToken.save();\n\n    await RefreshToken.create({\n      userId: user._id,\n      tokenHash: newRefreshTokenHash,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    return {\n      accessToken: newAccessToken,\n      refreshToken: newRefreshToken\n    };\n  }\n\n  static async logoutUser(userId: string) {\n    return await RefreshToken.updateMany(\n      { userId },\n      { isRevoked: true, revokedAt: new Date() }\n    );\n  }\n\n  static async forgotPassword(next: NextFunction, email: string) {\n    const user = await User.findOne({ email });\n\n    if (!user) {\n      return next(\n        ApiError.badRequest(\"If this email is registered, check your inbox.\")\n      );\n    }\n\n    const result = await OtpService.sendOtp(next, {\n      email,\n      otpType: \"password-reset\",\n      subject: \"Password Reset\"\n    });\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to send otp!\"));\n    }\n\n    return result;\n  }\n\n  static async resetPassword(\n    next: NextFunction,\n    email: string,\n    newPassword: string\n  ) {\n    const user = await User.findOne({ email }).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 ${Math.ceil(\n            (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n          )} minutes.`\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 ${Math.ceil(\n            (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n          )} minutes.`\n        )\n      );\n    }\n\n    if (!user.isEmailVerified) {\n      return next(ApiError.unauthorized(\"Please verify your email first.\"));\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      { email },\n      {\n        $set: {\n          password: hashedPassword,\n          isEmailVerified: true\n        }\n      }\n    );\n    return { message: \"Password reset successfully!\" };\n  }\n\n  static async changePassword(\n    next: NextFunction,\n    {\n      newPassword,\n      oldPassword,\n      userId\n    }: { userId: string; newPassword: string; oldPassword: string }\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      { _id: userId },\n      {\n        $set: {\n          password: hashedPassword\n        }\n      }\n    );\n    return { message: \"Password changed successfully. Please login again!\" };\n  }\n\n  static async deleteOrDeactiveAccount(\n    next: NextFunction,\n    userId: string,\n    type: \"soft\" | \"hard\"\n  ) {\n    const user = await User.findById(userId);\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\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    } else if (type === \"hard\") {\n      if (user?.avatar?.public_id) {\n        await deleteFileFromCloudinary([user.avatar.public_id]);\n      }\n      await User.findOneAndDelete({ _id: userId });\n      await user.save();\n    }\n  }\n\n  static async reactivateAccount(next: NextFunction, userId: string) {\n    const user = await User.findById(userId);\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.badRequest(\n          `Your account has been locked. Please try again after ${Math.ceil(\n            (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n          )} minutes.`\n        )\n      );\n    }\n\n    if (!user?.isDeleted || !user?.deletedAt) {\n      return next(ApiError.badRequest(\"Your account is already active!\"));\n    }\n\n    if (\n      user?.reActivateAvailableAt &&\n      new Date(user?.reActivateAvailableAt) > new Date()\n    ) {\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${Math.ceil(\n            (user.reActivateAvailableAt.getTime() - Date.now()) / (1000 * 60)\n          )} minutes.`\n        )\n      );\n    }\n\n    await User.findOneAndUpdate(\n      { _id: userId },\n      {\n        $set: {\n          isDeleted: false,\n          deletedAt: null,\n          reActivateAvailableAt: null\n        }\n      },\n      { new: true }\n    );\n\n    await user.save();\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/routes/oauth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport passport from \"passport\";\nimport { 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  \"/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  forgotPassword,\n  getUserProfile,\n  logoutUser,\n  reactivateAccount,\n  refreshToken,\n  resetPassword,\n  signinUser,\n  signupUser,\n  updateProfile,\n  verifyOtp\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  otpVerificationLimiter,\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  \"/verify-otp\",\n  validateRequest(VerifyOtpSchema),\n  otpVerificationLimiter,\n  verifyOtp\n);\n\nrouter.post(\n  \"/signup\",\n  validateRequest(SignupSchema),\n  signupRateLimiter,\n  signupUser\n);\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.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  \"/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.delete(\n  \"/delete-account\",\n  verifyAuthentication,\n  validateRequest(DeleteAccountSchema),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  deleteAccount\n);\n\nrouter.put(\"/reactivate-account\", verifyAuthentication, reactivateAccount);\n\nexport default router;\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  };\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"
                        },
                        {
                          "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/models/refresh-token.model.ts",
                          "content": "import mongoose, { Document, Model, Schema } from \"mongoose\";\nimport { REFRESH_TOKEN_EXPIRY } from \"../constants/auth\";\n\nexport interface IRefreshToken extends Document {\n  _id: mongoose.Types.ObjectId;\n  userId: mongoose.Types.ObjectId;\n  tokenHash: string;\n  expiresAt: Date;\n  isRevoked: boolean;\n  revokedAt?: Date;\n  replacedByTokenHash?: string;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nconst refreshTokenSchema = new Schema<IRefreshToken>(\n  {\n    userId: {\n      type: Schema.Types.ObjectId,\n      ref: \"User\",\n      required: [true, \"User ID is required\"]\n    },\n    tokenHash: {\n      type: String,\n      required: [true, \"Token hash is required\"],\n      select: false // Secure by default\n    },\n    expiresAt: {\n      type: Date,\n      required: [true, \"Expiration time is required\"]\n    },\n    isRevoked: {\n      type: Boolean,\n      default: false\n    },\n    revokedAt: {\n      type: Date\n    },\n    replacedByTokenHash: {\n      type: String,\n      select: false\n    }\n  },\n  {\n    timestamps: true\n  }\n);\n\nrefreshTokenSchema.index({ userId: 1 });\nrefreshTokenSchema.index({ tokenHash: 1 });\nrefreshTokenSchema.index({ isRevoked: 1 });\n\n//? TTL index to auto-delete expired tokens\nrefreshTokenSchema.index(\n  {\n    createdAt: 1\n  },\n  {\n    expireAfterSeconds: REFRESH_TOKEN_EXPIRY / 1000,\n    partialFilterExpression: { isRevoked: true }\n  }\n);\n\nconst RefreshToken: Model<IRefreshToken> =\n  mongoose.models.RefreshToken ||\n  mongoose.model<IRefreshToken>(\"RefreshToken\", refreshTokenSchema);\n\nexport default RefreshToken;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/models/otp.model.ts",
                          "content": "import mongoose, { Document, Model, Schema } from \"mongoose\";\nimport { OTPType } from \"../types/user\";\nimport { OTP_EXPIRES_IN, OTP_MAX_ATTEMPTS, OTP_TYPES } from \"../constants/auth\";\n\n//? otp interface\nexport interface IOtp extends Document {\n  _id: mongoose.Types.ObjectId;\n  email: string;\n  otpHashCode: string;\n  nextResendAllowedAt: Date;\n  type: OTPType;\n  expiresAt: Date;\n  isUsed: boolean;\n  usedAt?: Date;\n  attempts: number;\n  maxAttempts: number;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\n//? otp schema\nconst otpSchema = new Schema<IOtp>(\n  {\n    email: {\n      type: String,\n      required: [true, \"Email is required\"],\n      lowercase: true,\n      trim: true\n    },\n    otpHashCode: {\n      type: String,\n      required: [true, \"OTP hash code is required\"],\n      select: false // Never return OTP hash code in queries by default\n    },\n    nextResendAllowedAt: {\n      type: Date,\n      required: [true, \"Next resend allowed at is required\"]\n    },\n    type: {\n      type: String,\n      enum: OTP_TYPES,\n      required: [true, \"OTP type is required\"]\n    },\n    expiresAt: {\n      type: Date,\n      required: [true, \"Expiration time is required\"]\n    },\n    isUsed: {\n      type: Boolean,\n      default: false\n    },\n    usedAt: {\n      type: Date\n    },\n    attempts: {\n      type: Number,\n      default: 0\n    },\n    maxAttempts: {\n      type: Number,\n      default: OTP_MAX_ATTEMPTS // Prevent brute force attacks\n    }\n  },\n  {\n    timestamps: true\n  }\n);\n\n// Performance Indexes\notpSchema.index({ email: 1, type: 1 }); // Quick lookup by email and type\n\notpSchema.index(\n  { createdAt: 1 },\n  { expireAfterSeconds: OTP_EXPIRES_IN / 1000 } // 5 minutes\n); // ttl index\n\nconst Otp: Model<IOtp> =\n  mongoose.models.Otp || mongoose.model<IOtp>(\"Otp\", otpSchema);\n\nexport default Otp;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/middlewares/verify-auth.ts",
                          "content": "import { NextFunction, Request, Response } from \"express\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../utils/jwt\";\nimport { ApiError } from \"../utils/api-error\";\nimport { logger } from \"../utils/logger\";\nimport User from \"../models/user.model\";\nimport { UserRequest } from \"../types/user\";\nimport { setAuthCookies } from \"../helpers/cookie.helper\";\nimport RefreshToken from \"../models/refresh-token.model\";\nimport { generateHashedToken } from \"../helpers/token.helpers\";\n\nconst REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000;\n\nexport async function verifyAuthentication(\n  req: UserRequest,\n  res: Response,\n  next: NextFunction\n): Promise<void> {\n  const accessToken = req.cookies?.accessToken;\n  const refreshToken = req.cookies?.refreshToken;\n\n  //? 1. Try access token\n  if (accessToken) {\n    try {\n      const decoded = verifyAccessToken(accessToken);\n      req.user = decoded;\n      return next();\n    } catch {\n      logger.warn(\"Access token expired or invalid, attempting refresh\");\n    }\n  }\n\n  //? 2. Refresh token required\n  if (!refreshToken) {\n    return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n  }\n\n  try {\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n    if (!decodedRefresh?.userId) {\n      return next(ApiError.unauthorized(\"Invalid refresh token.\"));\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const storedToken = await RefreshToken.findOne({\n      userId: decodedRefresh.userId,\n      tokenHash: refreshTokenHash\n    });\n\n    //? Reuse detection\n    if (!storedToken) {\n      await RefreshToken.updateMany(\n        { userId: decodedRefresh.userId },\n        { isRevoked: true, revokedAt: new Date() }\n      );\n      return next(\n        ApiError.unauthorized(\"Token reuse detected. Please login again.\")\n      );\n    }\n\n    if (storedToken.isRevoked) {\n      return next(ApiError.unauthorized(\"Refresh token revoked.\"));\n    }\n\n    if (storedToken.expiresAt < new Date()) {\n      return next(ApiError.unauthorized(\"Refresh token expired.\"));\n    }\n\n    const user = await User.findById(storedToken.userId);\n    if (!user) {\n      return next(ApiError.unauthorized(\"User not found.\"));\n    }\n\n    //? 3. Rotate tokens\n    const newAccessToken = generateAccessToken({\n      _id: user._id.toString(),\n      role: user.role\n    });\n\n    const newRefreshToken = generateRefreshToken(user._id.toString());\n    const newRefreshTokenHash = generateHashedToken(newRefreshToken);\n\n    storedToken.isRevoked = true;\n    storedToken.revokedAt = new Date();\n    storedToken.replacedByTokenHash = newRefreshTokenHash;\n    await storedToken.save();\n\n    await RefreshToken.create({\n      userId: user._id,\n      tokenHash: newRefreshTokenHash,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    setAuthCookies(res, newAccessToken, newRefreshToken);\n\n    req.user = {\n      _id: user._id.toString(),\n      role: user.role\n    };\n\n    return next();\n  } catch (err) {\n    logger.warn(\"Refresh token verification failed\");\n    return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\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\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": "import { 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\";\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 minutesLeft = Math.ceil(\n        (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n      );\n\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${minutesLeft} minutes.`\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  console.log({ file });\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": "import { 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/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\";\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    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(user, {\n      setAuthCookie: (accessToken: string, refreshToken: string) => {\n        setAuthCookies(res, accessToken, refreshToken);\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    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (accessToken: string, refreshToken: string) => {\n        setAuthCookies(res, accessToken, refreshToken);\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 { OtpService } from \"../services/otp.service\";\nimport {\n  clearAuthCookies,\n  setAuthCookies,\n  setCookies\n} from \"../helpers/cookie.helper\";\nimport { UserRequest } from \"../types/user\";\nimport {\n  deleteFileFromCloudinary,\n  uploadToCloudinary\n} from \"../services/cloudinary.service\";\nimport { RESET_PASSWORD_TOKEN_EXPIRY } from \"../constants/auth\";\nimport { DeleteAccountType, VerifyOtpType } from \"../validators/auth\";\n\n//? VERIFY OTP\nexport const verifyOtp = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, otpCode, otpType }: VerifyOtpType = req.body;\n    if (!email || !otpCode || !otpType) {\n      return next(\n        ApiError.badRequest(\"Email, OTP code and OTP type are required\")\n      );\n    }\n\n    const otp = await OtpService.verifyOtp(\n      next,\n      { email, otpCode, otpType },\n      {\n        setAuthCookie: (accessToken: string, refreshToken: string) => {\n          setAuthCookies(res, accessToken, refreshToken);\n        }\n      },\n      {\n        setCookie: (token: string) => {\n          setCookies(res, [\n            {\n              cookie: \"hashedResetPasswordToken\",\n              value: token,\n              maxAge: RESET_PASSWORD_TOKEN_EXPIRY\n            }\n          ]);\n        }\n      }\n    );\n\n    if (!otp) {\n      return next(ApiError.server(\"Failed to verify OTP!\"));\n    }\n    return ApiResponse.ok(res, otp.message || \"OTP verified successfully!\");\n  }\n);\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    const user = await AuthService.registerUser(next, {\n      name,\n      email,\n      password,\n      role\n    });\n\n    if (!user) {\n      return next(ApiError.server(\"Failed to register user!\"));\n    }\n\n    return ApiResponse.created(res, \"User registered successfully\", {\n      name: user.name,\n      email: user.email,\n      role: user.role\n    });\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 result = await AuthService.loginAndSendOtp(next, { email, password });\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to login!\"));\n    }\n\n    return ApiResponse.ok(res, result.message || \"Otp sent 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    if (!userId) {\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    return ApiResponse.ok(res, \"User profile fetched successfully\", {\n      user: {\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//? 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!\", user);\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(\n      next,\n      accessToken,\n      refreshToken\n    );\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);\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    await AuthService.logoutUser(userId.toString());\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    const result = await AuthService.forgotPassword(next, email);\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to send otp!\"));\n    }\n\n    return ApiResponse.ok(res, result.message || \"Otp sent successfully!\");\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 hashedResetPasswordToken = req.cookies?.hashedResetPasswordToken;\n\n    if (!hashedResetPasswordToken) {\n      return next(\n        ApiError.badRequest(\"Reset password token not found or expired\")\n      );\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    res.clearCookie(\"hashedResetPasswordToken\");\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//? 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\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(next, userId, type);\n\n    if (type === \"hard\") {\n      clearAuthCookies(res);\n    }\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(next, userId);\n\n    return ApiResponse.Success(res, \"Account reactivated 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_EXPIRES_IN = 5 * 60 * 1000; // 5 minutes\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 RESET_PASSWORD_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n\nexport const REACTIVATION_AVAILABLE_AT = 24 * 60 * 60 * 1000; // 24 hours\n"
                        },
                        {
                          "type": "file",
                          "path": "src/docs/swagger.json",
                          "content": "{}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/swagger.ts",
                          "content": "import swaggerUi from \"swagger-ui-express\";\nimport { Express } from \"express\";\n\nimport swaggerDocument from \"../docs/swagger.json\";\nimport env from \"../configs/env\";\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/passport.ts",
                          "content": "import 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 env from \"./env\";\n\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\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"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/nodemailer.ts",
                          "content": "import nodemailer from \"nodemailer\";\nimport \"dotenv/config\";\nimport env from \"./env\";\n\nlet transporter: nodemailer.Transporter | null = null;\n\nexport function getTransporter() {\n  if (transporter) return transporter;\n  const host = env.SMTP_HOST;\n  const port = Number(env.SMTP_PORT || 465);\n  const user = env.SMTP_USER;\n  const pass = env.SMTP_PASS;\n  const from = env.EMAIL_FROM;\n  if (!host || !user || !pass || !from) {\n    throw new Error(\"SMTP/EMAIL env not configured\");\n  }\n\n  transporter = nodemailer.createTransport({\n    host,\n    port,\n    secure: port === 465,\n    auth: { user, pass }\n  });\n  return transporter;\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/configs/env.ts",
                          "content": "import \"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\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  SMTP_HOST: z.string(),\n  SMTP_PORT: z\n    .string()\n    .regex(/^\\d+$/, \"SMTP_PORT must be a number\")\n    .transform(Number),\n  SMTP_USER: z.string(),\n  SMTP_PASS: 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\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"
                        },
                        {
                          "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 { ACCESS_TOKEN_EXPIRY, REFRESH_TOKEN_EXPIRY } 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) {\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    }\n  ]);\n}\n\nexport function clearAuthCookies(res: Response) {\n  res.clearCookie(\"accessToken\", COOKIE_OPTIONS);\n  res.clearCookie(\"refreshToken\", COOKIE_OPTIONS);\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};\n\nexport function setCookies(res: Response, cookies: Cookie[]) {\n  cookies.forEach(({ cookie, value, maxAge }) => {\n    res.cookie(cookie, value, {\n      ...COOKIE_OPTIONS,\n      maxAge\n    });\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/helpers/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"
                        }
                      ]
                    },
                    "feature": {
                      "files": [
                        {
                          "type": "file",
                          "path": "swagger.config.ts",
                          "content": "import swaggerAutoGen from \"swagger-autogen\";\n\nconst doc = {\n  info: {\n    title: \"Stateless Auth API\",\n    description: \"Stateless Auth API\",\n    version: \"1.0.0\"\n  },\n  host: \"localhost:3000\",\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": "package.json",
                          "content": "{\n  \"name\": \"servercn-stateless-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  \"devDependencies\": {},\n  \"dependencies\": {}\n}\n"
                        },
                        {
                          "type": "file",
                          "path": ".husky/pre-commit",
                          "content": "npx lint-staged"
                        },
                        {
                          "type": "file",
                          "path": "src/server.ts",
                          "content": "import app from \"./app\";\nimport { connectDB } from \"./config/db\";\nimport env from \"./shared/configs/env\";\nimport { logger } from \"./shared/utils/logger\";\nimport { configureGracefulShutdown } from \"./shared/utils/shutdown\";\n\nconst port = env.PORT || 9000;\n\nconnectDB();\n\nconst 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\nconfigureGracefulShutdown(server);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/app.ts",
                          "content": "import express, { Express, Request, Response } from \"express\";\nimport cookieParser from \"cookie-parser\";\nimport morgan from \"morgan\";\n\nimport Routes from \"./routes/index\";\n\nimport \"./shared/configs/passport\";\nimport { configureSecurityHeaders } from \"./shared/middlewares/security-header\";\nimport { notFoundHandler } from \"./shared/middlewares/not-found-handler\";\nimport { errorHandler } from \"./shared/middlewares/error-handler\";\nimport env from \"./shared/configs/env\";\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": "src/routes/index.ts",
                          "content": "import { Router } from \"express\";\nimport healthRoutes from \"../modules/health/health.routes\";\nimport oauthRoutes from \"../modules/oauth/oauth.routes\";\nimport authRoutes from \"../modules/auth/auth.routes\";\n\nconst router = Router();\n\nrouter.use(\"/v1/health\", healthRoutes);\nrouter.use(\"/v1/auth\", authRoutes);\nrouter.use(\"/auth\", oauthRoutes);\n\nexport default router;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/config/db.ts",
                          "content": "import mongoose from \"mongoose\";\nimport { logger } from \"../shared/utils/logger\";\nimport env from \"../shared/configs/env\";\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/docs/swagger.json",
                          "content": ""
                        },
                        {
                          "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  };\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/verify-auth.ts",
                          "content": "import { NextFunction, Response } from \"express\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../utils/jwt\";\nimport { ApiError } from \"../errors/api-error\";\nimport { logger } from \"../utils/logger\";\nimport { generateHashedToken } from \"../helpers/token.helpers\";\nimport { UserRequest } from \"../../@types/global\";\nimport RefreshToken from \"../../modules/auth/refresh-token.model\";\nimport User from \"../../modules/auth/user.model\";\nimport { setAuthCookies } from \"../helpers/cookie.helper\";\n\nconst REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000;\n\nexport async function verifyAuthentication(\n  req: UserRequest,\n  res: Response,\n  next: NextFunction\n): Promise<void> {\n  const accessToken = req.cookies?.accessToken;\n  const refreshToken = req.cookies?.refreshToken;\n\n  //? 1. Try access token\n  if (accessToken) {\n    try {\n      const decoded = verifyAccessToken(accessToken);\n      req.user = decoded;\n      return next();\n    } catch {\n      logger.warn(\"Access token expired or invalid, attempting refresh\");\n    }\n  }\n\n  //? 2. Refresh token required\n  if (!refreshToken) {\n    return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n  }\n\n  try {\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n    if (!decodedRefresh?.userId) {\n      return next(ApiError.unauthorized(\"Invalid refresh token.\"));\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const storedToken = await RefreshToken.findOne({\n      userId: decodedRefresh.userId,\n      tokenHash: refreshTokenHash\n    });\n\n    //? Reuse detection\n    if (!storedToken) {\n      await RefreshToken.updateMany(\n        { userId: decodedRefresh.userId },\n        { isRevoked: true, revokedAt: new Date() }\n      );\n      return next(\n        ApiError.unauthorized(\"Token reuse detected. Please login again.\")\n      );\n    }\n\n    if (storedToken.isRevoked) {\n      return next(ApiError.unauthorized(\"Refresh token revoked.\"));\n    }\n\n    if (storedToken.expiresAt < new Date()) {\n      return next(ApiError.unauthorized(\"Refresh token expired.\"));\n    }\n\n    const user = await User.findById(storedToken.userId);\n    if (!user) {\n      return next(ApiError.unauthorized(\"User not found.\"));\n    }\n\n    //? 3. Rotate tokens\n    const newAccessToken = generateAccessToken({\n      _id: user._id.toString(),\n      role: user.role\n    });\n\n    const newRefreshToken = generateRefreshToken(user._id.toString());\n    const newRefreshTokenHash = generateHashedToken(newRefreshToken);\n\n    storedToken.isRevoked = true;\n    storedToken.revokedAt = new Date();\n    storedToken.replacedByTokenHash = newRefreshTokenHash;\n    await storedToken.save();\n\n    await RefreshToken.create({\n      userId: user._id,\n      tokenHash: newRefreshTokenHash,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    setAuthCookies(res, newAccessToken, newRefreshToken);\n\n    req.user = {\n      _id: user._id.toString(),\n      role: user.role\n    };\n\n    return next();\n  } catch (err) {\n    logger.warn(\"Refresh token verification failed\");\n    return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/middlewares/validate-request.ts",
                          "content": "import { Request, Response, NextFunction } from \"express\";\nimport z, { ZodError, type ZodObject } from \"zod\";\n\nimport { ApiError } from \"../errors/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 \"../errors/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": "import { NextFunction, Response } from \"express\";\nimport { ApiError } from \"../errors/api-error\";\nimport { logger } from \"../utils/logger\";\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 minutesLeft = Math.ceil(\n        (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n      );\n\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${minutesLeft} minutes.`\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  console.log({ file });\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 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, res, next) => {\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": "import { rateLimit } from \"express-rate-limit\";\nimport { STATUS_CODES } from \"../constants/status-codes\";\nimport { ApiError } from \"../errors/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, res, next, options) => {\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 \"../errors/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 \"../errors/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": "import env from \"../configs/env\";\nimport { getTransporter } from \"../configs/nodemailer\";\nimport { ApiError } from \"../errors/api-error\";\n\ntype sendMail = {\n  from?: string;\n  subject: string;\n  html: string;\n  email: string;\n};\n\nexport async function sendEmail({ from, email, subject, html }: sendMail) {\n  const transporter = getTransporter();\n  return transporter\n    .sendMail({\n      from: from || `<${env.EMAIL_FROM}>`,\n      to: email,\n      subject,\n      html\n    })\n    .catch(err => {\n      throw ApiError.badRequest(\"Failed to send email\");\n    });\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}) {\n  return jwt.sign({ _id: user._id, role: user.role }, env.JWT_ACCESS_SECRET!, {\n    expiresIn: JWT_ACCESS_TOKEN_EXPIRY\n  });\n}\n\nexport function generateRefreshToken(userId: string) {\n  return jwt.sign({ userId }, env.JWT_REFRESH_SECRET!, {\n    expiresIn: JWT_REFRESH_TOKEN_EXPIRY\n  });\n}\n\nexport function verifyAccessToken(token: string) {\n  return jwt.verify(token, env.JWT_ACCESS_SECRET!) as {\n    _id: string;\n  };\n}\n\nexport function verifyRefreshToken(token: string) {\n  return jwt.verify(token, env.JWT_REFRESH_SECRET!) as {\n    userId: string;\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/errors/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} 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) {\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    }\n  ]);\n}\n\nexport function clearAuthCookies(res: Response) {\n  res.clearCookie(\"accessToken\", COOKIE_OPTIONS);\n  res.clearCookie(\"refreshToken\", COOKIE_OPTIONS);\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};\n\nexport function setCookies(res: Response, cookies: Cookie[]) {\n  cookies.forEach(({ cookie, value, maxAge }) => {\n    res.cookie(cookie, value, {\n      ...COOKIE_OPTIONS,\n      maxAge\n    });\n  });\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/swagger.ts",
                          "content": "import swaggerUi from \"swagger-ui-express\";\nimport { Express } from \"express\";\nimport env from \"./env\";\nimport swaggerDocument from \"../../docs/swagger.json\";\n\nexport const setupSwagger = (app: Express) => {\n  if (env.NODE_ENV !== \"development\") return;\n  app.use(\"/api/docs\", swaggerUi.serve, swaggerUi.setup(swaggerDocument));\n};\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/passport.ts",
                          "content": "import 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 env from \"./env\";\n\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\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"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/nodemailer.ts",
                          "content": "import nodemailer from \"nodemailer\";\nimport \"dotenv/config\";\nimport env from \"./env\";\n\nlet transporter: nodemailer.Transporter | null = null;\n\nexport function getTransporter() {\n  if (transporter) return transporter;\n  const host = env.SMTP_HOST;\n  const port = Number(env.SMTP_PORT || 465);\n  const user = env.SMTP_USER;\n  const pass = env.SMTP_PASS;\n  const from = env.EMAIL_FROM;\n  if (!host || !user || !pass || !from) {\n    throw new Error(\"SMTP/EMAIL env not configured\");\n  }\n\n  transporter = nodemailer.createTransport({\n    host,\n    port,\n    secure: port === 465,\n    auth: { user, pass }\n  });\n  return transporter;\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/shared/configs/env.ts",
                          "content": "import \"dotenv-flow/config\";\nimport { z } from \"zod\";\n\nconst envSchema = z.object({\n  NODE_ENV: z\n    .enum([\"development\", \"test\", \"production\"])\n    .default(\"development\"),\n  PORT: z.string().default(\"9000\"),\n  DATABASE_URL: z.string(),\n\n  CORS_ORIGIN: z.string(),\n  LOG_LEVEL: z.string().default(\"info\"),\n\n  JWT_REFRESH_SECRET: z.string(),\n  JWT_ACCESS_SECRET: z.string(),\n\n  CRYPTO_SECRET: z.string(),\n\n  SMTP_HOST: z.string(),\n  SMTP_PORT: z.string(),\n  SMTP_USER: z.string(),\n  SMTP_PASS: z.string(),\n  EMAIL_FROM: z.string(),\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.string(),\n\n  GITHUB_CLIENT_ID: z.string(),\n  GITHUB_CLIENT_SECRET: z.string(),\n  GITHUB_REDIRECT_URI: z.string()\n});\nconst parsed = envSchema.safeParse(process.env);\n\nif (!parsed.success) {\n  console.error(\"Invalid environment variables\", z.treeifyError(parsed.error));\n  process.exit(1);\n}\n\nconst env = parsed.data;\n\nexport default env;\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/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/modules/otp/otp.validator.ts",
                          "content": "import z from \"zod\";\nimport { emailSchema } from \"../auth/auth.validator\";\nimport { OTP_TYPES } from \"./otp.constants\";\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  otpType: z.enum(OTP_TYPES, { error: \"Invalid otp type\" })\n});\n\nexport type RequestOtpType = z.infer<typeof RequestOtpSchema>;\nexport type VerifyOtpType = z.infer<typeof VerifyOtpSchema>;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/otp/otp.types.ts",
                          "content": "import { OTP_TYPES } from \"./otp.constants\";\n\nexport type OTPType = (typeof OTP_TYPES)[number];\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/otp/otp.service.ts",
                          "content": "import { NextFunction } from \"express\";\nimport { OTPType } from \"./otp.types\";\nimport User from \"../auth/user.model\";\nimport { ApiError } from \"../../shared/errors/api-error\";\nimport Otp from \"./otp.model\";\nimport {\n  NEXT_OTP_DELAY,\n  OTP_CODE_LENGTH,\n  OTP_EXPIRES_IN,\n  OTP_MAX_ATTEMPTS\n} from \"./otp.constants\";\nimport {\n  generateHashedToken,\n  generateOTP\n} from \"../../shared/helpers/token.helpers\";\nimport { logger } from \"../../shared/utils/logger\";\nimport { sendEmail } from \"../../shared/utils/send-mail\";\nimport { AuthService } from \"../auth/auth.service\";\nimport { RESET_PASSWORD_TOKEN_EXPIRY } from \"../auth/auth.constants\";\nimport { IUser } from \"../auth/auth.types\";\n\ntype VerifyOtpPayload = {\n  email: string;\n  otpCode: string;\n  otpType: OTPType;\n};\n\ntype VerifyOtpContext = {\n  setAuthCookie?: (accessToken: string, refreshToken: string) => void;\n};\n\ntype ResetPassowrdContext = {\n  setCookie?: (token: string) => void;\n};\n\ntype SendOtpPayload = {\n  email: string;\n  otpType: OTPType;\n  subject: string;\n};\n\nexport class OtpService {\n  static async sendOtp(next: NextFunction, payload: SendOtpPayload) {\n    const { email, otpType, subject } = payload;\n\n    const user = await User.findOne({ email });\n    if (!user) {\n      return next(ApiError.badRequest(\"Invalid request\"));\n    }\n\n    if (user.lockUntil && user.lockUntil > new Date()) {\n      return next(ApiError.badRequest(\"Account locked\"));\n    }\n\n    const existingOtp = await Otp.findOne({ email, type: otpType });\n\n    if (existingOtp && existingOtp.nextResendAllowedAt > new Date()) {\n      const remainingSec = Math.ceil(\n        (existingOtp.nextResendAllowedAt.getTime() - Date.now()) / 1000\n      );\n      return next(\n        ApiError.badRequest(\n          `Please wait ${remainingSec} seconds before requesting another OTP`\n        )\n      );\n    }\n\n    const otp = generateOTP(OTP_CODE_LENGTH, OTP_EXPIRES_IN);\n    logger.info(\n      `Sending OTP to ${email} with type ${otpType} and code ${otp.code}`\n    );\n    const nextResendAllowedAt = new Date(Date.now() + NEXT_OTP_DELAY);\n\n    let html = `<p>Your OTP for ${otpType}: ${otp.code}</p>`;\n    await sendEmail({\n      email,\n      subject: subject,\n      html\n    });\n\n    await Otp.create({\n      email: payload.email,\n      type: payload.otpType,\n      otpHashCode: otp.hashCode,\n      attempts: 0,\n      isUsed: false,\n      expiresAt: otp.expiresAt,\n      nextResendAllowedAt\n    });\n\n    return { message: `OTP sent to ${email} successfully` };\n  }\n\n  static async verifyOtp(\n    next: NextFunction,\n    payload: VerifyOtpPayload,\n    context: VerifyOtpContext,\n    resetPasswordContext: ResetPassowrdContext\n  ): Promise<any> {\n    const { email, otpCode, otpType } = payload;\n\n    const user = await User.findOne({ email });\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized, Please login first.\"));\n    }\n\n    if (user.lockUntil && user.lockUntil > new Date()) {\n      const minutes = Math.ceil(\n        (user.lockUntil.getTime() - Date.now()) / 60000\n      );\n      return next(\n        ApiError.badRequest(\n          `Your account has been locked. Try again in ${minutes} minutes.`\n        )\n      );\n    }\n\n    const otp = await Otp.findOne({\n      email,\n      type: otpType,\n      isUsed: false,\n      expiresAt: { $gt: new Date() }\n    })\n      .sort({ createdAt: -1 })\n      .select(\"+otpHashCode\");\n\n    if (!otp) {\n      return next(ApiError.badRequest(\"Invalid or expired OTP\"));\n    }\n\n    if (otp.attempts >= (otp.maxAttempts || OTP_MAX_ATTEMPTS)) {\n      return next(ApiError.badRequest(\"Maximum OTP attempts reached\"));\n    }\n\n    const hashedOtp = generateHashedToken(String(otpCode));\n\n    if (otp.otpHashCode !== hashedOtp) {\n      await Otp.updateOne({ _id: otp._id }, { $inc: { attempts: 1 } });\n      return next(ApiError.badRequest(\"Invalid OTP code\"));\n    }\n\n    otp.isUsed = true;\n    await otp.save();\n\n    if (otp.type === \"signin\") {\n      return await AuthService.handleToken(\n        {\n          _id: user._id.toString(),\n          role: user.role,\n          isEmailVerified: user.isEmailVerified\n        },\n        context\n      );\n    }\n\n    if (otp.type === \"password-reset\") {\n      return this.handlePasswordReset(\n        {\n          _id: user._id.toString()\n        },\n        resetPasswordContext\n      );\n    }\n\n    await Otp.deleteOne({ _id: otp._id });\n    await Otp.deleteMany({\n      expiresAt: { $lt: new Date() },\n      isUsed: true\n    });\n    return { message: \"OTP verified successfully\" };\n  }\n\n  private static handlePasswordReset(\n    user: Pick<IUser, \"_id\">,\n    context: ResetPassowrdContext\n  ) {\n    const hashedResetPasswordToken = generateHashedToken(user._id.toString());\n    const resetPasswordExpiry = new Date(\n      Date.now() + RESET_PASSWORD_TOKEN_EXPIRY\n    );\n\n    context.setCookie && context.setCookie(hashedResetPasswordToken);\n\n    return {\n      hashedResetPasswordToken,\n      resetPasswordExpiry\n    };\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/otp/otp.model.ts",
                          "content": "import mongoose, { Document, Model, Schema } from \"mongoose\";\nimport { OTPType } from \"./otp.types\";\nimport { OTP_EXPIRES_IN, OTP_MAX_ATTEMPTS, OTP_TYPES } from \"./otp.constants\";\n\n//? otp interface\nexport interface IOtp extends Document {\n  _id: mongoose.Types.ObjectId;\n  email: string;\n  otpHashCode: string;\n  nextResendAllowedAt: Date;\n  type: OTPType;\n  expiresAt: Date;\n  isUsed: boolean;\n  usedAt?: Date;\n  attempts: number;\n  maxAttempts: number;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\n//? otp schema\nconst otpSchema = new Schema<IOtp>(\n  {\n    email: {\n      type: String,\n      required: [true, \"Email is required\"],\n      lowercase: true,\n      trim: true\n    },\n    otpHashCode: {\n      type: String,\n      required: [true, \"OTP hash code is required\"],\n      select: false // Never return OTP hash code in queries by default\n    },\n    nextResendAllowedAt: {\n      type: Date,\n      required: [true, \"Next resend allowed at is required\"]\n    },\n    type: {\n      type: String,\n      enum: OTP_TYPES,\n      required: [true, \"OTP type is required\"]\n    },\n    expiresAt: {\n      type: Date,\n      required: [true, \"Expiration time is required\"]\n    },\n    isUsed: {\n      type: Boolean,\n      default: false\n    },\n    usedAt: {\n      type: Date\n    },\n    attempts: {\n      type: Number,\n      default: 0\n    },\n    maxAttempts: {\n      type: Number,\n      default: OTP_MAX_ATTEMPTS // Prevent brute force attacks\n    }\n  },\n  {\n    timestamps: true\n  }\n);\n\n// Performance Indexes\notpSchema.index({ email: 1, type: 1 }); // Quick lookup by email and type\notpSchema.index(\n  { createdAt: 1 },\n  { expireAfterSeconds: OTP_EXPIRES_IN / 1000 }\n); // ttl index\n\nconst Otp: Model<IOtp> =\n  mongoose.models.Otp || mongoose.model<IOtp>(\"Otp\", otpSchema);\n\nexport default Otp;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/otp/otp.constants.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 OTP_CODE_LENGTH = 6 as const;\n\nexport const OTP_EXPIRES_IN = 5 * 60 * 1000; // 5 minutes\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/upload/cloudinary.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/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/refresh-token.model.ts",
                          "content": "import mongoose, { Document, Model, Schema } from \"mongoose\";\nimport { REFRESH_TOKEN_EXPIRY } from \"./auth.constants\";\n\nexport interface IRefreshToken extends Document {\n  _id: mongoose.Types.ObjectId;\n  userId: mongoose.Types.ObjectId;\n  tokenHash: string;\n  expiresAt: Date;\n  isRevoked: boolean;\n  revokedAt?: Date;\n  replacedByTokenHash?: string;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nconst refreshTokenSchema = new Schema<IRefreshToken>(\n  {\n    userId: {\n      type: Schema.Types.ObjectId,\n      ref: \"User\",\n      required: [true, \"User ID is required\"]\n    },\n    tokenHash: {\n      type: String,\n      required: [true, \"Token hash is required\"],\n      select: false // Secure by default\n    },\n    expiresAt: {\n      type: Date,\n      required: [true, \"Expiration time is required\"]\n    },\n    isRevoked: {\n      type: Boolean,\n      default: false\n    },\n    revokedAt: {\n      type: Date\n    },\n    replacedByTokenHash: {\n      type: String,\n      select: false\n    }\n  },\n  {\n    timestamps: true\n  }\n);\n\nrefreshTokenSchema.index({ userId: 1 });\nrefreshTokenSchema.index({ tokenHash: 1 });\nrefreshTokenSchema.index({ isRevoked: 1 });\nrefreshTokenSchema.index(\n  { createdAt: 1 },\n  { expireAfterSeconds: REFRESH_TOKEN_EXPIRY / 1000 } // 7 days\n);\n\nconst RefreshToken: Model<IRefreshToken> =\n  mongoose.models.RefreshToken ||\n  mongoose.model<IRefreshToken>(\"RefreshToken\", refreshTokenSchema);\n\nexport default RefreshToken;\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.validator.ts",
                          "content": "import * as z from \"zod\";\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 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>;\n\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": "export 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"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.service.ts",
                          "content": "import mongoose from \"mongoose\";\nimport { NextFunction } from \"express\";\nimport User from \"./user.model\";\nimport { hashPassword, verifyPassword } from \"./auth.helpers\";\nimport {\n  LOCK_TIME_MS,\n  LOGIN_MAX_ATTEMPTS,\n  REACTIVATION_AVAILABLE_AT,\n  REFRESH_TOKEN_EXPIRY\n} from \"./auth.constants\";\n\nimport { OtpService } from \"../otp/otp.service\";\nimport { SignupUserType } from \"./auth.validator\";\nimport { ApiError } from \"../../shared/errors/api-error\";\nimport { IUser } from \"./auth.types\";\nimport {\n  generateAccessToken,\n  generateRefreshToken,\n  verifyAccessToken,\n  verifyRefreshToken\n} from \"../../shared/utils/jwt\";\nimport { generateHashedToken } from \"../../shared/helpers/token.helpers\";\nimport RefreshToken from \"./refresh-token.model\";\nimport { deleteFileFromCloudinary } from \"../upload/cloudinary.service\";\n\nexport type Context = {\n  setAuthCookie?: (accessToken: string, refreshToken: string) => void;\n};\n\nexport class AuthService {\n  static async registerUser(\n    next: NextFunction,\n    user: Omit<SignupUserType, \"confirmPassword\">\n  ) {\n    const { name, email, password, role } = user;\n    const existingUser = await User.findOne({ email }).select(\"+password\");\n\n    if (existingUser) {\n      return next(ApiError.conflict(\"User with this email already exists\"));\n    }\n\n    const hashedPassword = await hashPassword(password);\n\n    const newUser = await User.create({\n      name,\n      email,\n      password: hashedPassword,\n      role\n    });\n\n    return newUser;\n  }\n\n  static async loginAndSendOtp(\n    next: NextFunction,\n    { email, password }: { email: string; password: string }\n  ) {\n    const session = await mongoose.startSession();\n\n    try {\n      session.startTransaction();\n\n      const user = await User.findOne({ email })\n        .session(session)\n        .select(\"+password\");\n      if (!user) {\n        await session.abortTransaction();\n        return next(ApiError.unauthorized(\"Invalid credentials\"));\n      }\n\n      if (user.lockUntil && new Date(user.lockUntil) > new Date()) {\n        await session.abortTransaction();\n        return next(\n          ApiError.forbidden(\n            `Your account has been locked. Please try again after ${Math.ceil(\n              (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n            )} minutes.`\n          )\n        );\n      }\n\n      const isPasswordValid = await verifyPassword(\n        password,\n        user.password || \"\"\n      );\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          { email },\n          {\n            $set: {\n              failedLoginAttempts: newAttempts,\n              lockUntil\n            }\n          }\n        );\n\n        await session.abortTransaction();\n        return next(ApiError.unauthorized(\"Invalid credentials\"));\n      }\n\n      const otp = await OtpService.sendOtp(next, {\n        email,\n        otpType: \"signin\",\n        subject: \"Signin\"\n      });\n\n      if (!otp) {\n        await session.abortTransaction();\n        return next(ApiError.server(\"Failed to generate OTP\"));\n      }\n\n      await session.commitTransaction();\n      session.endSession();\n\n      return {\n        message: otp.message\n      };\n    } catch (err) {\n      await session.abortTransaction();\n      session.endSession();\n      return next(ApiError.server(\"Signin failed\"));\n    }\n  }\n\n  static async handleToken(\n    user: Pick<IUser, \"isEmailVerified\" | \"_id\" | \"role\">,\n    context: Context\n  ) {\n    if (!user.isEmailVerified) {\n      await User.updateOne(\n        { _id: user._id },\n        { $set: { isEmailVerified: true } }\n      );\n    }\n\n    const accessToken = generateAccessToken({\n      _id: user._id,\n      role: user.role\n    });\n\n    const refreshToken = generateRefreshToken(user._id);\n\n    const hashedRefreshToken = generateHashedToken(refreshToken);\n\n    await RefreshToken.create({\n      userId: user._id,\n      tokenHash: hashedRefreshToken,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    context.setAuthCookie && context.setAuthCookie(accessToken, refreshToken);\n\n    await User.updateOne(\n      { _id: user._id },\n      {\n        $set: { lastLogin: new Date(), failedLoginAttempts: 0 },\n        $unset: { lockUntil: 1 }\n      }\n    );\n    return { message: \"OTP verified and user signed in successfully\" };\n  }\n\n  static async getUserProfile(userId: string) {\n    const user = await User.findById(userId);\n    return user;\n  }\n\n  static async refreshTokens(\n    next: NextFunction,\n    accessToken: string | null,\n    refreshToken: string\n  ) {\n    if (!refreshToken) {\n      return next(ApiError.unauthorized(\"Unauthorized, please login.\"));\n    }\n\n    const decodedRefresh = verifyRefreshToken(refreshToken);\n    if (!decodedRefresh?.userId) {\n      return next(ApiError.unauthorized(\"Invalid refresh token.\"));\n    }\n\n    const refreshTokenHash = generateHashedToken(refreshToken);\n\n    const storedToken = await RefreshToken.findOne({\n      userId: decodedRefresh.userId,\n      tokenHash: refreshTokenHash\n    });\n\n    // Reuse detection\n    if (!storedToken) {\n      await RefreshToken.updateMany(\n        { userId: decodedRefresh.userId },\n        { isRevoked: true, revokedAt: new Date() }\n      );\n      return next(\n        ApiError.unauthorized(\"Token reuse detected. Please login again.\")\n      );\n    }\n\n    if (storedToken.isRevoked) {\n      return next(ApiError.unauthorized(\"Refresh token revoked.\"));\n    }\n\n    if (storedToken.expiresAt < new Date()) {\n      return next(ApiError.unauthorized(\"Refresh token expired.\"));\n    }\n\n    if (accessToken) {\n      const decodedAccess = verifyAccessToken(accessToken);\n      if (decodedAccess._id !== decodedRefresh.userId) {\n        return next(ApiError.unauthorized(\"Token mismatch.\"));\n      }\n    }\n\n    const user = await User.findById(decodedRefresh.userId);\n    if (!user) {\n      return next(ApiError.unauthorized(\"User not found.\"));\n    }\n\n    const newAccessToken = generateAccessToken({\n      _id: user._id.toString(),\n      role: user.role\n    });\n\n    const newRefreshToken = generateRefreshToken(user._id.toString());\n    const newRefreshTokenHash = generateHashedToken(newRefreshToken);\n\n    //? Rotate token\n    storedToken.isRevoked = true;\n    storedToken.revokedAt = new Date();\n    storedToken.replacedByTokenHash = newRefreshTokenHash;\n    await storedToken.save();\n\n    await RefreshToken.create({\n      userId: user._id,\n      tokenHash: newRefreshTokenHash,\n      expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY)\n    });\n\n    return {\n      accessToken: newAccessToken,\n      refreshToken: newRefreshToken\n    };\n  }\n\n  static async logoutUser(userId: string) {\n    return await RefreshToken.updateMany(\n      { userId },\n      { isRevoked: true, revokedAt: new Date() }\n    );\n  }\n\n  static async forgotPassword(next: NextFunction, email: string) {\n    const user = await User.findOne({ email });\n\n    if (!user) {\n      return next(\n        ApiError.badRequest(\"If this email is registered, check your inbox.\")\n      );\n    }\n\n    const result = await OtpService.sendOtp(next, {\n      email,\n      otpType: \"password-reset\",\n      subject: \"Password Reset\"\n    });\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to send otp!\"));\n    }\n\n    return result;\n  }\n\n  static async resetPassword(\n    next: NextFunction,\n    email: string,\n    newPassword: string\n  ) {\n    const user = await User.findOne({ email }).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 ${Math.ceil(\n            (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n          )} minutes.`\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 ${Math.ceil(\n            (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n          )} minutes.`\n        )\n      );\n    }\n\n    if (!user.isEmailVerified) {\n      return next(ApiError.unauthorized(\"Please verify your email first.\"));\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      { email },\n      {\n        $set: {\n          password: hashedPassword,\n          isEmailVerified: true\n        }\n      }\n    );\n    return { message: \"Password reset successfully!\" };\n  }\n\n  static async changePassword(\n    next: NextFunction,\n    {\n      newPassword,\n      oldPassword,\n      userId\n    }: { userId: string; newPassword: string; oldPassword: string }\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      { _id: userId },\n      {\n        $set: {\n          password: hashedPassword\n        }\n      }\n    );\n    return { message: \"Password changed successfully. Please login again!\" };\n  }\n\n  static async deleteOrDeactiveAccount(\n    next: NextFunction,\n    userId: string,\n    type: \"soft\" | \"hard\"\n  ) {\n    const user = await User.findById(userId);\n    if (!user) {\n      return next(ApiError.unauthorized(\"Unauthorized access\"));\n    }\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    } else if (type === \"hard\") {\n      if (user?.avatar?.public_id) {\n        await deleteFileFromCloudinary([user.avatar.public_id]);\n      }\n      await User.findOneAndDelete({ _id: userId });\n      await user.save();\n    }\n  }\n\n  static async reactivateAccount(next: NextFunction, userId: string) {\n    const user = await User.findById(userId);\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.badRequest(\n          `Your account has been locked. Please try again after ${Math.ceil(\n            (user.lockUntil.getTime() - Date.now()) / (1000 * 60)\n          )} minutes.`\n        )\n      );\n    }\n\n    if (!user?.isDeleted || !user?.deletedAt) {\n      return next(ApiError.badRequest(\"Your account is already active!\"));\n    }\n\n    if (\n      user?.reActivateAvailableAt &&\n      new Date(user?.reActivateAvailableAt) > new Date()\n    ) {\n      return next(\n        ApiError.forbidden(\n          `Your account has been locked. Please try again after ${Math.ceil(\n            (user.reActivateAvailableAt.getTime() - Date.now()) / (1000 * 60)\n          )} minutes.`\n        )\n      );\n    }\n\n    await User.findOneAndUpdate(\n      { _id: userId },\n      {\n        $set: {\n          isDeleted: false,\n          deletedAt: null,\n          reActivateAvailableAt: null\n        }\n      },\n      { new: true }\n    );\n\n    await user.save();\n  }\n}\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.routes.ts",
                          "content": "import { Router } from \"express\";\nimport {\n  ChangePasswordSchema,\n  DeleteAccountSchema,\n  ResetPasswordSchema,\n  SigninSchema,\n  SignupSchema,\n  UpdateProfileSchema\n} from \"./auth.validator\";\nimport {\n  changePassword,\n  deleteAccount,\n  forgotPassword,\n  getUserProfile,\n  logoutUser,\n  reactivateAccount,\n  refreshToken,\n  resetPassword,\n  signinUser,\n  signupUser,\n  updateProfile,\n  verifyOtp\n} from \"./auth.controller\";\nimport {\n  changePasswordLimiter,\n  deleteAccountLimiter,\n  otpRequestLimiter,\n  otpVerificationLimiter,\n  resetPasswordLimiter,\n  signinRateLimiter,\n  signupRateLimiter\n} from \"../../shared/middlewares/rate-limiter\";\nimport { validateRequest } from \"../../shared/middlewares/validate-request\";\nimport { verifyAuthentication } from \"../../shared/middlewares/verify-auth\";\nimport upload from \"../../shared/middlewares/upload-file\";\nimport { checkUserAccountRestriction } from \"../../shared/middlewares/user-account-restriction\";\nimport { RequestOtpSchema, VerifyOtpSchema } from \"../otp/otp.validator\";\n\nconst router = Router();\n\nrouter.post(\n  \"/verify-otp\",\n  validateRequest(VerifyOtpSchema),\n  otpVerificationLimiter,\n  verifyOtp\n);\n\nrouter.post(\n  \"/signup\",\n  validateRequest(SignupSchema),\n  signupRateLimiter,\n  signupUser\n);\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.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  \"/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.delete(\n  \"/delete-account\",\n  verifyAuthentication,\n  validateRequest(DeleteAccountSchema),\n  checkUserAccountRestriction,\n  deleteAccountLimiter,\n  deleteAccount\n);\n\nrouter.put(\"/reactivate-account\", 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 { AsyncHandler } from \"../../shared/utils/async-handler\";\nimport { DeleteAccountType } from \"./auth.validator\";\nimport { ApiError } from \"../../shared/errors/api-error\";\nimport { OtpService } from \"../otp/otp.service\";\nimport {\n  clearAuthCookies,\n  setAuthCookies,\n  setCookies\n} from \"../../shared/helpers/cookie.helper\";\nimport { RESET_PASSWORD_TOKEN_EXPIRY } from \"./auth.constants\";\nimport { ApiResponse } from \"../../shared/utils/api-response\";\nimport { AuthService } from \"./auth.service\";\nimport { UserRequest } from \"../../@types/global\";\nimport {\n  deleteFileFromCloudinary,\n  uploadToCloudinary\n} from \"../upload/cloudinary.service\";\nimport { VerifyOtpType } from \"../otp/otp.validator\";\n\n//? VERIFY OTP\nexport const verifyOtp = AsyncHandler(\n  async (req: Request, res: Response, next: NextFunction) => {\n    const { email, otpCode, otpType }: VerifyOtpType = req.body;\n    if (!email || !otpCode || !otpType) {\n      return next(\n        ApiError.badRequest(\"Email, OTP code and OTP type are required\")\n      );\n    }\n\n    const otp = await OtpService.verifyOtp(\n      next,\n      { email, otpCode, otpType },\n      {\n        setAuthCookie: (accessToken: string, refreshToken: string) => {\n          setAuthCookies(res, accessToken, refreshToken);\n        }\n      },\n      {\n        setCookie: (token: string) => {\n          setCookies(res, [\n            {\n              cookie: \"hashedResetPasswordToken\",\n              value: token,\n              maxAge: RESET_PASSWORD_TOKEN_EXPIRY\n            }\n          ]);\n        }\n      }\n    );\n\n    if (!otp) {\n      return next(ApiError.server(\"Failed to verify OTP!\"));\n    }\n    return ApiResponse.ok(res, otp.message || \"OTP verified successfully!\");\n  }\n);\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    const user = await AuthService.registerUser(next, {\n      name,\n      email,\n      password,\n      role\n    });\n\n    if (!user) {\n      return next(ApiError.server(\"Failed to register user!\"));\n    }\n\n    return ApiResponse.created(res, \"User registered successfully\", {\n      name: user.name,\n      email: user.email,\n      role: user.role\n    });\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 result = await AuthService.loginAndSendOtp(next, { email, password });\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to login!\"));\n    }\n\n    return ApiResponse.ok(res, result.message || \"Otp sent 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    if (!userId) {\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    return ApiResponse.ok(res, \"User profile fetched successfully\", {\n      user: {\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//? 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!\", user);\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(\n      next,\n      accessToken,\n      refreshToken\n    );\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);\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    await AuthService.logoutUser(userId.toString());\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    const result = await AuthService.forgotPassword(next, email);\n\n    if (!result) {\n      return next(ApiError.server(\"Failed to send otp!\"));\n    }\n\n    return ApiResponse.ok(res, result.message || \"Otp sent successfully!\");\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 hashedResetPasswordToken = req.cookies?.hashedResetPasswordToken;\n\n    if (!hashedResetPasswordToken) {\n      return next(\n        ApiError.badRequest(\"Reset password token not found or expired\")\n      );\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    res.clearCookie(\"hashedResetPasswordToken\");\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//? 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\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(next, userId, type);\n\n    if (type === \"hard\") {\n      clearAuthCookies(res);\n    }\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(next, userId);\n\n    return ApiResponse.Success(res, \"Account reactivated successfully!\");\n  }\n);\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/auth/auth.constants.ts",
                          "content": "export const LOGIN_MAX_ATTEMPTS = 5 as const;\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 RESET_PASSWORD_TOKEN_EXPIRY = 5 * 60 * 1000; // 5 minutes\n\nexport const REACTIVATION_AVAILABLE_AT = 24 * 60 * 60 * 1000; // 24 hours\n"
                        },
                        {
                          "type": "file",
                          "path": "src/modules/oauth/oauth.service.ts",
                          "content": "import { AuthService, Context } 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};\n\nexport class OAuthService {\n  static async handleOAuthLogin(user: OAuthProfile, context: Context) {\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          isEmailVerified: existingUser.isEmailVerified\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        isEmailVerified: newUser.isEmailVerified\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 { 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  \"/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\";\n\nimport { Profile as GoogleProfile } from \"passport-google-oauth20\";\nimport { AsyncHandler } from \"../../shared/utils/async-handler\";\nimport { ApiError } from \"../../shared/errors/api-error\";\nimport { OAuthService } from \"./oauth.service\";\nimport { setAuthCookies } from \"../../shared/helpers/cookie.helper\";\nimport { ApiResponse } from \"../../shared/utils/api-response\";\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    // console.log(data);\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    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(user, {\n      setAuthCookie: (accessToken: string, refreshToken: string) => {\n        setAuthCookies(res, accessToken, refreshToken);\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    };\n\n    const existingUser = await OAuthService.handleOAuthLogin(userInfo, {\n      setAuthCookie: (accessToken: string, refreshToken: string) => {\n        setAuthCookies(res, accessToken, refreshToken);\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/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\";\n\n/**\n * Basic health check endpoint\n * GET /api/health\n */\nexport const healthCheck = 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 * Detailed health check with system information\n * GET /api/health/detailed\n */\nexport const detailedHealthCheck = 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) / 100,\n      total:\n        Math.round((process.memoryUsage().heapTotal / 1024 / 1024) * 100) / 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"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
