==== File : server/db.
ts =====
// db.ts
import mongoose from "mongoose";
import dotenv from "dotenv";
dotenv.config();
const mongoUrl = process.env.DATABASE_URL;
if (!mongoUrl) {
throw new Error("DATABASE_URL must be set in .env");
}
export async function connectDB() {
try {
✅
const connection = await mongoose.connect(mongoUrl);
console.log(" Connected to MongoDB Atlas");
return connection; // return the connection object
❌
} catch (error) {
console.error(" Failed to connect MongoDB:", error);
process.exit(1);
}
}
====== File : env.ts =======
import dotenv from "dotenv";
const result = dotenv.config();
if (result.error) {
throw result.error;
}
export const env = process.env;
====== File : server/index.ts ========
import express, { Request, Response, NextFunction } from "express";
import "tsconfig-paths/register";
import cors from "cors";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import path from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
import otpRoutes from "./routes/auth/otp.routes";
import authRoutes from "./routes/auth.routes";
import { registerRoutes } from "./routes";
import { setupVite, serveStatic, log } from "./vite";
import { connectDB } from "./db";
import { setupSwagger } from "./swagger";
import { verifyToken as authMiddleware } from "./middlewares/auth.middleware";
// Resolve __dirname for ES Modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Setup Express
const app = express();
// Connect to DB before server starts
await connectDB();
// Swagger setup
setupSwagger(app);
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Rate Limiting
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { message: "Too many requests from this IP, please try again later." },
});
app.use("/api", apiLimiter);
// Routes
app.use("/api/auth", authRoutes);
app.use("/api/auth/otp", otpRoutes);
// app.use("/api", authMiddleware); // Enable if needed
// Request logger
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
let capturedJsonResponse: Record<string, any> | undefined;
const originalResJson = res.json;
res.json = function (bodyJson, ...args) {
capturedJsonResponse = bodyJson;
return originalResJson.apply(res, [bodyJson, ...args]);
};
res.on("finish", () => {
const duration = Date.now() - start;
if (path.startsWith("/api")) {
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
if (capturedJsonResponse) {
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
}
if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + "…";
}
log(logLine);
}
});
next();
});
// Global error handler
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = err.message || "Internal Server Error";
❌
res.status(status).json({ message });
console.error(" Server Error:", err);
});
// Start server
const startServer = async () => {
await registerRoutes(app);
const port = parseInt(process.env.PORT || "5000", 10);
if (app.get("env") === "development") {
await setupVite(app); // Vite dev middleware
} else {
serveStatic(app);
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "..", "client", "dist", "index.html"));
});
}
✅
app.listen(port, "0.0.0.0", () => {
log(` Server is running on http://localhost:${port}`);
});
};
❌
startServer().catch((err) => {
console.error(" Failed to start server:", err);
process.exit(1);
});
========== File : server/routes.ts ========
import type { Express } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import {
insertBusinessSchema,
insertProductSchema,
insertJobSchema,
insertRideRequestSchema,
insertReviewSchema,
} from "@shared/schema";
import { z } from "zod";
import jwt from "jsonwebtoken";
import { verifyToken, getUserFromRequest } from "./middlewares/auth.middleware";
import { env } from "./env";
import { Redis } from "@upstash/redis";
import { nanoid } from "nanoid";
import twilio from "twilio";
// Import register/initiate and register/verify from auth.routes.ts
import authRoutes from "./routes/auth.routes"; // Assumes you default-exported your router in
auth.routes.ts
// Mobile normalization helper
function normalizeMobile(mobile: string) {
let cleaned = mobile.replace(/\D/g, "");
if (cleaned.length === 10) {
cleaned = "91" + cleaned;
}
if (!cleaned.startsWith("91")) cleaned = "91" + cleaned.slice(-10);
return "+" + cleaned;
}
// Initialize Upstash Redis
const redis = new Redis({
url: env.UPSTASH_REDIS_REST_URL,
token: env.UPSTASH_REDIS_REST_TOKEN,
});
// Initialize Twilio client
const twilioClient = twilio(env.TWILIO_ACCOUNT_SID, env.TWILIO_AUTH_TOKEN);
// Send SMS via Twilio (just SMS, no verify API)
async function sendSmsOtp(mobile: string, otp: string) {
return twilioClient.messages.create({
body: `Your OTP is ${otp}`,
to: mobile,
from: env.TWILIO_PHONE_NUMBER,
});
}
export async function registerRoutes(app: Express): Promise<Server> {
// Mount auth routes (includes /register/initiate and /register/verify)
app.use("/", authRoutes);
// Email/Password Login
app.post("/login/password", async (req, res) => {
const { email, password } = req.body;
const user = await storage.getUserByEmail(email);
if (!user) return res.status(404).json({ message: "User not found" });
const isValid = await storage.validateUserPassword(email, password);
if (!isValid) return res.status(401).json({ message: "Invalid password" });
const token = jwt.sign({ sub: user.id }, env.JWT_SECRET, { expiresIn: "7d" });
res.json({ token });
});
// Email OTP - Send
app.post("/send-email-otp", async (req, res) => {
const { email } = req.body;
if (!email) return res.status(400).json({ message: "Email is required" });
const emailOtp = Math.floor(100000 + Math.random() * 900000).toString();
await redis.set(`otp:email:${email}`, emailOtp, { ex: 300 });
console.log(`[EMAIL OTP] Sent OTP ${emailOtp} to ${email}`);
// TODO: Add email sending logic here if needed
res.json({ message: "Email OTP sent" });
});
// Email OTP - Verify (Do NOT delete OTP here)
app.post("/verify-email-otp", async (req, res) => {
const { email, emailOTP } = req.body;
const expectedOtp = await redis.get(`otp:email:${email}`);
console.log(`[EMAIL OTP VERIFY] Email: ${email} | Input: ${emailOTP} | Stored:
${expectedOtp}`);
if (expectedOtp !== emailOTP) {
return res.status(400).json({ message: "Invalid or expired OTP" });
}
// OTP deletion removed here to delete after user creation
res.json({ message: "OTP verification successful" });
});
// Mobile OTP - Send (generate OTP, save in Redis, send SMS via Twilio)
app.post("/send-mobile-otp", async (req, res) => {
const { mobile } = req.body;
if (!mobile) return res.status(400).json({ message: "Mobile number is required" });
const normalizedMobile = normalizeMobile(mobile);
const otp = Math.floor(100000 + Math.random() * 900000).toString();
try {
// Send SMS via Twilio
await sendSmsOtp(normalizedMobile, otp);
// Save OTP in Redis with 5 minutes expiry
await redis.set(`otp:mobile:${normalizedMobile}`, otp, { ex: 300 });
console.log(`[MOBILE OTP] Sent OTP ${otp} to ${normalizedMobile}`);
res.json({ message: "Mobile OTP sent" });
} catch (err) {
console.error("Error sending mobile OTP:", err);
res.status(500).json({ message: "Failed to send mobile OTP" });
}
});
// Mobile OTP - Verify (Do NOT delete OTP here)
app.post("/verify-mobile-otp", async (req, res) => {
const { mobile, mobileOTP } = req.body;
if (!mobile || !mobileOTP)
return res.status(400).json({ message: "Mobile number and OTP are required" });
const normalizedMobile = normalizeMobile(mobile);
const storedOtp = await redis.get(`otp:mobile:${normalizedMobile}`);
console.log(`[MOBILE OTP VERIFY] Mobile: ${normalizedMobile} | Input: ${mobileOTP} |
Stored: ${storedOtp}`);
if (storedOtp !== mobileOTP) {
return res.status(400).json({ message: "Invalid or expired OTP" });
}
// OTP deletion removed here to delete after user creation
res.json({ message: "Mobile verified successfully" });
});
// Google Social Login/Register
app.post("/login/social", async (req, res) => {
const { email, provider } = req.body;
let user = await storage.getUserByEmail(email);
if (!user) {
user = await storage.createUser({
email,
provider: provider || "google",
});
}
const token = jwt.sign({ sub: user.id }, env.JWT_SECRET, { expiresIn: "7d" });
res.json({ token });
});
// Phone Login/Register (mobile OTP via Redis)
app.post("/login/mobile", async (req, res) => {
const { mobile, mobileOTP } = req.body;
if (!mobile || !mobileOTP) return res.status(400).json({ message: "Mobile and OTP required"
});
const normalizedMobile = normalizeMobile(mobile);
const storedOtp = await redis.get(`otp:mobile:${normalizedMobile}`);
if (storedOtp !== mobileOTP) {
return res.status(401).json({ message: "Mobile verification failed" });
}
// Delete OTP after successful login
await redis.del(`otp:mobile:${normalizedMobile}`);
let user = await storage.getUserByPhone(normalizedMobile);
if (!user) {
user = await storage.createUser({
phone: normalizedMobile,
provider: "phone",
});
}
const token = jwt.sign({ sub: user.id }, env.JWT_SECRET, { expiresIn: "7d" });
res.json({ token });
});
// Fetch Logged In User
app.get("/api/auth/user", async (req: any, res) => {
try {
let user = null;
try {
user = await getUserFromRequest(req);
} catch {
user = null;
}
return res.json({ user });
} catch (error) {
console.error("Error fetching user:", error);
res.status(500).json({ message: "Failed to fetch user" });
}
});
// Business routes
app.get("/api/businesses", async (req, res) => {
try {
const { category, search } = req.query;
const businesses = await storage.getBusinesses(category as string, search as string);
res.json(businesses);
} catch (error) {
console.error("Error fetching businesses:", error);
res.status(500).json({ message: "Failed to fetch businesses" });
}
});
app.get("/api/businesses/:id", async (req, res) => {
try {
const business = await storage.getBusiness(req.params.id);
if (!business) return res.status(404).json({ message: "Business not found" });
res.json(business);
} catch (error) {
console.error("Error fetching business:", error);
res.status(500).json({ message: "Failed to fetch business" });
}
});
app.post("/api/businesses", verifyToken, async (req: any, res) => {
try {
const userId = req.user.sub;
const businessData = insertBusinessSchema.parse({ ...req.body, ownerId: userId });
const business = await storage.createBusiness(businessData);
res.json(business);
} catch (error) {
if (error instanceof z.ZodError)
return res.status(400).json({ message: "Invalid business data", errors: error.errors });
console.error("Error creating business:", error);
res.status(500).json({ message: "Failed to create business" });
}
});
app.get("/api/my-businesses", verifyToken, async (req: any, res) => {
try {
const userId = req.user.sub;
const businesses = await storage.getBusinessesByOwner(userId);
res.json(businesses);
} catch (error) {
console.error("Error fetching user businesses:", error);
res.status(500).json({ message: "Failed to fetch businesses" });
}
});
// Product routes
app.get("/api/products", async (req, res) => {
try {
const { businessId } = req.query;
const products = await storage.getProducts(businessId as string);
res.json(products);
} catch (error) {
console.error("Error fetching products:", error);
res.status(500).json({ message: "Failed to fetch products" });
}
});
app.post("/api/products", verifyToken, async (req: any, res) => {
try {
const productData = insertProductSchema.parse(req.body);
const product = await storage.createProduct(productData);
res.json(product);
} catch (error) {
if (error instanceof z.ZodError)
return res.status(400).json({ message: "Invalid product data", errors: error.errors });
console.error("Error creating product:", error);
res.status(500).json({ message: "Failed to create product" });
}
});
// Job routes
app.get("/api/jobs", async (req, res) => {
try {
const { category, location } = req.query;
const jobs = await storage.getJobs(category as string, location as string);
res.json(jobs);
} catch (error) {
console.error("Error fetching jobs:", error);
res.status(500).json({ message: "Failed to fetch jobs" });
}
});
app.get("/api/jobs/:id", async (req, res) => {
try {
const job = await storage.getJob(req.params.id);
if (!job) return res.status(404).json({ message: "Job not found" });
res.json(job);
} catch (error) {
console.error("Error fetching job:", error);
res.status(500).json({ message: "Failed to fetch job" });
}
});
app.post("/api/jobs", verifyToken, async (req: any, res) => {
try {
const jobData = insertJobSchema.parse(req.body);
const job = await storage.createJob(jobData);
res.json(job);
} catch (error) {
if (error instanceof z.ZodError)
return res.status(400).json({ message: "Invalid job data", errors: error.errors });
console.error("Error creating job:", error);
res.status(500).json({ message: "Failed to create job" });
}
});
// Ride routes
app.post("/api/ride-requests", verifyToken, async (req: any, res) => {
try {
const userId = req.user.sub;
const rideRequestData = insertRideRequestSchema.parse({ ...req.body, userId });
const rideRequest = await storage.createRideRequest(rideRequestData);
res.json(rideRequest);
} catch (error) {
if (error instanceof z.ZodError)
return res.status(400).json({ message: "Invalid ride request data", errors: error.errors });
console.error("Error creating ride request:", error);
res.status(500).json({ message: "Failed to create ride request" });
}
});
app.get("/api/my-ride-requests", verifyToken, async (req: any, res) => {
try {
const userId = req.user.sub;
const rideRequests = await storage.getRideRequests(userId);
res.json(rideRequests);
} catch (error) {
console.error("Error fetching ride requests:", error);
res.status(500).json({ message: "Failed to fetch ride requests" });
}
});
// Review routes
app.post("/api/reviews", verifyToken, async (req: any, res) => {
try {
const userId = req.user.sub;
const reviewData = insertReviewSchema.parse({ ...req.body, userId });
const review = await storage.createReview(reviewData);
res.json(review);
} catch (error) {
if (error instanceof z.ZodError)
return res.status(400).json({ message: "Invalid review data", errors: error.errors });
console.error("Error creating review:", error);
res.status(500).json({ message: "Failed to create review" });
}
});
app.get("/api/businesses/:id/reviews", async (req, res) => {
try {
const reviews = await storage.getBusinessReviews(req.params.id);
res.json(reviews);
} catch (error) {
console.error("Error fetching reviews:", error);
res.status(500).json({ message: "Failed to fetch reviews" });
}
});
const httpServer = createServer(app);
return httpServer;
}
========= File : server/storage.ts ==========
import {
type User,
type UpsertUser,
type Business,
type InsertBusiness,
type Product,
type InsertProduct,
type Job,
type InsertJob,
type RideRequest,
type InsertRideRequest,
type Review,
type InsertReview,
} from "@shared/schema";
import { BusinessModel } from "./models/business.model";
import { ProductModel } from "./models/product.model";
import { JobModel } from "./models/job.model";
import { RideRequestModel } from "./models/rideRequest.model";
import { ReviewModel } from "./models/review.model";
import { UserModel } from "./models/user.model";
// Add getUserByEmail to the interface
export interface IStorage {
getUser(id: string): Promise<User | null>;
getUserByEmail(email: string): Promise<User | null>;
createUser(user: UpsertUser): Promise<User>;
upsertUser(user: UpsertUser): Promise<User>;
getBusinesses(category?: string, search?: string): Promise<Business[]>;
getBusiness(id: string): Promise<Business | null>;
createBusiness(business: InsertBusiness): Promise<Business>;
updateBusiness(id: string, business: Partial<InsertBusiness>): Promise<Business | null>;
getBusinessesByOwner(ownerId: string): Promise<Business[]>;
getProducts(businessId?: string): Promise<Product[]>;
getProduct(id: string): Promise<Product | null>;
createProduct(product: InsertProduct): Promise<Product>;
getJobs(category?: string, location?: string): Promise<Job[]>;
getJob(id: string): Promise<Job | null>;
createJob(job: InsertJob): Promise<Job>;
createRideRequest(rideRequest: InsertRideRequest): Promise<RideRequest>;
getRideRequests(userId: string): Promise<RideRequest[]>;
createReview(review: InsertReview): Promise<Review>;
getBusinessReviews(businessId: string): Promise<Review[]>;
}
export class DatabaseStorage implements IStorage {
async getUser(id: string): Promise<User | null> {
return await UserModel.findById(id).lean();
}
// Implementation of getUserByEmail
async getUserByEmail(email: string): Promise<User | null> {
return await UserModel.findOne({ email }).lean();
}
// NEW createUser method to trigger pre-save hooks like password hashing
async createUser(user: UpsertUser): Promise<User> {
const newUser = new UserModel(user);
await newUser.save(); // triggers pre('save') hooks, including password hashing
return newUser.toObject();
}
async upsertUser(user: UpsertUser): Promise<User> {
return await UserModel.findByIdAndUpdate(user.id, user, { upsert: true, new: true,
setDefaultsOnInsert: true }).lean();
}
async getBusinesses(category?: string, search?: string): Promise<Business[]> {
const query: any = { isActive: true };
if (category) query.category = category;
if (search) query.name = { $regex: search, $options: "i" };
return await BusinessModel.find(query).sort({ rating: -1, createdAt: -1 }).lean();
}
async getBusiness(id: string): Promise<Business | null> {
return await BusinessModel.findById(id).lean();
}
async createBusiness(business: InsertBusiness): Promise<Business> {
const newBusiness = new BusinessModel(business);
await newBusiness.save();
return newBusiness.toObject();
}
async updateBusiness(id: string, business: Partial<InsertBusiness>): Promise<Business | null>
{
return await BusinessModel.findByIdAndUpdate(id, business, { new: true }).lean();
}
async getBusinessesByOwner(ownerId: string): Promise<Business[]> {
return await BusinessModel.find({ ownerId }).sort({ createdAt: -1 }).lean();
}
async getProducts(businessId?: string): Promise<Product[]> {
const query: any = { isAvailable: true };
if (businessId) query.businessId = businessId;
return await ProductModel.find(query).sort({ createdAt: -1 }).lean();
}
async getProduct(id: string): Promise<Product | null> {
return await ProductModel.findById(id).lean();
}
async createProduct(product: InsertProduct): Promise<Product> {
const newProduct = new ProductModel(product);
await newProduct.save();
return newProduct.toObject();
}
async getJobs(category?: string, location?: string): Promise<Job[]> {
const query: any = { isActive: true };
if (category) query.category = category;
if (location) query.location = { $regex: location, $options: "i" };
return await JobModel.find(query).sort({ createdAt: -1 }).lean();
}
async getJob(id: string): Promise<Job | null> {
return await JobModel.findById(id).lean();
}
async createJob(job: InsertJob): Promise<Job> {
const newJob = new JobModel(job);
await newJob.save();
return newJob.toObject();
}
async createRideRequest(rideRequest: InsertRideRequest): Promise<RideRequest> {
const newRideRequest = new RideRequestModel(rideRequest);
await newRideRequest.save();
return newRideRequest.toObject();
}
async getRideRequests(userId: string): Promise<RideRequest[]> {
return await RideRequestModel.find({ userId }).sort({ createdAt: -1 }).lean();
}
async createReview(review: InsertReview): Promise<Review> {
const newReview = await ReviewModel.create(review);
const reviews = await ReviewModel.find({ businessId: review.businessId });
const avgRating = reviews.reduce((sum, r) => sum + r.rating, 0) / (reviews.length || 1);
await BusinessModel.findByIdAndUpdate(review.businessId, {
rating: avgRating.toFixed(1),
reviewCount: reviews.length,
});
return newReview.toObject();
}
async getBusinessReviews(businessId: string): Promise<Review[]> {
return await ReviewModel.find({ businessId }).sort({ createdAt: -1 }).lean();
}
}
export const storage = new DatabaseStorage();
===== File : server/config/mailer.ts=======
import nodemailer from "nodemailer";
import dotenv from "dotenv";
dotenv.config();
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT) || 587,
secure: false, // true for 465, false for other ports
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
/**
* Send an email
* @param to Receiver's email
* @param subject Subject line
* @param html HTML content
*/
export const sendEmail = async (to: string, subject: string, html: string) => {
try {
const info = await transporter.sendMail({
from: process.env.FROM_EMAIL, // e.g., "YourApp <[email protected]>"
to,
subject,
html,
});
console.log("Email sent: %s", info.messageId);
return true;
} catch (err) {
console.error("Email send failed:", err);
return false;
}
};
===== File : server/config/redis.ts======
// server/config/redis.ts
import { Redis } from "@upstash/redis";
import dotenv from "dotenv";
dotenv.config();
if (!process.env.UPSTASH_REDIS_REST_URL ||
❌
!process.env.UPSTASH_REDIS_REST_TOKEN) {
throw new Error(" UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN is
not defined in .env");
}
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
// Optional: test connection
(async () => {
try {
✅
await redis.ping();
console.log(" Connected to Upstash Redis");
❌
} catch (err: any) {
console.error(" Redis connection error:", err.message || err);
}
})();
export default redis;
===== File : server/controllers/auth/otp.controller.ts=======
// server/config/redis.ts
import { Redis } from "@upstash/redis";
import dotenv from "dotenv";
dotenv.config();
if (!process.env.UPSTASH_REDIS_REST_URL ||
❌
!process.env.UPSTASH_REDIS_REST_TOKEN) {
throw new Error(" UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN is
not defined in .env");
}
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
// Optional: test connection
(async () => {
try {
✅
await redis.ping();
console.log(" Connected to Upstash Redis");
❌
} catch (err: any) {
console.error(" Redis connection error:", err.message || err);
}
})();
export default redis;
====== File : server/controllers/authController.ts =======
import { Request, Response } from "express";
import * as authService from "../services/auth.service"; // <-- Importing auth.service
import { createToken } from "../utils/jwt.util";
import User from "../models/user.model";
// -------------------- REGISTRATION --------------------
// Step 1: Initiate registration by sending OTPs (user not yet created)
export const registerInitiate = async (req: Request, res: Response) => {
try {
const { name, email, mobile, password } = req.body;
if (!name || !email || !mobile || !password) {
return res.status(400).json({ message: "All fields are required" });
}
const result = await authService.registerInitiate(
name,
email,
mobile,
password
);
return res.json({
message: "OTP sent to email and mobile for verification.",
...result,
});
} catch (error: any) {
console.error("registerInitiate error:", error);
return res.status(500).json({ message: error.message || "Server error" });
}
};
// Step 2: Verify OTPs and create user
export const registerVerify = async (req: Request, res: Response) => {
try {
const { userId, emailOTP, mobileOTP } = req.body;
if (!userId || !emailOTP || !mobileOTP) {
return res
.status(400)
.json({ message: "userId, emailOTP, and mobileOTP are required" });
}
const result = await authService.registerVerify(
userId,
emailOTP,
mobileOTP
);
return res.json(result);
} catch (error: any) {
console.error("registerVerify error:", error);
return res.status(500).json({ message: error.message || "Server error" });
}
};
// -------------------- LOGIN --------------------
// Login with email & password
export const loginWithPassword = async (req: Request, res: Response) => {
try {
const { email, password } = req.body;
if (!email || !password)
return res
.status(400)
.json({ message: "Email and password are required" });
const result = await authService.loginWithPassword(email, password);
return res.json(result);
} catch (error: any) {
console.error("loginWithPassword error:", error);
return res.status(500).json({ message: error.message || "Server error" });
}
};
// Login with mobile OTP
export const loginWithMobileOTP = async (req: Request, res: Response) => {
try {
const { mobile, mobileOTP } = req.body;
if (!mobile || !mobileOTP)
return res
.status(400)
.json({ message: "Mobile and OTP are required" });
const result = await authService.loginWithMobileOTP(mobile, mobileOTP);
return res.json(result);
} catch (error: any) {
console.error("loginWithMobileOTP error:", error);
return res.status(500).json({ message: error.message || "Server error" });
}
};
====== File : server/middlewares/auth.middleware.ts =======
// server/middlewares/auth.middleware.ts
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";
// Define what the decoded JWT will look like
export interface DecodedToken {
uid: string;
email?: string;
iat: number;
exp: number;
}
// Extend Express Request interface to include `user`
declare module "express-serve-static-core" {
interface Request {
user?: DecodedToken;
}
}
// Middleware for protecting routes
export const verifyToken = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ message: "Unauthorized: No token provided" });
}
❌
if (!secret) {
console.error(" JWT_SECRET is not defined in environment variables.");
return res.status(500).json({ message: "Server configuration error" });
}
try {
const decoded = jwt.verify(token, secret) as DecodedToken;
req.user = decoded;
next();
❌
} catch (error) {
console.error(" Token verification failed:", error);
return res.status(403).json({ message: "Forbidden: Invalid or expired token" });
}
};
// Helper for optionally extracting user from request, returns null if not present or invalid
export const getUserFromRequest = (req: Request): DecodedToken | null => {
const authHeader = req.headers.authorization;
const secret = process.env.JWT_SECRET;
if (!authHeader || !authHeader.startsWith("Bearer ") || !secret) {
return null;
}
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, secret) as DecodedToken;
return decoded;
} catch {
return null;
}
};
====== File : rateLimit.middleware.ts ======
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
import redis from "@server/config/redis"; // your Redis instance
import { Request, Response } from "express";
// Apply to OTP or login-related routes
export const otpRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 OTP requests per windowMs
message: {
status: 429,
message: "Too many OTP requests. Please try again after 15 minutes.",
},
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req: Request): string => {
return req.ip || "global";
},
store: new RedisStore({
sendCommand: (...args: string[]) => redis.call(...args),
}),
});
// Apply to login routes
export const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: {
status: 429,
message: "Too many login attempts. Please try again later.",
},
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req: Request): string => {
return req.ip || "global";
},
store: new RedisStore({
sendCommand: (...args: string[]) => redis.call(...args),
}),
});
====== File : server/models/user.models.ts =========
import mongoose from "mongoose";
import bcrypt from "bcryptjs";
export interface IUser extends mongoose.Document {
name?: string;
email?: string;
mobile?: string;
password?: string;
emailVerified?: boolean;
phoneVerified?: boolean;
comparePassword(password: string): Promise<boolean>;
}
const userSchema = new mongoose.Schema<IUser>(
{
name: {
type: String,
trim: true,
},
email: {
type: String,
unique: true,
sparse: true,
lowercase: true,
trim: true,
},
mobile: {
type: String,
unique: true,
sparse: true,
trim: true,
},
password: {
type: String,
minlength: 6,
},
emailVerified: {
type: Boolean,
default: false,
},
mobileVerified: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
}
);
// Hash password before saving
userSchema.pre("save", async function (next) {
if (!this.isModified("password") || !this.password) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Compare password method
userSchema.methods.comparePassword = async function (password: string):
Promise<boolean> {
if (!this.password) return false;
return await bcrypt.compare(password, this.password);
};
export const UserModel = mongoose.model<IUser>("User", userSchema);
export default UserModel;
======= File : server/models/business.model.ts ======
// server/models/business.model.ts
import mongoose from "mongoose";
const businessSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
category: {
type: String,
required: true,
enum: ["shop", "service", "ride", "delivery", "job", "others"],
},
description: {
type: String,
},
address: {
street: String,
city: String,
state: String,
pincode: String,
},
phone: {
type: String,
required: true,
},
email: {
type: String,
},
location: {
latitude: { type: Number },
longitude: { type: Number },
},
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
approved: {
type: Boolean,
default: false,
},
services: {
type: [String],
default: [],
},
createdAt: {
type: Date,
default: Date.now,
},
},
{
timestamps: true,
}
);
export const BusinessModel = mongoose.model("Business", businessSchema);
====== File : server/models/job.models.ts =======
// server/models/job.model.ts
import mongoose from "mongoose";
const jobSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
},
description: {
type: String,
default: "",
},
jobType: {
type: String,
enum: ["Full-time", "Part-time", "Contract", "Internship", "Freelance"],
default: "Full-time",
},
location: {
type: String,
default: "Remote",
},
salary: {
type: Number,
},
skillsRequired: {
type: [String], // Example: ["JavaScript", "React", "MongoDB"]
default: [],
},
business: {
type: mongoose.Schema.Types.ObjectId,
ref: "Business",
required: true,
},
open: {
type: Boolean,
default: true,
},
},
{
timestamps: true,
}
);
export const JobModel = mongoose.model("Job", jobSchema);
====== File : server/models/product.model.ts ===========
// server/models/product.model.ts
import mongoose from "mongoose";
const productSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
description: {
type: String,
default: "",
},
price: {
type: Number,
required: true,
},
discountedPrice: {
type: Number,
},
stock: {
type: Number,
default: 0,
},
images: {
type: [String], // Array of image URLs or filenames
default: [],
},
category: {
type: String,
required: true,
},
business: {
type: mongoose.Schema.Types.ObjectId,
ref: "Business",
required: true,
},
available: {
type: Boolean,
default: true,
},
},
{
timestamps: true,
}
);
export const ProductModel = mongoose.model("Product", productSchema);
========= File : server/models/review.model.ts =======
// server/models/review.model.ts
import mongoose from "mongoose";
const reviewSchema = new mongoose.Schema(
{
reviewerName: {
type: String,
required: true,
},
reviewerId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User", // Optional if you add a user model
},
rating: {
type: Number,
required: true,
min: 1,
max: 5,
},
comment: {
type: String,
},
business: {
type: mongoose.Schema.Types.ObjectId,
ref: "Business",
},
product: {
type: mongoose.Schema.Types.ObjectId,
ref: "Product",
},
job: {
type: mongoose.Schema.Types.ObjectId,
ref: "Job",
},
rideRequest: {
type: mongoose.Schema.Types.ObjectId,
ref: "RideRequest",
},
photos: [
{
type: String, // URLs or file paths
},
],
},
{
timestamps: true,
}
);
export const ReviewModel = mongoose.model("Review", reviewSchema);
====== File : server/models/rideRequest.model.ts ========
// server/models/rideRequest.model.ts
import mongoose from "mongoose";
const rideRequestSchema = new mongoose.Schema(
{
pickupLocation: {
type: String,
required: true,
},
dropLocation: {
type: String,
required: true,
},
rideType: {
type: String,
enum: ["Bike", "Auto", "Car", "Delivery"],
default: "Bike",
},
business: {
type: mongoose.Schema.Types.ObjectId,
ref: "Business",
required: true,
},
customerName: {
type: String,
},
customerPhone: {
type: String,
},
status: {
type: String,
enum: ["Pending", "Accepted", "Completed", "Cancelled"],
default: "Pending",
},
fareEstimate: {
type: Number,
},
distanceKm: {
type: Number,
},
durationMinutes: {
type: Number,
},
},
{
timestamps: true,
}
);
export const RideRequestModel = mongoose.model("RideRequest", rideRequestSchema);
==== File : server/routes/auth/emailOtp.ts ======
import express from "express";
import nodemailer from "nodemailer";
import { Redis } from "@upstash/redis";
const router = express.Router();
const redis = new Redis({ /* config */ });
const transporter = nodemailer.createTransport({ /* SMTP config */ });
router.post("/send-email-otp", async (req, res) => {
const { email } = req.body;
if (!email) return res.status(400).json({ message: "Email is required" });
const emailOtp = Math.floor(100000 + Math.random() * 900000).toString();
await redis.set(`otp:email:${email}`, emailOtp, { ex: 300 });
// Actually send email
await transporter.sendMail({
to: email,
subject: "Your OTP",
text: `Your OTP is ${emailOtp}`,
});
res.json({ message: "Email OTP sent" });
});
export default router;
===== Files : server/routes/auth/mobileOtp.ts =======
import express from "express";
import redis from "../config/redis";
import { generateOTP } from "../utils/otp.util";
const router = express.Router();
function normalizeMobile(mobile: string) {
let cleaned = mobile.replace(/\D/g, "");
if (cleaned.length === 10) cleaned = "91" + cleaned;
if (!cleaned.startsWith("91")) cleaned = "91" + cleaned.slice(-10);
return "+" + cleaned;
}
router.post("/send-mobile-otp", async (req, res) => {
try {
const { mobile } = req.body;
if (!mobile) return res.status(400).json({ message: "Mobile number is required" });
const normalizedMobile = normalizeMobile(mobile);
const mobileOtp = generateOTP();
await redis.set(`otp:mobile:${normalizedMobile}`, mobileOtp, { ex: 300 });
// TODO: Send SMS with your SMS gateway here
console.log(`[SEND MOBILE OTP] Mobile: ${normalizedMobile} | OTP: ${mobileOtp}`);
res.json({
message: "Mobile OTP saved (and sent if SMS configured)",
mobile: normalizedMobile,
mobileOTP: mobileOtp // Useful for testing/dev, remove in production!
});
} catch (error) {
console.error(error);
res.status(500).json({ message: "Internal server error" });
}
});
export default router;
====== File : server/routes/auth/otp.routes.ts =======
import express from "express";
import {
sendEmailOTP,
verifyEmailOTPController,
} from "../../controllers/auth/otp.controller";
const router = express.Router();
/**
* @swagger
* /api/auth/otp/send-email:
* post:
* summary: Send OTP to email address
* tags: [OTP]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* required:
* - email
* responses:
* 200:
* description: Email OTP sent
* 400:
* description: Email required
*/
router.post("/send-email", sendEmailOTP);
/**
* @swagger
* /api/auth/otp/verify-email:
* post:
* summary: Verify email OTP
* tags: [OTP]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* emailOTP:
* type: string
* required:
* - email
* - emailOTP
* responses:
* 200:
* description: OTP verified
* 401:
* description: OTP invalid or expired
*/
router.post("/verify-email", verifyEmailOTPController);
export default router;
===== Files : server/routes/auth.routes.ts ======
import express from "express";
import {
registerInitiate,
registerVerify,
loginWithPassword,
loginWithMobileOTP,
} from "../controllers/authController"; // <-- Import main auth controller
import {
sendEmailOTP,
verifyEmailOTPController,
sendMobileOTPController,
verifyMobileOTPController,
} from "../controllers/auth/otp.controller"; // <-- Import OTP controller
const router = express.Router();
// -------------------- REDIRECTS --------------------
router.get("/login", (req, res) => {
res.redirect("/login");
});
// -------------------- REGISTRATION --------------------
router.post("/register/initiate", registerInitiate);
router.post("/register/verify", registerVerify);
// -------------------- LOGIN --------------------
router.post("/login/password", loginWithPassword);
router.post("/login/mobile-otp", loginWithMobileOTP);
// -------------------- EMAIL OTP --------------------
router.post("/send-email-otp", sendEmailOTP);
router.post("/verify-email-otp", verifyEmailOTPController);
// -------------------- MOBILE OTP --------------------
router.post("/send-mobile-otp", sendMobileOTPController);
router.post("/verify-mobile-otp", verifyMobileOTPController);
export default router;
===== File : server/routes/otp.ts ========
import express from "express";
import {
sendEmailOTP,
verifyEmailOTPController,
sendMobileOTPController,
verifyMobileOTPController,
} from "../controllers/auth/otp.controller";
// Mobile normalization helper (always returns +91xxxxxxxxxx for India)
function normalizeMobile(mobile: string) {
let cleaned = mobile.replace(/\D/g, "");
if (cleaned.length === 10) cleaned = "91" + cleaned;
if (!cleaned.startsWith("91")) cleaned = "91" + cleaned.slice(-10);
return "+" + cleaned;
}
const router = express.Router();
/**
* @route POST /api/auth/otp/send
* @desc Send OTP to mobile or email
* @body { target: string, type: "mobile" | "email", purpose: string }
*/
router.post("/send", async (req, res) => {
const { type, target } = req.body;
if (type === "email") {
req.body.email = target || req.body.email;
return sendEmailOTP(req, res);
} else if (type === "mobile") {
req.body.mobile = normalizeMobile(target || req.body.mobile);
return sendMobileOTPController(req, res);
} else {
return res.status(400).json({ message: "Invalid OTP type" });
}
});
/**
* @route POST /api/auth/otp/verify
* @desc Verify OTP sent to mobile or email
* @body { target: string, code: string, type: "mobile" | "email", purpose: string }
*/
router.post("/verify", async (req, res) => {
const { type, target, code } = req.body;
if (type === "email") {
req.body.email = target || req.body.email;
req.body.emailOTP = code || req.body.emailOTP;
return verifyEmailOTPController(req, res);
} else if (type === "mobile") {
req.body.mobile = normalizeMobile(target || req.body.mobile);
req.body.mobileOTP = code || req.body.mobileOTP; // Fix: always use 'mobileOTP' for
backend consistency
return verifyMobileOTPController(req, res);
} else {
return res.status(400).json({ message: "Invalid OTP type" });
}
});
export default router;
====== File : server/services/auth.service.ts ========
// server/services/auth.service.ts
import User from "../models/user.model";
import { hashPassword, comparePassword } from "../utils/hash.util";
import { generateOTP } from "../utils/otp.util";
import { sendEmail } from "../utils/email";
import { createToken } from "../utils/jwt.util";
import redis from "../config/redis";
// Mobile normalization helper (always returns +91xxxxxxxxxx for India)
function normalizeMobile(mobile: string) {
let cleaned = mobile.replace(/\D/g, "");
if (cleaned.length === 10) cleaned = "91" + cleaned;
if (!cleaned.startsWith("91")) cleaned = "91" + cleaned.slice(-10);
return "+" + cleaned;
}
// -------------------- REGISTRATION --------------------
// Step 1: Initiate registration
export const registerInitiate = async (
name: string,
email: string,
mobile: string,
password: string
) => {
const normalizedMobile = normalizeMobile(mobile);
const existingUser = await User.findOne({
$or: [{ email }, { mobile: normalizedMobile }],
});
if (existingUser) {
throw new Error("User already exists with this email or mobile.");
}
// Generate a temp user ID
const tempUserId = `temp_${Date.now()}_${Math.floor(Math.random() * 1000000)}`;
// Hash the password before storing in Redis
const hashedPassword = await hashPassword(password);
// Store pending user data in Redis for 5 minutes
await redis.hmset(`register:user:${tempUserId}`, {
name,
email,
mobile: normalizedMobile,
password: hashedPassword,
});
await redis.expire(`register:user:${tempUserId}`, 300); // 5 minutes
// Generate & store OTP for email
✅
const emailOtp = generateOTP();
await redis.set(`otp:email:${email}`, emailOtp, { ex: 300 }); // Upstash syntax
await sendEmail(email, "Email OTP", `Your OTP is ${emailOtp}`);
// Generate & store OTP for mobile
✅ Upstash syntax
const mobileOtp = generateOTP();
await redis.set(`otp:mobile:${normalizedMobile}`, mobileOtp, { ex: 300 }); //
// TODO: Send mobileOtp via SMS provider here
return { userId: tempUserId, email, mobile: normalizedMobile };
};
// Step 2: Verify OTPs and create actual user
export const registerVerify = async (
tempUserId: string,
emailOtp: string,
mobileOtp: string
) => {
const pendingUser = await redis.hgetall(`register:user:${tempUserId}`);
if (!pendingUser || !pendingUser.email || !pendingUser.mobile) {
throw new Error("Invalid or expired registration session.");
}
// Check email OTP
const storedEmailOtp = await redis.get(`otp:email:${pendingUser.email}`);
if (!storedEmailOtp || storedEmailOtp !== emailOtp) {
throw new Error("Invalid or expired email OTP.");
}
// Check mobile OTP
const storedMobileOtp = await redis.get(`otp:mobile:${pendingUser.mobile}`);
if (!storedMobileOtp || storedMobileOtp !== mobileOtp) {
throw new Error("Invalid or expired mobile OTP.");
}
// Create actual user in DB
const user = await User.create({
name: pendingUser.name,
email: pendingUser.email,
mobile: pendingUser.mobile,
password: pendingUser.password,
emailVerified: true,
phoneVerified: true,
});
// Cleanup Redis
await redis.del(`register:user:${tempUserId}`);
await redis.del(`otp:email:${pendingUser.email}`);
await redis.del(`otp:mobile:${pendingUser.mobile}`);
const token = createToken({ uid: user._id, email: user.email });
return { token, user };
};
// -------------------- LOGIN --------------------
// Login with email & password
export const loginWithPassword = async (email: string, password: string) => {
const user = await User.findOne({ email });
if (!user) throw new Error("User not found.");
const isMatch = await comparePassword(password, user.password);
if (!isMatch) throw new Error("Invalid password.");
const token = createToken({ uid: user._id, email: user.email });
return { token, user };
};
// Login with mobile OTP
export const loginWithMobileOTP = async (mobile: string, otp: string) => {
const normalizedMobile = normalizeMobile(mobile);
const user = await User.findOne({ mobile: normalizedMobile });
if (!user) throw new Error("User not found.");
const storedOtp = await redis.get(`otp:mobile:${normalizedMobile}`);
if (!storedOtp || storedOtp !== otp) {
throw new Error("Invalid or expired OTP.");
}
// OTP matched, delete it now
await redis.del(`otp:mobile:${normalizedMobile}`);
const token = createToken({ uid: user._id, email: user.email });
return { token, user };
};
// -------------------- OTP SENDER --------------------
export const sendOtpService = async (
type: "email" | "mobile",
receiver: string
) => {
if (type === "email") {
✅
const otp = generateOTP();
await redis.set(`otp:email:${receiver}`, otp, { ex: 300 }); // Upstash syntax
await sendEmail(receiver, "Email OTP", `Your OTP is ${otp}`);
return { message: "Email OTP sent" };
} else if (type === "mobile") {
const normalizedReceiver = normalizeMobile(receiver);
✅
const otp = generateOTP();
await redis.set(`otp:mobile:${normalizedReceiver}`, otp, { ex: 300 }); // Upstash syntax
// TODO: send SMS here
return { message: "Mobile OTP saved and ready to send" };
} else {
throw new Error("Invalid OTP type");
}
};
======= File : server/utils/email.ts ========
import nodemailer from "nodemailer";
/**
* Configure the transporter with Gmail SMTP or your preferred service.
* Use env vars: EMAIL_USER, EMAIL_PASS
*/
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
/**
* Send an OTP email to the specified recipient.
*/
export async function sendEmail(email: string, subject: string, body: string): Promise<void> {
const mailOptions = {
from: process.env.EMAIL_USER,
to: email,
subject,
text: body,
};
await transporter.sendMail(mailOptions);
}
====== File : server/utils/hash.util.ts ======
import bcrypt from "bcryptjs";
/**
* Hash a password using bcrypt.
* @param password - The plain text password
* @returns The hashed password string
*/
export const hashPassword = async (password: string): Promise<string> => {
const saltRounds = 10;
const salt = await bcrypt.genSalt(saltRounds);
return bcrypt.hash(password, salt);
};
/**
* Compare a plain password with a hashed password.
* @param password - The plain text password
* @param hashedPassword - The hashed password from the DB
* @returns True if match, false otherwise
*/
export const comparePassword = async (
password: string,
hashedPassword: string
): Promise<boolean> => {
return bcrypt.compare(password, hashedPassword);
};
======== File : server/util/jwt.util.ts ======
// server/utils/jwt.util.ts
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET || "default_secret";
const JWT_EXPIRES_IN = "7d";
/**
* Alias generateToken so existing code using generateToken still works
*/
export function generateToken(payload: object): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
}
/**
* Keep your original createToken name for flexibility
*/
export function createToken(payload: object): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
}
export function verifyToken(token: string): any {
try {
return jwt.verify(token, JWT_SECRET);
} catch (err) {
return null;
}
}
===== File : server/utils/otp.util.ts ======
import redis from "../config/redis";
const OTP_EXPIRATION_SECONDS = 300; // 5 minutes
// Normalize email for consistent keying
function normalizeEmail(email: string): string {
return email.trim().toLowerCase();
}
// Normalize mobile for consistent keying (+91xxxxxxxxxx)
function normalizeMobile(mobile: string): string {
let cleaned = mobile.replace(/\D/g, "");
if (cleaned.length === 10) cleaned = "91" + cleaned;
if (!cleaned.startsWith("91")) cleaned = "91" + cleaned.slice(-10);
return "+" + cleaned;
}
// Generate random 6-digit OTP
export function generateOTP(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// ------------------- EMAIL OTP -------------------
// Store OTP in Redis for email
export async function setEmailOTP(email: string, otp: string): Promise<void> {
const key = `otp:email:${normalizeEmail(email)}`;
await redis.set(key, otp, { ex: OTP_EXPIRATION_SECONDS });
}
// Retrieve OTP from Redis for email
export async function getEmailOTP(email: string): Promise<string | null> {
const key = `otp:email:${normalizeEmail(email)}`;
return await redis.get(key);
}
// Verify OTP for email (DO NOT delete OTP here)
export async function verifyEmailOTP(email: string, inputOtp: string | number):
Promise<boolean> {
const storedOtp = await getEmailOTP(email);
console.log(`[OTP VERIFY] Email: ${email} | Input OTP: ${inputOtp} | Stored OTP:
${storedOtp}`);
return String(storedOtp || "").trim() === String(inputOtp || "").trim();
}
// ------------------- MOBILE OTP -------------------
// Store OTP in Redis for mobile
export async function setMobileOTP(mobile: string, otp: string): Promise<void> {
const key = `otp:mobile:${normalizeMobile(mobile)}`;
await redis.set(key, otp, { ex: OTP_EXPIRATION_SECONDS });
}
// Retrieve OTP from Redis for mobile
export async function getMobileOTP(mobile: string): Promise<string | null> {
const key = `otp:mobile:${normalizeMobile(mobile)}`;
return await redis.get(key);
}
// Verify OTP for mobile (DO NOT delete OTP here)
export async function verifyMobileOTP(mobile: string, inputOtp: string | number):
Promise<boolean> {
const storedOtp = await getMobileOTP(mobile);
console.log(`[OTP VERIFY] Mobile: ${mobile} | Input OTP: ${inputOtp} | Stored OTP:
${storedOtp}`);
return String(storedOtp || "").trim() === String(inputOtp || "").trim();
}
// ------------------- ALIAS EXPORTS -------------------
// For backward compatibility
export const setOTP = setEmailOTP;
export const getOTP = getEmailOTP;
export const verifyOTP = verifyEmailOTP;
// Note: deleteOTP alias is NOT exported as per new plan to delete OTP only after user creation
success
====== File : server/utils/redis.ts =======
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
export default redis;
======= File : server/utils/sms.util.ts ========
// server/utils/sms.util.ts
export const sendSMS = async (mobile: string, message: string): Promise<boolean> => {
📱
try {
console.log(` Sending SMS to ${mobile}: ${message}`);
// TODO: Integrate real SMS API here (e.g., Twilio or Fast2SMS)
return true;
❌
} catch (error) {
console.error(" SMS sending failed:", error);
return false;
}
};