Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8d79615
feat: Implement login,newRequest,check APIs with new Architecture
bhuveshrohankar28-cmd Sep 24, 2026
e8d7d6a
cleanup
bhuveshrohankar28-cmd Sep 25, 2026
8f102a6
feat: Implement login,newRequest,check APIs with new Architecture
bhuveshrohankar28-cmd Sep 24, 2026
065b75d
cleanup
bhuveshrohankar28-cmd Sep 25, 2026
b054c42
Merge branch 'MyBackend' of https://github.com/bhuveshrohankar28-cmd/…
bhuveshrohankar28-cmd Sep 25, 2026
154191c
feat: Implement login,newRequest,check APIs with new Architecture
bhuveshrohankar28-cmd Sep 25, 2026
2a9fbc9
Merge branch 'MyBackend' of https://github.com/bhuveshrohankar28-cmd/…
bhuveshrohankar28-cmd Sep 25, 2026
79c5751
resolved concurent email check
bhuveshrohankar28-cmd Sep 25, 2026
e85303a
resolved concurent email check
bhuveshrohankar28-cmd Sep 25, 2026
ff5f4a1
Merge branch 'MyBackend' of https://github.com/bhuveshrohankar28-cmd/…
bhuveshrohankar28-cmd Sep 25, 2026
bf8fe0a
resolved
bhuveshrohankar28-cmd Sep 25, 2026
fca7ccc
feat: complete authentication and email services
manthanumredkar Sep 26, 2026
5d71e1c
chore: remove local environment files
manthanumredkar Sep 26, 2026
f9f9a5b
chore: update environment example
manthanumredkar Sep 26, 2026
a276a71
Merge pull request #53 from bhuveshrohankar28-cmd/MyBackend
dakshtitarmare Sep 26, 2026
9531443
Merge branch 'phase-2-practice' into phase-2-practice
manthanumredkar Sep 26, 2026
4237d46
Merge pull request #54 from manthanumredkar/phase-2-practice
dakshtitarmare Sep 27, 2026
e9b7fe1
Refactor authentication and ID generation logic
dakshtitarmare Sep 29, 2026
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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
PORT=5000
NODE_ENV=development
CLIENT_URL=http://localhost:5173

JWT_SECRET=replace_with_your_secret

FIREBASE_PROJECT_ID=your_project_id
FIREBASE_CLIENT_EMAIL=your_service_account_email
FIREBASE_PRIVATE_KEY=your_private_key
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ node_modules
dist
dist-ssr
*.local
.env
.env.*

# Editor directories and files
.vscode/*
Expand All @@ -22,3 +24,10 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env
.env.*
!.env.example
backend/.env
backend/.env2

backend/rtf-firebase-sdk-serviceSDK.json
Empty file removed backend/.env
Empty file.
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FIREBASE_PROJECT_ID=your_project_id
FIREBASE_CLIENT_EMAIL=your_service_account_email
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_KEY_HERE\n-----END PRIVATE KEY-----\n"
FIREBASE_DATABASE_URL=https://your-project-default-rtdb.firebaseio.com
Binary file added backend/.gitignore
Binary file not shown.
35 changes: 12 additions & 23 deletions backend/config/firebaseAdmin.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,26 @@
// config/firebaseAdmin.js
// ─────────────────────────────────────────────────────────────
// Initializes the Firebase Admin SDK ONCE, using your sandbox
// project's service account credentials from .env (see
// contribution-guide/firebase-local-setup.md if you haven't set
// this up yet).
//
// The Admin SDK has FULL access to the database — it ignores
// database.rules.json entirely. This is why the frontend must
// NEVER get these credentials; only this backend process holds them.
// ─────────────────────────────────────────────────────────────
require('dotenv').config();

const admin = require('firebase-admin');
const { initializeApp, cert, getApps } = require('firebase-admin/app');
const { getDatabase } = require('firebase-admin/database');

// The private key in .env has literal "\n" characters (since .env
// values are single-line strings) — we convert them back to real
// newlines here, or the SDK will reject the key as malformed.
const firebaseConfig = {
credential: admin.credential.cert({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
}),
databaseURL: process.env.FIREBASE_DATABASE_URL,
};

// Guard against accidentally initializing twice (can happen if this
// file gets required from multiple places during hot-reload).
if (!admin.apps.length) {
admin.initializeApp(firebaseConfig);
let app;

if (!getApps().length) {
app = initializeApp(firebaseConfig);
console.log('✅ Firebase Admin initialized');
} else {
app = getApps()[0];
}

// db is what every models/*.js file will import and use to read/write.
const db = admin.database();
const db = getDatabase(app);

module.exports = { admin, db };
module.exports = { db };
32 changes: 32 additions & 0 deletions backend/config/nodemailer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require("dotenv").config();

const nodemailer = require("nodemailer");

const transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,

auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});

async function sendMail(to, sub, msg) {
try {
const info = await transporter.sendMail({
from: process.env.EMAIL_USER,
to: to,
subject: sub,
html: msg,
});

console.log("Email sent:", info.messageId);
} catch (error) {
console.error("Email failed:", error);
}
}

module.exports = sendMail;

188 changes: 131 additions & 57 deletions backend/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,84 +12,158 @@
// ─────────────────────────────────────────────────────────────

const userModel = require('../models/userModel');
const { hashPassword } = require('../services/authService');
const asyncHandler = require('../utils/asyncHandler');
const {
hashPassword,
comparePassword,
generateAccessToken,
} = require("../services/authService");
const { generateRtfId } = require("../services/idGeneratorService");
const asyncHandler = require("../utils/asyncHandler");


/**
* POST /api/auth/register
* Body (already validated by validateRequest(registerSchema)):
* name, collegeEnrollmentNo, collegeEmail, personalEmail,
* branch, yearOfPassing, phone, domain, password
*
* Flow:
* 1. Check personalEmail isn't already registered
* 2. Hash the password (NEVER store it plain)
* 3. Create the user record (status: "pending")
* 4. Return success — the frontend shows a
* "awaiting domain admin approval" message, NOT a logged-in state.
* (No JWT is issued here — the account can't log in until approved.
* The login endpoint, built the same way, checks status === "active".)
*/
const register = asyncHandler(async (req, res) => {
const { personalEmail, password, ...rest } = req.body;

// 1. Duplicate check
const alreadyExists = await userModel.emailExists(personalEmail);
if (alreadyExists) {
// Throwing an error with .statusCode is how we control the
// HTTP status code that errorHandler.js eventually sends.
const err = new Error('An account with this email already exists');
err.statusCode = 409; // 409 Conflict
throw err;
const {
name,
collegeEnrollmentNo,
collegeEmail,
personalEmail,
branch,
yearOfPassing,
phone,
domain,
password,
} = req.body;

// 1. Basic validation
if (!personalEmail || !password || !domain || !yearOfPassing) {
const error = new Error(
"personalEmail, password, domain, and yearOfPassing are required fields."
);
error.statusCode = 400;
throw error;
}

// 2. Check if user already exists
const existingUser = await userModel.getUserByEmail(personalEmail);

if (existingUser) {
const error = new Error(
"User with this personal email already exists."
);
error.statusCode = 409;
throw error;
}

// 2. Hash the password — this is the ONLY place a password
// should ever be touched in plain text, and it happens
// immediately, before anything is stored.
// 3. Hash password
const passwordHash = await hashPassword(password);

// 3. Create the record via the model (model handles the
// /users + /usersByEmail multi-path write internally)
const { uid } = await userModel.createUser({
...rest,
// 4. Generate temporary RTF ID
const rtfId = await generateRtfId(
domain,
yearOfPassing
);

// 5. Create user
const newUser = await userModel.createUser({
name,
collegeEnrollmentNo,
collegeEmail,
personalEmail,
branch,
yearOfPassing,
phone,
domain,
rtfId,
passwordHash,
role: "member",
status: "pending",
createdAt: Date.now(),
});

// 4. Respond — 201 Created, consistent { success, data } shape
// 6. Remove passwordHash from response
const { passwordHash: _, ...safeUserData } = newUser;

// 7. Send response
res.status(201).json({
success: true,
message: "Registration successful. Account pending approval.",
data: {
uid,
message: 'Registration received. Your domain admin will review your request.',
user: safeUserData,
},
});
});

// ─────────────────────────────────────────────────────────────
// NEXT UP (build these the same way, as separate functions below,
// once register is tested and working):
//
// const login = asyncHandler(async (req, res) => {
// 1. userModel.getUserByEmail(personalEmail)
// 2. if not found → 401 "Invalid credentials" (don't reveal
// whether it was the email or password that was wrong)
// 3. if found but status !== "active" → 403 "Account pending approval"
// 4. comparePassword(password, user.passwordHash) → if false, 401
// 5. generateAccessToken({ uid, role, domain })
// 6. res.json({ success: true, data: { token, user: {...safe fields} } })
// — NEVER include passwordHash in what you send back!
// });
//
// const getMe = asyncHandler(async (req, res) => {
// // req.user is attached by authMiddleware.js after verifying the JWT
// const user = await userModel.getUserByUid(req.user.uid);
// res.json({ success: true, data: user });
// });
// ─────────────────────────────────────────────────────────────

/**
* POST /api/auth/login
*/
const login = asyncHandler(async (req, res) => {
const { rtfId, password } = req.body;

// 1. Validate input
if (!rtfId || !password) {
const error = new Error(
"Both rtfId and password are required."
);
error.statusCode = 400;
throw error;
}

// 2. Find user
const user = await userModel.getUserByRtfId(rtfId);

if (!user) {
const error = new Error("Invalid credentials.");
error.statusCode = 401;
throw error;
}

// 3. Compare password
const isPasswordValid = await comparePassword(
password,
user.passwordHash
);

if (!isPasswordValid) {
const error = new Error("Invalid credentials.");
error.statusCode = 401;
throw error;
}

// 4. Check account status
if (user.status !== "active") {
const error = new Error(
"Your account has not been approved yet."
);
error.statusCode = 403;
throw error;
}

// 5. Generate JWT
const token = generateAccessToken({
uid: user.uid,
role: user.role,
domain: user.domain,
});

// 6. Remove passwordHash
const { passwordHash: _, ...safeUserData } = user;

// 7. Send response
res.status(200).json({
success: true,
message: "Login successful",
data: {
token,
user: safeUserData,
},
});
});

module.exports = {
register,
// login,
// getMe,
login,
};
Loading