Skip to content
Merged
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
7 changes: 4 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Environment Configuration
NODE_ENV=development
PORT=3000
BACKEND_URL=http://localhost:3000

# Database Configuration
# PostgreSQL connection string
Expand Down Expand Up @@ -44,9 +45,9 @@ REDIS_DB=0
EMAIL_ENABLED=false
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_app_password_here
EMAIL_FROM="Code Duel <noreply@codeduel.com>"
SMTP_USER=your_email_address
SMTP_PASS=your_app_password
SMTP_FROM=your_email_address

# Email Reminder Cron Configuration
# Daily reminder time (default: 6 PM daily)
Expand Down
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ package-lock.json
*.sw?
.env

# Generated JS artifacts from TS/TSX (do not commit)
src/**/*.js
src/**/*.js.map
# Temporary files
tmp/
temp/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ When creating a challenge, configure:
- Input validation on all endpoints
- SQL injection protection via Prisma ORM
- Environment variable validation on startup
- Email verification before account activation

## 📝 Development Notes

Expand Down
11 changes: 3 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@prisma/client": "^5.8.0",
"@prisma/client": "^5.22.0",
"axios": "^1.6.5",
"bcryptjs": "^2.4.3",
"bottleneck": "^2.19.5",
Expand All @@ -31,16 +31,11 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^8.2.1",
"express-request-id": "^3.0.0",
"express-session": "^1.17.3",
"express-validator": "^7.0.1",
"ioredis": "^5.9.3",
"express-validator": "^7.3.1",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.1",
"node-cron": "^3.0.3",
"nodemailer": "^8.0.1",
"prisma": "^5.8.0",
"response-time": "^2.3.4",
"prisma": "^5.22.0",
"winston": "^3.11.0"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ model User {
leetcodeUsername String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isEmailVerified Boolean @default(false)
emailVerificationToken String?
emailVerificationExpires DateTime?

// Email Preferences
emailPreferences Json? @default("{\"welcomeEmail\": true, \"streakReminder\": true, \"streakBroken\": true, \"weeklySummary\": true}")
Expand Down
24 changes: 10 additions & 14 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,23 @@ const {
const createApp = () => {
const app = express();

// 1. Security Middlewares (Team T066 Implementation)
// Apply global rate limiting to all API routes
app.use('/api/', apiLimiter);

// Apply strict limiting specifically to auth routes
app.use('/api/auth/', authLimiter);
app.use("/api/admin", adminRoutes);
// 2. CORS configuration
// 1. CORS configuration (must come before rate limiters so preflight requests are handled)
app.use(
cors({
// if corsOrigin is wildcard we avoid setting credentials since browsers reject wildcard with credentials
origin: config.corsOrigin,
credentials: true,
...(config.corsOrigin === '*' ? {} : { credentials: true }),
})
);

// Request tracking middleware
app.use(addRequestId());
app.use(responseTime());
app.use(requestLogger);
// 2. Security Middlewares (Team T066 Implementation)
// Apply global rate limiting to all API routes
app.use('/api/', apiLimiter);

// Apply strict limiting specifically to auth routes
app.use('/api/auth/', authLimiter);

// Body parser middleware
// 3. Body parser middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

Expand Down
2 changes: 2 additions & 0 deletions src/config/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const config = {
// Server Configuration
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || "development",
backendUrl:process.env.BACKEND_URL || "http://localhost:3000",


// Database Configuration
databaseUrl: process.env.DATABASE_URL,
Expand Down
36 changes: 35 additions & 1 deletion src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ const validateResetPassword = [
.withMessage("New password must be at least 6 characters"),
];

/**
* Validation middleware for forgot password
*/
const validateForgotPassword = [
body("email")
.isEmail()
.normalizeEmail()
.withMessage("Valid email is required"),
];

/**
* Validation middleware for reset password
*/
const validateResetPassword = [
body("token").notEmpty().withMessage("Reset token is required"),
body("newPassword")
.isLength({ min: 6 })
.withMessage("New password must be at least 6 characters"),
];

/**
* Validation middleware for profile update
*/
Expand Down Expand Up @@ -97,6 +117,18 @@ const validateUpdateProfile = [
* Register a new user
* POST /api/auth/register
*/

const verifyEmail = asyncHandler(async (req, res) => {
const { token } = req.query;

const result = await authService.verifyEmail(token);

res.status(200).json({
success: true,
message: result.message,
});
});

const register = asyncHandler(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
Expand Down Expand Up @@ -254,12 +286,14 @@ module.exports = {
login,
getProfile,
updateProfile,
validateRegister,
validateLogin,
verifyEmail,
forgotPassword,
resetPassword,
logout,
validateRegister,
validateLogin,
validateForgotPassword,
validateResetPassword,
validateUpdateProfile,
};
24 changes: 13 additions & 11 deletions src/routes/auth.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { authLimiter, registerLimiter } = require("../config/rateLimiter");
* @route POST /api/auth/register
* @desc Register a new user
* @access Public
*/
*/
router.post(
"/register",
registerLimiter,
Expand All @@ -20,8 +20,8 @@ router.post(
* @route POST /api/auth/login
* @desc Login user
* @access Public
*/
router.post("/login", authLimiter, authController.validateLogin, authController.login);
*/
router.post("/login", authController.validateLogin, authController.login);

/**
* @route POST /api/auth/forgot-password
Expand Down Expand Up @@ -49,20 +49,22 @@ router.post(
* @route GET /api/auth/profile
* @desc Get current user profile
* @access Private
*/
*/
router.get("/profile", authenticate, authController.getProfile);

/**
* @route PUT /api/auth/profile
* @desc Update user profile
* @access Private
*/
router.put(
"/profile",
authenticate,
authController.validateUpdateProfile,
authController.updateProfile
);
*/
router.put("/profile", authenticate, authController.updateProfile);

/**
* @route PUT /api/auth/verify-email
* @desc Verifies user email
* @access Public
*/
router.get("/verify-email", authController.verifyEmail);

/**
* @route POST /api/auth/logout
Expand Down
84 changes: 57 additions & 27 deletions src/services/auth.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ const { config } = require("../config/env");
const { generateToken, decodeToken } = require("../utils/jwt");
const { AppError } = require("../middlewares/error.middleware");
const logger = require("../utils/logger");
const { logAudit } = require("../utils/auditLogger");
const { sendWelcomeEmail, sendPasswordResetEmail } = require("./email.service");

const { sendWelcomeEmail, sendVerificationEmail } = require("./email.service");
const crypto = require('crypto');
/**
* Register a new user
*/
Expand All @@ -30,43 +29,70 @@ const register = async (userData) => {
}
}

// Hash password

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

// Create user
const verificationToken = crypto.randomBytes(32).toString("hex");
const tokenExpiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour

const user = await prisma.user.create({
data: {
email,
username,
password: hashedPassword,
leetcodeUsername: leetcodeUsername || null,
isEmailVerified: false,
emailVerificationToken: verificationToken,
emailVerificationExpires: tokenExpiry,
},
});

// Generate JWT token
const token = generateToken({ userId: user.id });

logger.info(`New user registered: ${user.username} (${user.email})`);

await logAudit("USER_REGISTERED", user.id, {
username: user.username,
email: user.email,
sendVerificationEmail(
user.email,
user.username,
verificationToken
).catch((err) => {
logger.error(`Failed to send verification email: ${err.message}`);
});

return {
user,
message: "Registration successful. Please verify your email.",
};
};
// VerifyEmail
const verifyEmail = async (token) => {
const user = await prisma.user.findFirst({
where: {
emailVerificationToken: token,
emailVerificationExpires: {
gte: new Date(),
},
},
});
// Send welcome email (non-blocking)

if (!user) {
throw new AppError("Invalid or expired verification token", 400);
}

await prisma.user.update({
where: { id: user.id },
data: {
isEmailVerified: true,
emailVerificationToken: null,
emailVerificationExpires: null,
},
});

sendWelcomeEmail(user.email, user.username).catch((err) => {
logger.error(`Failed to send welcome email: ${err.message}`);
});

logger.info(`Email verified for user: ${user.username}`);

return {
user: {
id: user.id,
email: user.email,
username: user.username,
leetcodeUsername: user.leetcodeUsername,
createdAt: user.createdAt,
},
token,
};
return { message: "Email verified successfully" };
};

/**
Expand All @@ -91,7 +117,14 @@ const login = async (emailOrUsername, password) => {
throw new AppError("Invalid credentials", 401);
}

// Generate token
if (!user.isEmailVerified) {
throw new AppError(
"Please verify your email before logging in.",
403
);
}

// Generate JWT token
const token = generateToken({ userId: user.id });

logger.info(`User logged in: ${user.username}`);
Expand Down Expand Up @@ -381,8 +414,5 @@ module.exports = {
login,
getProfile,
updateProfile,
forgotPassword,
resetPassword,
blacklistToken,
isTokenBlacklisted,
verifyEmail,
};
Loading