first commit

This commit is contained in:
Diwakar Mehta 2025-06-18 18:19:34 +05:30
commit 5530c9d336
19 changed files with 2941 additions and 0 deletions

22
.env.example Normal file
View file

@ -0,0 +1,22 @@
PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
# twilio creds
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_PHONE_NUMBER=
JWT_SECRET=
REFRESH_TOKEN_EXPIRY=
ACCESS_TOKEN_EXPIRY=
# supabase creds
SUPABASE_URL=
SUPABASE_KEY=
DATABASE_URL=

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.env
node_modules

2223
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "smasher",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/server.js",
"dev": "nodemon src/server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"bcrypt": "^6.0.0",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"pg": "^8.16.0",
"pg-hstore": "^2.3.4",
"postgres": "^3.4.7",
"sequelize": "^6.37.7",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"twilio": "^5.7.1"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}

16
src/config/database.js Normal file
View file

@ -0,0 +1,16 @@
import { Sequelize } from 'sequelize';
import { config } from 'dotenv';
config();
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
dialect: 'postgres',
logging: false,
}
);
export default sequelize;

View file

@ -0,0 +1,214 @@
import User from "../models/user.model.js";
import { config } from "dotenv";
import jwt from "jsonwebtoken";
import twilio from "twilio";
config();
const JWT_SECRET = process.env.JWT_SECRET;
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const client = twilio(accountSid, authToken);
// Utility: Send OTP using Twilio
async function sendOtpFromTwilio(to, otp) {
try {
const message = await client.messages.create({
body: `Your one-time password is ${otp}`,
from: process.env.TWILIO_PHONE_NUMBER, // example: "+18563867972"
to: "+918847080824",
});
return message;
} catch (err) {
console.error("Twilio send error:", err);
return err.message;
}
}
// Register or login with OTP
const registerLogin = async (req, res) => {
try {
const { countryCode, phoneNumber } = req.body;
const full_number = countryCode.toString() + phoneNumber;
const otp = Math.floor(100000 + Math.random() * 900000).toString();
const otp_expiry = new Date(Date.now() + 10 * 60 * 1000); // 10 mins
const twilioResponse = await sendOtpFromTwilio(full_number, otp);
console.log("twilioResponse:", twilioResponse.sid || twilioResponse);
let user = await User.findOne({ where: { full_number } });
if (!user) {
user = await User.create({
country_code: countryCode,
phone_number: phoneNumber,
full_number: full_number,
otp,
otp_expiry,
});
} else {
await User.update({ otp, otp_expiry }, { where: { full_number } });
user = await User.findOne({ where: { full_number } });
}
res.status(201).json({
message: "OTP sent successfully",
user: { id: user.id, full_number },
});
} catch (err) {
console.error("registerLogin error:", err);
res.status(500).json({ error: err.message });
}
};
// Verify OTP
const verifyOtp = async (req, res) => {
try {
const { countryCode, phoneNumber, otp } = req.body;
const full_number = countryCode.toString() + phoneNumber;
if (!phoneNumber || !otp) {
return res
.status(400)
.json({ error: "Phone number and OTP are required" });
}
const user = await User.findOne({ where: { full_number } });
if (!user) {
return res.status(404).json({ error: "User not found" });
}
if (user.otp !== otp || new Date() > new Date(user.otp_expiry)) {
return res.status(400).json({ error: "Invalid or expired OTP" });
}
user.otp = null;
user.otp_expiry = null;
await user.save();
const refreshToken = jwt.sign({ id: user.id }, JWT_SECRET, {
expiresIn: process.env.REFRESH_TOKEN_EXPIRY,
});
const accessToken = jwt.sign({ id: user.id }, JWT_SECRET, {
expiresIn: process.env.ACCESS_TOKEN_EXPIRY,
});
res.json({
message: "OTP verified successfully",
refreshToken,
accessToken,
user: {
id: user.id,
name: user.name,
country_code: user?.country_code,
phone_number: user?.phone_number,
full_number: user?.full_number,
},
});
} catch (err) {
res.status(500).json({ error: err.message });
}
};
// const login = async (req, res) => {
// try {
// const { phoneNumber } = req.body;
// const user = await User.findOne({ where: { phone_number: } });
// if (!user) {
// return res.status(400).json({ error: "Invalid phone number" });
// }
// const token = sign({ id: user.id }, JWT_SECRET, { expiresIn: "1d" });
// res.json({
// token,
// user: { id: user.id, name: user.name, phone_number },
// });
// } catch (err) {
// res.status(500).json({ error: err.message });
// }
// };
const triggerOtp = async (req, res) => {
try {
const { countryCode, phoneNumber } = req.body;
const full_number = countryCode.toString() + phoneNumber;
const exist = await User.findOne({ where: { full_number: full_number } });
if (!exist) return res.status(400).json({ message: "Number Not exist" });
const otp = Math.floor(100000 + Math.random() * 900000).toString();
const otp_expiry = new Date(Date.now() + 10 * 60 * 1000); // 10 mins
const twilioResponse = await sendOtpFromTwilio(full_number, otp);
console.log("twilioResponse:", twilioResponse.sid || twilioResponse);
await User.update(
{
otp: otp,
otp_expiry: otp_expiry,
},
{
where: {
full_number: full_number,
},
}
);
res.status(200).json({ message: "Otp sent seccessfully" });
} catch (err) {
res.status(500).json({ error: err.message });
}
};
const generateAccessToken = (user) => {
try {
return jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: process.env.ACCESS_TOKEN_EXPIRY });
} catch (error) {
return res.status(400).json({ message: 'Invalid or expired token' });
}
};
const generateRefreshToken = (user) => {
try {
return jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: process.env.REFRESH_TOKEN_EXPIRY });
} catch (err) {
return res.status(400).json({ message: 'Invalid or expired refresh token' });
}
};
const refreshAccessToken = async (req, res) => {
const refreshToken = req.body.refreshToken;
if (!refreshToken) {
return res
.status(400)
.json({ message: "Refresh token not provided" });
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
const user = await User.findByPk(decoded.id);
if (!user)
return res
.status(404)
.json({ message: "User not found" });
const newAccessToken = generateAccessToken({
id: decoded.id
});
return res.status(200).json({ accessToken: newAccessToken });
} catch (err) {
console.error("Refresh token error:", err.message);
return res
.status(400)
.json({ message: "Invalid or expired refresh token" });
}
};
export default {
registerLogin,
verifyOtp,
// login,
triggerOtp,
refreshAccessToken,
generateRefreshToken
};

View file

@ -0,0 +1,52 @@
import User from '../models/user.model.js';
// UPDATE USER
const updateUser = async (req, res) => {
try {
const { name } = req.body;
const { id } = req.user;
const user = await User.findByPk({ id });
if(!user) return res.status(401).json({message:"User not found"})
await User.update({
name
},{
where:{
id:id
}
})
res.status(20).json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
};
// READ ALL
const getAllUsers = async (req, res) => {
const users = await User.findAll();
res.json(users);
};
// READ ONE
const getUserById = async (req, res) => {
const user = await User.findByPk(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
};
// DELETE
const deleteUser = async (req, res) => {
const user = await User.findByPk(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
await user.destroy();
res.json({ message: 'User deleted' });
};
export default {
updateUser,
getAllUsers,
getUserById,
updateUser,
deleteUser,
};

0
src/helpers/responses.js Normal file
View file

View file

@ -0,0 +1,20 @@
// middlewares/auth.middleware.js
import jwt from 'jsonwebtoken';
const verifyToken = (req, res, next) => {
const token = req.headers.token;
if (!token) {
return res.status(401).json({ message: 'Access denied. No token provided.' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET); // Ensure this env var exists
req.user = decoded; // Attach user payload to request
next();
} catch (err) {
return res.status(400).json({ message: 'Invalid token.' });
}
};
export default verifyToken;

View file

@ -0,0 +1,11 @@
const validate = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
next();
};
};
export default validate;

14
src/models/index.js Normal file
View file

@ -0,0 +1,14 @@
import sequelize from '../config/database.js';
import User from './user.model.js';
const connectDB = async () => {
try {
await sequelize.authenticate();
console.log('Database connected.');
await sequelize.sync(); // use { force: true } for dropping and recreating tables
} catch (error) {
console.error('Unable to connect to the database:', error);
}
};
export { sequelize, connectDB, User };

31
src/models/user.model.js Normal file
View file

@ -0,0 +1,31 @@
import { DataTypes } from "sequelize";
import sequelize from "../config/database.js";
const User = sequelize.define("users", {
name: {
type: DataTypes.STRING,
allowNull: true,
},
country_code: {
type: DataTypes.STRING,
allowNull: false,
},
phone_number: {
type: DataTypes.STRING,
allowNull: false,
},
full_number: {
type: DataTypes.STRING,
allowNull: false,
},
otp: {
type: DataTypes.STRING,
allowNull: true,
},
otp_expiry: {
type: DataTypes.DATE,
allowNull: true,
},
});
export default User;

178
src/routes/auth.routes.js Normal file
View file

@ -0,0 +1,178 @@
import { Router } from "express";
const router = Router();
import authController from "../controllers/auth.controller.js";
import validate from "../middleware/validate.js";
import {
registerSchema,
otpSchema,
triggerOtpSchema,
refreshTokenSchema,
} from "../validators/auth.validator.js";
/**
* @swagger
* /api/auth/register-login:
* post:
* tags:
* - Auth
* summary: Register or login user via phone number with OTP
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - countryCode
* - phoneNumber
* properties:
* countryCode:
* type: string
* example: "+91"
* phoneNumber:
* type: string
* example: "9876543210"
* responses:
* 201:
* description: OTP sent successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: OTP sent successfully
* user:
* type: object
* properties:
* id:
* type: integer
* example: 1
* full_number:
* type: string
* example: "+919876543210"
*/
router.post(
"/register-login",
validate(registerSchema),
authController.registerLogin
);
/**
* @swagger
* /api/auth/trigger-otp:
* post:
* tags:
* - Auth
* summary: Trigger an OTP for a given phone number
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - countryCode
* - phoneNumber
* properties:
* countryCode:
* type: string
* example: "+91"
* phoneNumber:
* type: string
* example: "9876543210"
* responses:
* 200:
* description: OTP sent successfully
* 400:
* description: Invalid input or user already exists
*/
router.post(
"/trigger-otp",
validate(triggerOtpSchema),
authController.triggerOtp
);
/**
* @swagger
* /api/auth/verify-otp:
* post:
* tags:
* - Auth
* summary: Verify the OTP sent to a phone number
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - countryCode
* - phoneNumber
* - otp
* properties:
* countryCode:
* type: string
* example: "+91"
* phoneNumber:
* type: string
* example: "9876543210"
* otp:
* type: string
* example: "123456"
* responses:
* 200:
* description: OTP verified and user authenticated
* 400:
* description: Invalid or expired OTP
* 404:
* description: User not found
*/
router.post("/verify-otp", validate(otpSchema), authController.verifyOtp);
/**
* @swagger
* /api/auth/refresh-token:
* post:
* tags:
* - Auth
* summary: Refresh access token
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - refreshToken
* properties:
* refreshToken:
* type: string
* example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
* responses:
* 200:
* description: Access token refreshed successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* accessToken:
* type: string
* example: accessToken
* 400:
* description: Invalid refresh token or schema validation failed
*/
router.post(
"/refresh-token",
validate(refreshTokenSchema),
authController.refreshAccessToken
);
export default router;

12
src/routes/index.js Normal file
View file

@ -0,0 +1,12 @@
// routes/combinedRoutes.js
import { Router } from 'express';
import authRoutes from './auth.routes.js';
import userRoutes from './user.routes.js';
const router = Router();
// Mount individual routes
router.use('/auth', authRoutes);
router.use('/users', userRoutes);
export default router;

View file

@ -0,0 +1,5 @@
import { Router } from 'express';
const router = Router();
export default router;

20
src/server.js Normal file
View file

@ -0,0 +1,20 @@
import config from 'dotenv'
// config()
import express, { urlencoded, json } from "express";
import { connectDB } from "./models/index.js";
import indexRoutes from "./routes/index.js";
import { serve, setup } from "swagger-ui-express";
import swaggerSpec from "./swaggerUI/swagger.js";
const app = express();
app.use(urlencoded());
app.use(json());
app.use("/api-docs", serve, setup(swaggerSpec));
app.use("/api", indexRoutes);
const PORT = process.env.PORT || 3001;
connectDB().then(() => {
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
});

19
src/swaggerUI/swagger.js Normal file
View file

@ -0,0 +1,19 @@
import swaggerJSDoc from "swagger-jsdoc";
const swaggerDefinition = {
openapi: "3.0.0",
info: {
title: "My API",
version: "1.0.0",
description: "My API Description",
},
};
const options = {
swaggerDefinition,
apis: ["./src/routes/*.js"], // Path to the API routes in your Node.js application
};
const swaggerSpec = swaggerJSDoc(options);
export default swaggerSpec;

View file

@ -0,0 +1,48 @@
import joi from "joi";
import { connectDB } from "../models/index.js";
import { User } from "../models/index.js";
const registerSchema = joi.object({
countryCode: joi.string().required(),
phoneNumber: joi
.string()
.pattern(/^[0-9+\-\s()]+$/)
.required(),
});
const loginSchema = joi.object({
countryCode: joi.string().required(),
phoneNumber: joi
.string()
.pattern(/^[0-9+\-\s()]+$/)
.required(),
});
const otpSchema = joi.object({
countryCode: joi.string().required(),
phoneNumber: joi
.string()
.pattern(/^[0-9+\-\s()]+$/)
.required(),
otp: joi.string().min(6).required(),
});
const triggerOtpSchema = joi.object({
countryCode: joi.string().required(),
phoneNumber: joi
.string()
.pattern(/^[0-9+\-\s()]+$/)
.required(),
});
const refreshTokenSchema = joi.object({
refreshToken: joi.string().required()
});
export {
registerSchema,
loginSchema,
otpSchema,
triggerOtpSchema,
refreshTokenSchema,
};

View file

@ -0,0 +1,22 @@
import joi from 'joi';
const createUserSchema = joi.object({
name: joi.string().min(1).required(),
phone_number: joi.string()
.pattern(/^[0-9+\-\s()]+$/)
.required()
.messages({ 'string.pattern.base': 'Invalid phone number format' }),
});
const updateUserSchema = joi.object({
name: joi.string().min(1).optional(),
phone_number: joi.string()
.pattern(/^[0-9+\-\s()]+$/)
.optional()
.messages({ 'string.pattern.base': 'Invalid phone number format' }),
});
export default {
createUserSchema,
updateUserSchema,
};