Compare commits

..

1 commit

Author SHA1 Message Date
Chetan Bedi 89ff6d27d4 aws s3 upload and get apis 2025-06-19 16:41:45 +05:30
15 changed files with 2094 additions and 229 deletions

View file

@ -4,7 +4,6 @@ DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
NODE_ENV=
# twilio creds
@ -20,4 +19,11 @@ ACCESS_TOKEN_EXPIRY=
SUPABASE_URL=
SUPABASE_KEY=
DATABASE_URL=
DATABASE_URL=
# aws s3 creds
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
AWS_S3_BUCKET=

127
.gitignore vendored
View file

@ -1,127 +1,2 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
node_modules

1933
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,18 +13,22 @@
"license": "ISC",
"description": "",
"dependencies": {
"@aws-sdk/client-s3": "^3.832.0",
"@aws-sdk/s3-request-presigner": "^3.832.0",
"bcrypt": "^6.0.0",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"multer": "^2.0.1",
"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"
"twilio": "^5.7.1",
"uuid": "^11.1.0"
},
"devDependencies": {
"nodemon": "^3.1.10"

View file

@ -1,7 +1,6 @@
import dotenv from "dotenv";
dotenv.config()
import path from 'path';
dotenv.config({ path: path.join(process.cwd(), "../.env") })
import path from "path";
dotenv.config({ path: path.join(process.cwd(), "../.env") });
const getDb = () => {
return {

View file

@ -1,10 +1,8 @@
import dotenv from 'dotenv'
dotenv.config()
import db from "../models/index.js";
const { users } = db;
import dotenv from "dotenv";
dotenv.config();
import User from "../models/user.js";
import jwt from "jsonwebtoken";
import twilio from "twilio";
// console.log('process.env: ', process.env);
const JWT_SECRET = process.env.JWT_SECRET;
const accountSid = process.env.TWILIO_ACCOUNT_SID;
@ -35,21 +33,22 @@ const registerLogin = async (req, res) => {
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);
const twilioResponse = await sendOtpFromTwilio(full_number, otp);
console.log("twilioResponse:", twilioResponse.sid || twilioResponse);
let user = await users.findOne({ where: { country_code:countryCode, phone_number:phoneNumber } });
let user = await User.findOne({ where: { full_number } });
if (!user) {
user = await users.create({
user = await User.create({
country_code: countryCode,
phone_number: phoneNumber,
full_number: full_number,
otp,
otp_expiry
otp_expiry,
});
} else {
await users.update({ otp, otp_expiry }, { country_code:countryCode, phone_number:phoneNumber } );
user = await users.findOne({ country_code:countryCode, phone_number:phoneNumber } );
await User.update({ otp, otp_expiry }, { where: { full_number } });
user = await User.findOne({ where: { full_number } });
}
res.status(201).json({
@ -57,7 +56,7 @@ const registerLogin = async (req, res) => {
user: { id: user.id, full_number },
});
} catch (err) {
console.log("registerLogin error:",err);
console.error("registerLogin error:", err);
res.status(500).json({ error: err.message });
}
};
@ -73,7 +72,7 @@ const verifyOtp = async (req, res) => {
.json({ error: "Phone number and OTP are required" });
}
const user = await users.findOne({ where: { full_number } });
const user = await User.findOne({ where: { full_number } });
if (!user) {
return res.status(404).json({ error: "User not found" });
}
@ -134,14 +133,14 @@ const triggerOtp = async (req, res) => {
const { countryCode, phoneNumber } = req.body;
const full_number = countryCode.toString() + phoneNumber;
const exist = await users.findOne({ where: { country_code:countryCode,phone_number: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 users.update(
await User.update(
{
otp: otp,
otp_expiry: otp_expiry,
@ -159,42 +158,41 @@ const triggerOtp = async (req, res) => {
};
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" });
}
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" });
}
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" });
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" });
if (!user)
return res
.status(404)
.json({ message: "User not found" });
const newAccessToken = generateAccessToken({
id: decoded.id,
id: decoded.id
});
return res.status(200).json({ accessToken: newAccessToken });
@ -212,5 +210,5 @@ export default {
// login,
triggerOtp,
refreshAccessToken,
generateRefreshToken,
generateRefreshToken
};

View file

@ -1,4 +1,5 @@
import User from '../models/users.js';
import User from '../models/user.js';
import { uploadFile, getSignedFileUrl } from "../service/s3.service.js";
// UPDATE USER
const updateUser = async (req, res) => {
@ -47,9 +48,48 @@ const deleteUser = async (req, res) => {
res.json({ message: 'User deleted' });
};
/**
* Upload file to S3 and return fileKey
*/
const uploadS3File = async (req, res) => {
try {
console.log("Received file:", req.file?.originalname);
if (!req.file) {
return res.status(400).json({ error: "No file provided" });
}
const fileKey = await uploadFile(req.file);
res.json({ message: "File uploaded successfully", fileKey });
} catch (err) {
console.error("S3 upload error:", err);
res.status(500).json({ error: err.message });
}
};
/**
* Generate a signed URL for S3 file
*/
const getS3SignedUrl = async (req, res) => {
try {
const { key } = req.params;
if (!key) {
return res.status(400).json({ error: "File key is required" });
}
const url = await getSignedFileUrl(key);
res.json({ url });
} catch (err) {
console.error("S3 signed URL error:", err);
res.status(500).json({ error: err.message });
}
};
export default {
updateUser,
getAllUsers,
getUserById,
deleteUser,
uploadS3File,
getS3SignedUrl
};

View file

@ -1,7 +1,7 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
export async function up(queryInterface, Sequelize) {
await queryInterface.createTable('users', {
await queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
@ -17,6 +17,9 @@ export async function up(queryInterface, Sequelize) {
phone_number: {
type: Sequelize.STRING
},
full_number: {
type: Sequelize.STRING
},
otp: {
type: Sequelize.STRING
},
@ -34,5 +37,5 @@ export async function up(queryInterface, Sequelize) {
});
}
export async function down(queryInterface, Sequelize) {
await queryInterface.dropTable('users');
await queryInterface.dropTable('Users');
}

View file

@ -33,7 +33,6 @@ for (const file of files) {
const model = modelModule.default(sequelize, DataTypes);
db[model.name] = model;
}
console.log(db)
Object.keys(db).forEach((modelName) => {
if (db[modelName].associate) {

32
src/models/user.js Normal file
View file

@ -0,0 +1,32 @@
"use strict";
import { Model } from "sequelize";
export default (sequelize, DataTypes) => {
class User extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
User.init(
{
name: DataTypes.STRING,
country_code: DataTypes.STRING,
phone_number: DataTypes.STRING,
full_number: DataTypes.STRING,
otp: DataTypes.STRING,
otp_expiry: DataTypes.DATE,
},
{
sequelize,
modelName: "User",
underscored: true,
timestamps: true,
}
);
return User;
};

View file

@ -1,25 +0,0 @@
'use strict';
import { Model } from 'sequelize';
export default (sequelize, DataTypes) => {
class users extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
users.init({
name: DataTypes.STRING,
country_code: DataTypes.STRING,
phone_number: DataTypes.STRING,
otp: DataTypes.STRING,
otp_expiry: DataTypes.DATE
}, {
sequelize,
modelName: 'users',
});
return users;
};

View file

@ -4,7 +4,8 @@ import userController from "../controllers/user.controller.js";
import validate from "../middleware/validate.js";
import { updateUserSchema } from "../validators/user.validator.js";
import verifyToken from "../middleware/authenticate.js";
import multer from "multer";
const upload = multer(); // memory storage
router.put(
"/update-user",
@ -13,5 +14,12 @@ router.put(
userController.updateUser
);
router.post("/upload", upload.single("file"), userController.uploadS3File);
router.get(
"/get-upload/:key",
userController.getS3SignedUrl
);
export default router;

View file

@ -1,7 +1,5 @@
import dotenv from 'dotenv'
import path from 'path'
dotenv.config()
// console.log(process.env)
import express, { urlencoded, json } from "express";
// import { connectDB } from "./models/index.js";
import indexRoutes from "./routes/index.js";

40
src/service/s3.service.js Normal file
View file

@ -0,0 +1,40 @@
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { v4 as uuidv4 } from "uuid";
import s3Client from "../utils/s3Client.js";
const bucketName = process.env.AWS_S3_BUCKET;
export const uploadFile = async (file) => {
if (!file || !file.buffer) {
throw new Error("Invalid file data");
}
const fileKey = `${uuidv4()}-${file.originalname}`;
const command = new PutObjectCommand({
Bucket: bucketName,
Key: fileKey,
Body: file.buffer,
ContentType: file.mimetype,
});
await s3Client.send(command);
return fileKey;
};
export const getSignedFileUrl = async (fileKey, expiresIn = 3600) => {
if (!fileKey) {
throw new Error("File key is required");
}
const command = new GetObjectCommand({
Bucket: bucketName,
Key: fileKey,
});
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn });
return signedUrl;
};

11
src/utils/s3Client.js Normal file
View file

@ -0,0 +1,11 @@
import { S3Client } from "@aws-sdk/client-s3";
const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
export default s3Client;