Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 90 additions & 35 deletions backend/__tests__/auth.test.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,99 @@
const request = require('supertest');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { app, server } = require('../server');
const User = require('../models/User');
process.env.NODE_ENV = "test";

const request = require("supertest");
const mongoose = require("mongoose");
const { MongoMemoryServer } = require("mongodb-memory-server");
const { app, server } = require("../server");
const User = require("../models/User");

let mongoServer;

beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
process.env.MONGO_URI = mongoUri;
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
process.env.MONGO_URI = mongoUri;
await mongoose.connect(mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});

afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
server.close();
await mongoose.disconnect();
await mongoServer.stop();
server.close();
});

describe('Auth API', () => {

beforeEach(async () => {
await User.deleteMany({});
});

it('should allow a new user to sign up', async () => {
const newUser = {
email: 'testuser@gmail.com',
password: 'Password123!',
};

const response = await request(app)
.post('/api/auth/signup')
.send(newUser);

expect(response.statusCode).toBe(201);
expect(response.body).toHaveProperty('token');

const savedUser = await User.findOne({ email: 'testuser@gmail.com' });
expect(savedUser).not.toBeNull();
});

});
describe("Auth API", () => {
beforeEach(async () => {
await User.deleteMany({});
});

it("should allow a new user to sign up", async () => {
const newUser = {
email: "testuser@gmail.com",
password: "Password123!",
};

const response = await request(app).post("/api/auth/signup").send(newUser);

expect(response.statusCode).toBe(201);
expect(response.body).toHaveProperty("token");

const savedUser = await User.findOne({ email: "testuser@gmail.com" });
expect(savedUser).not.toBeNull();
});

it("should reject signup with an existing email", async () => {
const testUser = {
email: "duplicate@gmail.com",
password: "Password123!",
};

await request(app).post("/api/auth/signup").send(testUser).expect(201);

const response = await request(app)
.post("/api/auth/signup")
.send(testUser)
.expect(400);

expect(response.body.message).toBe("User already exists");

const users = await User.find({ email: testUser.email });
expect(users.length).toBe(1);
});

it("should reject signup when email is missing", async () => {
const missingEmailUser = {
email: "",
password: "Password123!",
};

const response = await request(app)
.post("/api/auth/signup")
.send(missingEmailUser)
.expect(400);

expect(response.body.message).toBe("Please enter all fields");

const users = await User.find({});
expect(users.length).toBe(0);
});

it("should reject signup when password is missing", async () => {
const missingPasswordUser = {
email: "user@example.com",
password: "",
};

const response = await request(app)
.post("/api/auth/signup")
.send(missingPasswordUser)
.expect(400);

expect(response.body.message).toBe("Please enter all fields");

const users = await User.find({});
expect(users.length).toBe(0);
});
});
20 changes: 11 additions & 9 deletions backend/config/db.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
const mongoose = require('mongoose');
const mongoose = require("mongoose");

const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI);
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
try {
const conn = await mongoose.connect(process.env.MONGO_URI);
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error.message}`);
if (process.env.NODE_ENV !== "test") {
process.exit(1);
}
}
};

module.exports = connectDB;
module.exports = connectDB;
94 changes: 53 additions & 41 deletions backend/middleware/validationMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,62 @@
const { body, validationResult } = require('express-validator');
const dns = require('dns');
const { body, validationResult } = require("express-validator");
const dns = require("dns");

const validateRegistration = [
// Validate email
body('email')
.isEmail()
.withMessage('Please enter a valid email address.')
.bail() // Stop running validators if the previous one failed
.custom(async (email) => {
const domain = email.split('@')[1];
(req, res, next) => {
if (!req.body.email || !req.body.password) {
return res.status(400).json({ message: "Please enter all fields" });
}
next();
},
// Validate email
body("email")
.isEmail()
.withMessage("Please enter a valid email address.")
.bail() // Stop running validators if the previous one failed
.custom(async (email) => {
const domain = email.split("@")[1];

// Quick blacklist for common invalid domains
const blockedDomains = ['example.com', 'test.com', 'invalid.com'];
if (blockedDomains.includes(domain)) {
return Promise.reject('This email domain is not allowed.');
}
// Quick blacklist for common invalid domains
const blockedDomains = ["example.com", "test.com", "invalid.com"];
if (blockedDomains.includes(domain)) {
return Promise.reject("This email domain is not allowed.");
}

// Check for valid MX records
try {
const addresses = await dns.promises.resolveMx(domain);
if (!addresses || addresses.length === 0) {
return Promise.reject('Email domain does not exist or cannot receive mail.');
}
} catch (error) {
// If DNS resolution fails
return Promise.reject('Email domain does not exist or cannot receive mail.');
}
}),
// Check for valid MX records
try {
const addresses = await dns.promises.resolveMx(domain);
if (!addresses || addresses.length === 0) {
return Promise.reject(
"Email domain does not exist or cannot receive mail."
);
}
} catch (error) {
// If DNS resolution fails
return Promise.reject(
"Email domain does not exist or cannot receive mail."
);
}
}),

// Validate password
body('password')
.isLength({ min: 8, max: 16 })
.withMessage('Password must be between 8 and 16 characters long.')
.matches(/^(?=.*\d)(?=.*[a-zA-Z])(?=.*[\W_])/)
.withMessage('Password must contain at least one alphabet, one digit, and one symbol.'),
// Validate password
body("password")
.isLength({ min: 8, max: 16 })
.withMessage("Password must be between 8 and 16 characters long.")
.matches(/^(?=.*\d)(?=.*[a-zA-Z])(?=.*[\W_])/)
.withMessage(
"Password must contain at least one alphabet, one digit, and one symbol."
),

// Middleware to handle the validation result
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ message: errors.array()[0].msg });
}
next();
},
// Middleware to handle the validation result
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ message: errors.array()[0].msg });
}
next();
},
];

module.exports = {
validateRegistration,
};
validateRegistration,
};
20 changes: 19 additions & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.19.0",
"mongoose": "^8.18.1",
"multer": "^2.0.2"
"multer": "^2.0.2",
"resend": "^6.1.0"
},
"devDependencies": {
"jest": "^29.7.0",
Expand Down
58 changes: 32 additions & 26 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,54 @@
const express = require('express');
const path = require('path');
const dotenv = require('dotenv');
const cors = require('cors');
const connectDB = require('./config/db');
const express = require("express");
const path = require("path");
const dotenv = require("dotenv");
const cors = require("cors");
const connectDB = require("./config/db");

// Load environment variables
dotenv.config();

// Connect to database
connectDB();
if (process.env.NODE_ENV !== "test") {
connectDB();
}

const app = express();

const allowedOrigins = [
"http://localhost:5173",
"https://paisable.netlify.app"
"http://localhost:5173",
"https://paisable.netlify.app",
];

app.use(cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true
}));
app.use(
cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
})
);
app.use(express.json());

// Routes
app.use('/api/auth', require('./routes/authRoutes'));
app.use('/api/transactions', require('./routes/transactionRoutes'));
app.use('/api/receipts', require('./routes/receiptRoutes'));
app.use("/api/auth", require("./routes/authRoutes"));
app.use("/api/transactions", require("./routes/transactionRoutes"));
app.use("/api/receipts", require("./routes/receiptRoutes"));

// Serve static files from the uploads directory
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use("/uploads", express.static(path.join(__dirname, "uploads")));

app.get('/', (req, res) => {
res.send('API is Running');
app.get("/", (req, res) => {
res.send("API is Running");
});

const PORT = process.env.PORT || 5000;

const server = app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
const server = app.listen(PORT, () =>
console.log(`Server started on port ${PORT}`)
);

module.exports = { app, server };
module.exports = { app, server };
2 changes: 2 additions & 0 deletions frontend/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_API_URL=http://localhost:5000/api

Loading