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
29 changes: 29 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Dependencies
node_modules/

# Build outputs
dist/
build/

# Environment variables
.env
.env.local
.env.production

# IDE files
.vscode/
.idea/

# OS files
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*

# Package manager files
package-lock.json
yarn.lock
pnpm-lock.yaml
EOF
8 changes: 8 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Backend port
PORT=5000

# MongoDB connection string
MONGO_URI=mongodb://127.0.0.1:27017/vi-notes

# Secret key used to create login tokens
JWT_SECRET=change-this-secret-key
3 changes: 3 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.env
dist/
32 changes: 32 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch src/server.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"bcryptjs": "^3.0.3",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"mongoose": "^9.6.2",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.6.2",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
}
}
32 changes: 32 additions & 0 deletions backend/src/config/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import mongoose from "mongoose";

// Gets environment variables or crashes if missing
const getEnv = (key: string): string => {
const value = process.env[key];
if (!value) {
console.error(`Missing required environment variable: ${key}`);
console.error(
"Please check your .env file and ensure all required variables are set.",
);
process.exit(1);
}
return value;
};

// This function connects our backend to MongoDB.
const connectDB = async () => {
try {
// Get MongoDB connection string from environment
const MONGO_URI = getEnv("MONGO_URI");

// Actually connect to MongoDB
await mongoose.connect(MONGO_URI);
console.log("MongoDB connected");
} catch (error) {
// If database connection fails, stop the backend.
console.error("MongoDB connection failed:", error);
process.exit(1);
}
};

export default connectDB;
116 changes: 116 additions & 0 deletions backend/src/controllers/authController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Request, Response } from "express";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import User from "../models/User.js";
import { registerSchema, loginSchema } from "../validation/authValidation.js";
import { z } from "zod";

// Simple helper to get required environment variables safely.
const getEnv = (key: string): string => {
const value = process.env[key];
if (!value) {
console.error(`Missing required environment variable: ${key}`);
process.exit(1);
}
return value;
};

// This function handles new user registration.
export const register = async (req: Request, res: Response) => {
// First we check if name, email, and password are valid.
const result = registerSchema.safeParse(req.body);
if (!result.success) {
res.status(400).json({ errors: z.treeifyError(result.error) });
return;
}

// After validation, we can safely use these values.
const { name, email, password } = result.data;

// Do not allow two accounts with the same email.
const exists = await User.findOne({ email });
if (exists) {
res.status(409).json({ error: "Email already registered" });
return;
}

// Hash the password so the real password is not stored in MongoDB.
const hashedPassword = await bcrypt.hash(password, 10);

// Save the new user in the database.
const user = await User.create({ name, email, password: hashedPassword });

// Create a token so the frontend knows the user is logged in.
const token = jwt.sign({ id: user._id }, getEnv("JWT_SECRET"), {
expiresIn: "7d",
});

// Send back user details without sending the password.
res.status(201).json({ token, user: { id: user._id, name, email } });
};

// This function handles login for an existing user.
export const login = async (req: Request, res: Response) => {
// First we check if email and password are valid.
const result = loginSchema.safeParse(req.body);
if (!result.success) {
res.status(400).json({ errors: z.treeifyError(result.error) });
return;
}

const { email, password } = result.data;

// Find the account that uses this email.
const user = await User.findOne({ email });
if (!user) {
res.status(401).json({ message: "Invalid credentials" });
return;
}

// Compare the typed password with the hashed password in the database.
const match = await bcrypt.compare(password, user.password);
if (!match) {
res.status(401).json({ message: "Invalid credentials" });
return;
}

// If password is correct, create a new login token.
const token = jwt.sign({ id: user._id }, getEnv("JWT_SECRET"), {
expiresIn: "7d",
});

// Send back the token and basic user details.
res.json({ token, user: { id: user._id, name: user.name, email } });
};

// This function returns the current user from a valid JWT token.
export const me = async (req: Request, res: Response) => {
try {
// The user ID is attached to the request by the authenticateToken middleware.
const userId = req.user?.id;

if (!userId) {
res.status(401).json({ error: "User not found" });
return;
}

// Find the user in the database.
const user = await User.findById(userId).select("-password");

if (!user) {
res.status(401).json({ error: "User not found" });
return;
}

// Return user details without the password.
res.json({
user: {
id: user._id,
name: user.name,
email: user.email,
},
});
} catch (error) {
res.status(500).json({ error: "Server error" });
}
};
45 changes: 45 additions & 0 deletions backend/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";

// Simple helper to get required environment variables safely.
const getEnv = (key: string): string => {
const value = process.env[key];
if (!value) {
console.error(`Missing required environment variable: ${key}`);
process.exit(1);
}
return value;
};

// This adds the user's ID to the request when a valid JWT is provided.
export const authenticateToken = (req: Request, res: Response, next: NextFunction) => {
// Get the token from the Authorization header.
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1]; // Bearer <token>

if (!token) {
res.status(401).json({ error: "Access token required" });
return;
}

// Verify the token with our secret key.
jwt.verify(token, getEnv("JWT_SECRET"), (err, decoded) => {
if (err) {
res.status(401).json({ error: "Invalid or expired token" });
return;
}

// Add the user's ID to the request for use in protected routes.
req.user = { id: (decoded as any).id };
next();
});
};

// Extend the Request type to include user information.
declare global {
namespace Express {
interface Request {
user?: { id: string };
}
}
}
24 changes: 24 additions & 0 deletions backend/src/models/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import mongoose, { Document, Schema } from "mongoose";

// This tells TypeScript what a user should contain.
export interface IUser extends Document {
name: string;
email: string;
password: string;
}

// This tells MongoDB what fields to store for every user.
const UserSchema: Schema = new Schema(
{
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
},
{
// This adds createdAt and updatedAt automatically.
timestamps: true,
},
);

// This User model is used in controllers to create and find users.
export default mongoose.model<IUser>("User", UserSchema);
18 changes: 18 additions & 0 deletions backend/src/routes/authRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Router } from "express";
import { register, login, me } from "../controllers/authController.js";
import { authenticateToken } from "../middleware/auth.js";

// This file connects auth URLs to their controller functions.
const router = Router();

// When frontend calls POST /api/auth/register, run the register function.
router.post("/register", register);

// When frontend calls POST /api/auth/login, run the login function.
router.post("/login", login);

// When frontend calls GET /api/auth/me, verify JWT and return current user.
router.get("/me", authenticateToken, me);

// server.ts uses this router under /api/auth.
export default router;
35 changes: 35 additions & 0 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import connectDB from "./config/database.js";
import authRoutes from "./routes/authRoutes.js";

// This loads values like MONGO_URI and JWT_SECRET from the .env file.
dotenv.config();

// This creates the Express backend app.
const app = express();

// cors allows the frontend to call this backend.
app.use(cors());

// This lets the backend read JSON data sent from forms.
app.use(express.json());

const PORT = process.env.PORT || 5000;

// This route is just used to check if the backend is running.
app.get("/", (req, res) => {
res.json({ message: "Vi-Notes API running" });
});

// All login and register routes start with /api/auth.
app.use("/api/auth", authRoutes);

// Connect to MongoDB before the server starts accepting requests.
connectDB();

// This starts the backend server.
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
18 changes: 18 additions & 0 deletions backend/src/validation/authValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { z } from "zod";

// These are the backend rules for creating a new account.
export const registerSchema = z.object({
name: z.string().min(2, "Name must be atleast 2 characters long"),
email: z.email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters long"),
});

// These are the backend rules for logging in.
export const loginSchema = z.object({
email: z.email("Invalid email address"),
password: z.string().min(1, "Password is required"),
});

// These types are useful if we need the same shape in other backend files.
export type RegisterInput = z.infer<typeof registerSchema>;
export type LoginInput = z.infer<typeof loginSchema>;
26 changes: 26 additions & 0 deletions backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
/* Base Options */
"target": "ES2023", // Node 24 supports almost all ES2023 features natively
"module": "NodeNext", // Required for modern ESM in Node
"moduleResolution": "NodeNext", // Ensures imports work correctly in Node
"lib": ["ES2023"], // No "DOM" here because backend doesn't have a browser window

/* Build Settings */
"outDir": "./dist", // Where the compiled JS goes
"rootDir": "./src", // Where your TS source code lives
"noEmit": false, // MUST be false for backend so we can actually build it

/* Strictness & Safety */
"strict": true, // Enables all strict type-checking options
"noImplicitAny": true, // Prevents accidental "any" types
"esModuleInterop": true, // Allows you to import CommonJS packages (like bcryptjs) easily
"skipLibCheck": true, // Speeds up compilation by skipping type checks of node_modules

/* Cleanliness */
"removeComments": true, // Keeps the production JS files small
"sourceMap": true // Helps you debug the TS code even after it's compiled to JS
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Loading