Skip to content
Open
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
33 changes: 33 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.3",
"multer": "^2.0.2",
"proper-lockfile": "^4.1.2",
"rate-limit-redis": "^4.2.3",
"react-hot-toast": "^2.6.0",
"redis": "^4.7.1",
Expand Down
105 changes: 105 additions & 0 deletions server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,111 @@ describe("route error responses", () => {
);
});

test("POST /api/auth/signup - single user registration succeeds", async () => {
const testEmail = `test-single-${Date.now()}@example.com`;
const res = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: testEmail,
password: "Test@1234",
}),
});
assert.equal(res.status, 201);
const data = await res.json();
assert.ok(data.token);
assert.equal(data.message, "Signup successful");
});

test("POST /api/auth/signup - duplicate email is rejected", async () => {
const testEmail = `test-dup-${Date.now()}@example.com`;

const firstRes = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: testEmail,
password: "Test@1234",
}),
});
assert.equal(firstRes.status, 201);

const secondRes = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: testEmail,
password: "Different@1234",
}),
});
assert.equal(secondRes.status, 400);
const data = await secondRes.json();
assert.equal(data.message, "User already exists");
});

test("POST /api/auth/signup - concurrent registrations prevent lost updates", async () => {
const timestamp = Date.now();
const emails = [
`concurrent-1-${timestamp}@example.com`,
`concurrent-2-${timestamp}@example.com`,
`concurrent-3-${timestamp}@example.com`,
];

const promises = emails.map((email) =>
fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email,
password: "Test@1234",
}),
})
);

const results = await Promise.all(promises);

const successCount = results.filter((r) => r.status === 201).length;
assert.equal(successCount, 3, "All concurrent registrations should succeed");

const data = await results[0].json();
assert.ok(data.token);
});
Comment thread
Namraa310806 marked this conversation as resolved.

test("POST /api/auth/signup - validates password strength", async () => {
const testEmail = `test-weak-${Date.now()}@example.com`;
const res = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: testEmail,
password: "weak",
}),
});
assert.equal(res.status, 400);
const data = await res.json();
assert.match(data.message, /Password must contain/);
});

test("POST /api/auth/signup - rejects missing email or password", async () => {
const res1 = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
password: "Test@1234",
}),
});
assert.equal(res1.status, 400);

const res2 = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "test@example.com",
}),
});
assert.equal(res2.status, 400);
});

Comment thread
Namraa310806 marked this conversation as resolved.
test("successful upload response does not include a url field", async () => {
const originalPostForm = axios.postForm;
const originalPost = axios.post;
Expand Down
57 changes: 47 additions & 10 deletions src/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const fs = require("fs");
const path = require("path");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const lockfile = require("proper-lockfile");

const { validatePassword } = require("../utils/passwordValidator");

Expand All @@ -17,15 +18,38 @@ const getUsers = () => {
return JSON.parse(fs.readFileSync(usersFile));
};

const saveUsers = (users) => {
fs.writeFileSync(usersFile, JSON.stringify(users, null, 2));
const getUsersLocked = async () => {
const release = await lockfile.lock(usersFile, { retries: 3, stale: 5000 });
try {
const data = fs.readFileSync(usersFile, "utf8");
return { users: JSON.parse(data), release };
} catch (error) {
await release();
throw error;
}
};

const saveUsersLocked = async (users) => {
const tempFile = `${usersFile}.tmp`;
try {
fs.writeFileSync(tempFile, JSON.stringify(users, null, 2), "utf8");
fs.renameSync(tempFile, usersFile);
} catch (error) {
try {
fs.unlinkSync(tempFile);
} catch {
// Ignore cleanup errors - temp file might not exist
}
throw error;
}
};

const normalizeEmail = (email) => {
return email ? String(email).trim().toLowerCase() : email;
};

exports.signup = async (req, res) => {
let release = null;
try {
let { email, password } = req.body;

Expand All @@ -34,7 +58,7 @@ exports.signup = async (req, res) => {
message: "Email and password are required",
});
}

email = normalizeEmail(email);

const validation = validatePassword(password);
Expand All @@ -45,29 +69,34 @@ exports.signup = async (req, res) => {
});
}

const users = getUsers();
const hashedPassword = await bcrypt.hash(
password,
10
);

const { users, release: lockRelease } = await getUsersLocked();
release = lockRelease;

const existingUser = users.find(
(u) => u.email === email
);

if (existingUser) {
await release();
release = null;
return res.status(400).json({
message: "User already exists",
});
}

const hashedPassword = await bcrypt.hash(
password,
10
);

users.push({
email,
password: hashedPassword,
});

saveUsers(users);
await saveUsersLocked(users);
await release();
release = null;

const token = jwt.sign({ email }, SECRET, { expiresIn: "7d" });

Expand All @@ -76,6 +105,14 @@ exports.signup = async (req, res) => {
message: "Signup successful",
});
} catch (error) {
console.error("[signup] error:", error);
if (release) {
try {
await release();
} catch (releaseError) {
console.error("[signup] lock release error:", releaseError);
}
}
res.status(500).json({
message: "Server error",
});
Expand Down
Loading
Loading