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
5 changes: 5 additions & 0 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
PORT=
JWT_SECRET=
JWT_REFRESH_SECRET=
EMAIL_USER=
EMAIL_PASS=
91 changes: 91 additions & 0 deletions apps/backend/helpers/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import jwt from "jsonwebtoken";
import nodemailer from "nodemailer";
import crypto from "crypto";
import { client } from "db/client";

const generateOtp = () => {
return crypto.randomInt(100000, 999999);
};

const sendOtp = async (email: string, otp: string) => {
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});

const mailOptions = {
from: `"100xContest" <${process.env.EMAIL_USER}>`,
to: email,
subject: "Email verification code",

html: `
<div style="font-family: 'Segoe UI', sans-serif; max-width: 600px; margin: auto; padding: 30px; border: 1px solid #e0e0e0; border-radius: 10px;">
<h2 style="color: #333; font-weight: 500;">Hi there,</h2>
<p style="font-size: 15px; color: #444;">
Please use the code below to confirm your email address and continue on <strong>100xContest</strong>.
This code will expire in <strong>5 minutes</strong>. If you don't think you should be receiving this email, you can safely ignore it.
</p>

<div style="text-align: center; margin: 40px 0;">
<span style="font-size: 42px; font-weight: bold; color: #000;">${otp}</span>
</div>

<hr style="border: none; border-top: 1px solid #ccc;" />

<p style="font-size: 12px; color: #999; margin-top: 20px;">
You received this email because you requested a confirmation code from <strong>100xContest</strong>.
</p>
</div>
`,
};

await transporter.sendMail(mailOptions);
};

const resendOtp = async (email: string) => {
const otp = generateOtp().toString();
const expiresAt = new Date(Date.now() + 5 * 60 * 1000);

await client.otpVerification.upsert({
where: { email },
update: { otp, expiresAt, attempts: 0 },
create: { email, otp, expiresAt },
});

await sendOtp(email, otp);
};

const generateAccessToken = (user: any) => {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error("JWT_SECRET is not defined in environment variables");
}
return jwt.sign({ userId: user.id, email: user.email }, secret, {
expiresIn: "2h",
});
};

const generateRefreshToken = (user: any) => {
const refreshSecret = process.env.JWT_REFRESH_SECRET;
if (!refreshSecret) {
throw new Error(
"JWT_REFRESH_SECRET is not defined in environment variables"
);
}
return jwt.sign({ userId: user.id, email: user.email }, refreshSecret, {
expiresIn: "30d",
});
};

const refreshAccessToken = (refreshToken: string) => {
const decoded = jwt.verify(
refreshToken,
process.env.JWT_REFRESH_SECRET as string
) as any;
return generateAccessToken({ id: decoded.userId, email: decoded.email });
};

export { generateOtp, generateAccessToken, generateRefreshToken, sendOtp, resendOtp, refreshAccessToken };
12 changes: 12 additions & 0 deletions apps/backend/helpers/bcrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import bcrypt from "bcrypt";

const hash = (password: string) => {
const hashed = bcrypt.hash(password, 10);

return hashed;
};
const compare = (password: string, hash: string) => {
return bcrypt.compare(password, hash);
};

export { hash, compare };
56 changes: 48 additions & 8 deletions apps/backend/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@

import express from "express";
import userRouter from "./routes/user"
import contestRouter from "./routes/contest"
import adminRouter from "./routes/admin"
import cors from "cors";
import cookieParser from "cookie-parser";
import dotenv from "dotenv";

// routes import
import contestRouter from "./routes/contest";
import userRouter from "./routes/user";
import adminRouter from "./routes/admin";

const app = express();
dotenv.config();

const allowedOrigins = ["http://localhost:3000"];
const corsOptions = {
origin: function (origin: any, callback: any) {
if (!origin) {
return callback(null, true);
}

if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
console.error(`Blocked by CORS: ${origin}`);
callback(new Error(`Origin ${origin} not allowed by CORS`));
}
},
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
credentials: true,
preflightContinue: false,
optionsSuccessStatus: 204,
};

// Middleware
app.use(cors(corsOptions));
app.use(cookieParser());
app.use(express.json());

// Routes
app.get("/health", (req, res) => {
res.json({ message: "Health Check!" });
});
app.use("/api/v1/user", userRouter);
app.use("/api/v1/admin", adminRouter);
app.use("/api/v1/contest", contestRouter);

app.use("/user", userRouter);
app.use("/admin", adminRouter);
app.use("/contest", contestRouter);
const port = process.env.PORT || 4000;

app.listen(process.env.PORT ?? 4000);
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});
14 changes: 13 additions & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,20 @@
"typescript": "^5.0.0"
},
"dependencies": {
"@types/bcrypt": "^6.0.0",
"@types/cookie-parser": "^1.4.9",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/nodemailer": "^7.0.1",
"bcrypt": "^6.0.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"db": "*",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"db": "*"
"express-rate-limit": "^8.0.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^7.0.6"
}
}
66 changes: 61 additions & 5 deletions apps/backend/routes/admin.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,71 @@
import { Router } from "express";
import { client } from "db/client";
import { generateAccessToken, generateRefreshToken } from "../helpers/auth";

const router = Router();

router.post("/signup", (req, res) => {
// Note for bhayia - no signup needed for admin, seed an email- pass for admin, just login
// router.post("/signup", (req, res) => {

// })

})
router.post("/signin", async (req, res) => {
try {
const { email, password } = req.body;

router.post("/signin", (req, res) => {
if (!email || !password) {
return res.status(400).json({
success: false,
message: "Email and password are required",
});
}

const admin = await client.user.findFirst({
where: {
email,
role: "Admin",
},
});

})
if (!admin) {
return res.status(401).json({
success: false,
message: "Invalid credentials",
});
}

export default router;
if (admin.password !== password) {
return res.status(401).json({
success: false,
message: "Invalid credentials",
});
}

const accessToken = generateAccessToken(admin);
const refreshToken = generateRefreshToken(admin);

res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 30 * 24 * 60 * 60 * 1000,
});

res.json({
accessToken,
user: {
id: admin.id,
email: admin.email,
role: admin.role,
},
});
} catch (error) {
console.error("Admin signin error:", error);
res.status(500).json({
success: false,
message: "Internal server error",
});
}
});

export default router;
Loading