diff --git a/backend/config.js b/backend/config.js new file mode 100644 index 00000000..7c662db7 --- /dev/null +++ b/backend/config.js @@ -0,0 +1,3 @@ +module.exports = { + JWT_SECRETS: "paytm@prince" +} \ No newline at end of file diff --git a/backend/db.js b/backend/db.js new file mode 100644 index 00000000..5869d0ef --- /dev/null +++ b/backend/db.js @@ -0,0 +1,49 @@ +const mongoose = require("mongoose"); + +mongoose.connect("mongodb+srv://prince981620:Hello%4012345@cluster0.twgxb.mongodb.net/paytm"); + +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: true, + unique: true, + trim: true, // trim spaces from begininig and ends + lowercase: true, + minLength: 3, + maxLength: 30 + }, + password: { + type: String, + required: true, + minLength: 6 + }, + firstName: { + type: String, + required: true, + trim: true, + maxLength: 50 + }, + lastName: { + type: String, + required: true, + maxLength: 50 + } +}) +const accountSchema = new mongoose.Schema({ + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + balance: { + type: Number, + required: true + } +}) +const Account = mongoose.model("account",accountSchema); +const User = mongoose.model("User",userSchema); + +module.exports = { + User, + Account +} \ No newline at end of file diff --git a/backend/index.js b/backend/index.js index 825926c4..5986d5a5 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,3 +1,14 @@ const express = require("express"); +const mainRouter = require("./routes/index"); +const cors = require("cors"); +const app = express(); +app.use(cors()); +app.use(express.json()); +// all the req for /api/v1 goes to mainRouter i.e /routes/index.js +app.use("/api/v1",mainRouter); + +app.listen(3000,()=>{ + console.log("listening on port 3000"); +}) diff --git a/backend/middileware.js b/backend/middileware.js new file mode 100644 index 00000000..ed71d2e2 --- /dev/null +++ b/backend/middileware.js @@ -0,0 +1,26 @@ +const jwt = require("jsonwebtoken"); +const { JWT_SECRETS } = require("./config"); + + +const authMiddleware = (req,res,next)=>{ + const authHeader = req.headers.authorization; + if(!authHeader || !authHeader.startsWith('Bearer ')){ + return res.status(411).json({}); + } + const token = authHeader.split(" ")[1]; + + try{ + const decoded = jwt.verify(token,JWT_SECRETS); + if(decoded.userId){ + req.userId = decoded.userId; + next(); + }else{ + return res.status(403).json({}); + } + }catch(err){ + return res.status(403),json({}); + } +} +module.exports = { + authMiddleware +} \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 3ebc1111..ac0f15f1 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "cors": "^2.8.5", "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.1.0", "zod": "^3.22.4" } @@ -83,6 +85,11 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -136,6 +143,18 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -174,6 +193,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -390,6 +417,51 @@ "node": ">= 0.10" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", @@ -398,6 +470,41 @@ "node": ">=12.0.0" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -587,6 +694,14 @@ "node": ">= 0.6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -699,6 +814,17 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", diff --git a/backend/package.json b/backend/package.json index 73233240..16cc4390 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,7 +10,9 @@ "author": "", "license": "ISC", "dependencies": { + "cors": "^2.8.5", "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.1.0", "zod": "^3.22.4" } diff --git a/backend/routes/account.js b/backend/routes/account.js new file mode 100644 index 00000000..e9a24b44 --- /dev/null +++ b/backend/routes/account.js @@ -0,0 +1,56 @@ +const express = require("express"); +const { authMiddleware } = require("../middileware"); +const { Account } = require("../db"); +const { default: mongoose } = require("mongoose"); + +const router = express.Router(); + +router.get("/balance",authMiddleware, async (req,res)=>{ + const account = await Account.findOne({ + userId: req.userId + }) + if(account){ + res.status(200).json({ + balance: account.balance + }) + } +}) + +router.post("/transfer",authMiddleware,async (req,res)=>{ + const session = await mongoose.startSession(); + + session.startTransaction(); + const { amount, to } = req.body; + // fetch the account details initiating trtansaction + const account = await Account.findOne({userId: req.userId}).session(session); + + if(!account || account.balance < amount){ + await session.abortTransaction(); + return res.status(400).json({ + msg: "Insufficient balance" + }) + } + // fetch the account details of the user receivnig money + + const toAccount = await Account.findOne({userId: to}).session(session); + + if(!toAccount){ + await session.abortTransaction(); + return res.status(400).json({ + message: "Invalid Account" + }) + } + + // perform the transfer + await Account.updateOne({userId: req.userId},{$inc: {balance: -amount}}).session(session); + await Account.updateOne({userId: to},{$inc: {balance: amount}}).session(session); + + // now commit the transaction + await session.commitTransaction(); + res.json({ + msg: "transfer Successfull" + }) + +}) + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/index.js b/backend/routes/index.js new file mode 100644 index 00000000..00f629e8 --- /dev/null +++ b/backend/routes/index.js @@ -0,0 +1,12 @@ +const express = require("express"); +const userRouter = require("./user"); +const accountRouter = require("./account"); +const router = express.Router(); +// all the req for /api/v1 comes to this router i.e /routes/index.js + +// and from here all the req goes to /api/v1/user goes to +router.use("/user",userRouter); +// and from here all the req goes to /api/v1/account goes to +router.use("/account",accountRouter); + +module.exports = router; diff --git a/backend/routes/user.js b/backend/routes/user.js new file mode 100644 index 00000000..20a447eb --- /dev/null +++ b/backend/routes/user.js @@ -0,0 +1,128 @@ +const express = require("express"); +const jwt = require("jsonwebtoken"); +const zod = require("zod"); +const { User, Account } = require("../db"); +const { JWT_SECRETS } = require("../config"); +const { authMiddleware } = require("../middileware"); +const router = express.Router(); + +const signupBody = zod.object({ + username: zod.string().email(), + firstName: zod.string(), + lastName: zod.string(), + password: zod.string() +}) +router.post("/signup",async (req,res)=>{ + const userPayload = req.body; + const parcedPayload = signupBody.safeParse(userPayload); + if(!parcedPayload.success){ + return res.status(411).json({ + msg: "Email already taken/ Incorrect Inputs" + }) + } + const existingUser = await User.findOne({ + username: userPayload.username + }) + if(existingUser){ + return res.status(411).json({ + msg: "Email already taken" + }) + } + const user = await User.create({ + username: userPayload.username, + firstName: userPayload.firstName, + lastName: userPayload.lastName, + password: userPayload.password + }) + const userId = user._id; + // creating a new accoutn linked to this user and randomly giving him some money + await Account.create({ + userId, + balance: 1 + Math.random()*10000 + }) + const token = jwt.sign({ + userId: userId + },JWT_SECRETS); + + res.status(200).json({ + msg: "User created successfully", + token: token + }) +}) + + +const signInbody = zod.object({ + username: zod.string().email(), + password: zod.string() +}) +router.post("/signin",async (req,res)=>{ + const signinPayload = req.body; + const parcedPayload = signInbody.safeParse(signinPayload); + if(!parcedPayload.success){ + return res.status(411).msg({ + msg: "incorrect Inputs" + }) + } + + const user = await User.findOne({ + username: signinPayload.username, + password: signinPayload.password + }) + if(user){ + const userId = user._id; + const token = jwt.sign({ + userId: userId + },JWT_SECRETS); + return res.status(200).json({ + token: token + }) + }else{ + return res.status(411).json({ + msg: "Error while logging in" + }) + } +}) + +const updateBody = zod.object({ + password: zod.string().optional(), + firstName: zod.string().optional(), + lastName: zod.string().optional() +}) + +router.put("/",authMiddleware,async (req,res)=>{ + const { success } = updateBody.safeParse(req.body); + if(!success){ + return res.status(411).json({ + msg: "Error while updating information" + }) + } + await User.updateOne({_id: req.userId},req.body); + res.json({ + msg: "Updated SuccessFully" + }) +}) + +router.get("/bulk",async (req,res)=>{ + const filter = req.query.filter || ""; + const users = await User.find({ + $or: [{ + firstName: { + "$regex":filter + }, + },{ + lastName: { + "$regex": filter + } + }] + }) + res.json({ + user: users.map(user =>({ + username: user.username, + firstName: user.firstName, + lastName: user.lastName, + _id: user._id + })) + }) +}) + +module.exports = router; \ No newline at end of file