feat: add authentication routes, update environment variables, and enhance error handling

This commit is contained in:
2025-12-07 13:47:00 +00:00
parent 08aefd7cbe
commit df2b9e56b4
9 changed files with 261 additions and 4 deletions

View File

@@ -1 +1,4 @@
DATABASE_URL=postgres://username:password@localhost:5432/database_name
JWT_SECRET=replace_with_a_long_random_secret
JWT_EXPIRES_IN=7d
PORT=3000

View File

@@ -18,6 +18,9 @@ Set:
```bash
DATABASE_URL=postgres://username:password@localhost:5432/database_name
JWT_SECRET=replace_with_a_long_random_secret
JWT_EXPIRES_IN=7d
PORT=3000
```
## Run app
@@ -45,3 +48,33 @@ Open Drizzle Studio:
```bash
bun run db:studio
```
## Auth API
Register:
```bash
POST /auth/register
{
"email": "user@example.com",
"password": "password123",
"name": "User Name"
}
```
Login:
```bash
POST /auth/login
{
"email": "user@example.com",
"password": "password123"
}
```
Get current user:
```bash
GET /auth/me
Authorization: Bearer <token>
```

View File

@@ -4,5 +4,6 @@ export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 255 }).notNull(),
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});

View File

@@ -1,11 +1,25 @@
import 'dotenv/config';
import express from 'express';
import authRoutes from './routes/auth';
const app = express();
app.get('/', (req, res) => {
res.send('Hello World');
app.use(express.json());
app.get('/', (_req, res) => {
res.send('API is running');
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
app.use('/auth', authRoutes);
app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error(err);
res.status(500).json({ message: 'Internal server error' });
});
const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

View File

@@ -0,0 +1,22 @@
import type { NextFunction, Request, Response } from 'express';
import { verifyAccessToken } from '../utils/jwt';
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
const authorization = req.headers.authorization;
if (!authorization?.startsWith('Bearer ')) {
res.status(401).json({ message: 'Missing or invalid authorization header' });
return;
}
const token = authorization.slice(7);
try {
const payload = verifyAccessToken(token);
req.user = payload;
next();
} catch {
res.status(401).json({ message: 'Invalid or expired token' });
}
}

134
Backend/routes/auth.ts Normal file
View File

@@ -0,0 +1,134 @@
import { Router } from 'express';
import { and, eq } from 'drizzle-orm';
import { z } from 'zod';
import { db } from '../db/client';
import { users } from '../db/schema';
import { requireAuth } from '../middleware/auth';
import { signAccessToken } from '../utils/jwt';
import { hashPassword, verifyPassword } from '../utils/password';
const router = Router();
const registerSchema = z.object({
email: z.email().trim().toLowerCase(),
password: z.string().min(8),
name: z.string().trim().min(1).max(255),
});
const loginSchema = z.object({
email: z.email().trim().toLowerCase(),
password: z.string().min(1),
});
router.post('/register', async (req, res) => {
const parsed = registerSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() });
return;
}
const { email, password, name } = parsed.data;
const existingUser = await db.query.users.findFirst({
where: eq(users.email, email),
});
if (existingUser) {
res.status(409).json({ message: 'Email already in use' });
return;
}
const passwordHash = await hashPassword(password);
const [newUser] = await db
.insert(users)
.values({
email,
name,
passwordHash,
})
.returning({
id: users.id,
email: users.email,
name: users.name,
createdAt: users.createdAt,
});
if (!newUser) {
res.status(500).json({ message: 'Failed to create user' });
return;
}
const token = signAccessToken({ userId: newUser.id, email: newUser.email });
res.status(201).json({ token, user: newUser });
});
router.post('/login', async (req, res) => {
const parsed = loginSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() });
return;
}
const { email, password } = parsed.data;
const user = await db.query.users.findFirst({
where: eq(users.email, email),
});
if (!user) {
res.status(401).json({ message: 'Invalid email or password' });
return;
}
const isPasswordValid = await verifyPassword(password, user.passwordHash);
if (!isPasswordValid) {
res.status(401).json({ message: 'Invalid email or password' });
return;
}
const token = signAccessToken({ userId: user.id, email: user.email });
res.json({
token,
user: {
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt,
},
});
});
router.get('/me', requireAuth, async (req, res) => {
const authenticatedUser = req.user;
if (!authenticatedUser) {
res.status(401).json({ message: 'Unauthorized' });
return;
}
const user = await db.query.users.findFirst({
where: and(eq(users.id, authenticatedUser.userId), eq(users.email, authenticatedUser.email)),
columns: {
id: true,
email: true,
name: true,
createdAt: true,
},
});
if (!user) {
res.status(404).json({ message: 'User not found' });
return;
}
res.json({ user });
});
export default router;

11
Backend/types/express.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import type { JwtPayload } from '../utils/jwt';
declare global {
namespace Express {
interface Request {
user?: JwtPayload;
}
}
}
export {};

28
Backend/utils/jwt.ts Normal file
View File

@@ -0,0 +1,28 @@
import jwt from 'jsonwebtoken';
const JWT_EXPIRES_IN = (process.env.JWT_EXPIRES_IN ?? '7d') as jwt.SignOptions['expiresIn'];
function getJwtSecret(): string {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error('JWT_SECRET is not set');
}
return secret;
}
export type JwtPayload = {
userId: string;
email: string;
};
export function signAccessToken(payload: JwtPayload): string {
return jwt.sign(payload, getJwtSecret(), {
expiresIn: JWT_EXPIRES_IN,
});
}
export function verifyAccessToken(token: string): JwtPayload {
return jwt.verify(token, getJwtSecret()) as JwtPayload;
}

11
Backend/utils/password.ts Normal file
View File

@@ -0,0 +1,11 @@
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}