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
13 changes: 12 additions & 1 deletion server/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 server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"passport-google-oauth20": "^2.0.0",
"qs": "6.14.1",
"redis": "^5.8.3",
"socket.io": "^4.8.1"
"socket.io": "^4.8.1",
"zod": "^4.3.5"
},
"devDependencies": {
"@flydotio/dockerfile": "^0.7.10",
Expand Down
46 changes: 18 additions & 28 deletions server/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import prisma from "../db.js";
import { generateOTP, sendOTPEmail, isOTPEnabled } from "../utils/email.js";
import { signupSchema, resetPasswordSchema } from "../schemas/auth.schema. js";
import passport from "../config/passport.js";


const router = express.Router();

// Generate JWT token
Expand Down Expand Up @@ -141,23 +143,14 @@ router.post("/signup", async (req, res) => {

console.log("πŸ” Signup attempt:", { username, email });

if (!username || !email || !password) {
return res
.status(400)
.json({ error: "Username, email, and password are required" });
}

if (username.length < 3) {
return res
.status(400)
.json({ error: "Username must be at least 3 characters" });
}

if (password.length < 6) {
return res
.status(400)
.json({ error: "Password must be at least 6 characters long" });
}
// Validate request body with Zod schema
try {
signupSchema.parse({ username, email, password, otp });
} catch (zodError) {
// Format Zod errors into user-friendly messages
const errors = zodError.errors.map(err => err.message).join('; ');
return res.status(400).json({ error: errors });
}

// Verify OTP if enabled
if (isOTPEnabled()) {
Expand Down Expand Up @@ -342,17 +335,14 @@ router.post("/reset-password", async (req, res) => {
try {
const { email, otp, newPassword } = req.body;

if (!email || !otp || !newPassword) {
return res
.status(400)
.json({ error: "Email, OTP, and new password are required" });
}

if (newPassword.length < 6) {
return res
.status(400)
.json({ error: "Password must be at least 6 characters long" });
}
// Validate request body with Zod schema
try {
resetPasswordSchema.parse({ email, otp, newPassword });
} catch (zodError) {
// Format Zod errors into user-friendly messages
const errors = zodError.errors.map(err => err.message).join('; ');
return res.status(400).json({ error: errors });
}

// Verify OTP
const otpRecord = await prisma.otp.findFirst({
Expand Down
85 changes: 85 additions & 0 deletions server/schemas/auth.schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { z } from 'zod';

/**
* Password validation schema using Zod
* Aligned with frontend PasswordInput component from PR #85
*
* Criteria (matching frontend strength calculator):
* - Minimum 8 characters
* - At least one uppercase letter
* - At least one lowercase letter
* - At least one number
* - At least one special character
*/
export const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters long')
.regex(/[a-z]/, 'Password must include at least one lowercase letter')
.regex(/[A-Z]/, 'Password must include at least one uppercase letter')
.regex(/[0-9]/, 'Password must include at least one number')
.regex(
/[^A-Za-z0-9]/,
'Password must include at least one special character (! @#$%^&*... )'
)
.refine(
(password) => {
// Block common passwords
const commonPasswords = [
'password', 'password123', '123456', '123456789', '12345678',
'qwerty', 'abc123', 'monkey', '1234567', 'letmein',
'trustno1', 'dragon', 'baseball', 'iloveyou', 'master',
'sunshine', 'ashley', 'bailey', 'shadow', 'superman',
'qazwsx', 'welcome', 'admin', 'login', 'passw0rd'
];
return !commonPasswords.includes(password.toLowerCase());
},
{ message: 'This password is too common. Please choose a more secure password' }
)
.refine(
(password) => {
// Block sequential characters (abc, 123)
const sequential = /(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i;
return !sequential.test(password);
},
{ message: 'Password should not contain sequential characters (e.g., "abc", "123")' }
)
.refine(
(password) => {
// Block repeated characters (aaa, 111)
return !/(. )\1{2,}/.test(password);
},
{ message: 'Password should not contain repeated characters (e.g., "aaa", "111")' }
);

/**
* Signup request validation schema
*/
export const signupSchema = z. object({
username: z
.string()
.min(3, 'Username must be at least 3 characters')
.max(30, 'Username must not exceed 30 characters')
.regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),
email: z
. string()
.email('Please enter a valid email address'),
password: passwordSchema,
otp: z.string().optional()
});

/**
* Reset password request validation schema
*/
export const resetPasswordSchema = z. object({
email: z. string().email('Please enter a valid email address'),
otp: z.string().min(6, 'OTP must be 6 digits'),
newPassword: passwordSchema
});

/**
* Login request validation schema
*/
export const loginSchema = z.object({
usernameOrEmail: z.string().min(1, 'Username or email is required'),
password: z.string().min(1, 'Password is required')
});