Complete project scaffold with working auth, REST API, Prisma/SQLite schema, Docker config, and React frontend for both Rack Planner and Service Mapper modules. Both server and client pass TypeScript strict mode with zero errors. Initial migration applied. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
import { Router, Request, Response, NextFunction } from 'express';
|
|
import bcrypt from 'bcryptjs';
|
|
import jwt from 'jsonwebtoken';
|
|
import { AppError, ok } from '../types/index';
|
|
import { authMiddleware } from '../middleware/authMiddleware';
|
|
|
|
export const authRouter = Router();
|
|
|
|
const COOKIE_OPTS = {
|
|
httpOnly: true,
|
|
sameSite: 'strict' as const,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
path: '/',
|
|
};
|
|
|
|
authRouter.post('/login', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const { username, password } = req.body as { username?: string; password?: string };
|
|
|
|
if (!username || !password) {
|
|
throw new AppError('Username and password are required', 400, 'MISSING_FIELDS');
|
|
}
|
|
|
|
const adminUsername = process.env.ADMIN_USERNAME;
|
|
const adminHash = process.env.ADMIN_PASSWORD_HASH;
|
|
|
|
if (!adminUsername || !adminHash) {
|
|
throw new AppError('Server not configured: admin credentials missing', 500, 'CONFIG_ERROR');
|
|
}
|
|
|
|
const usernameMatch = username === adminUsername;
|
|
// Always run bcrypt to prevent timing attacks even if username is wrong
|
|
const passwordMatch = await bcrypt.compare(password, adminHash);
|
|
|
|
if (!usernameMatch || !passwordMatch) {
|
|
throw new AppError('Invalid username or password', 401, 'INVALID_CREDENTIALS');
|
|
}
|
|
|
|
const secret = process.env.JWT_SECRET;
|
|
if (!secret) throw new AppError('Server not configured: JWT_SECRET missing', 500, 'CONFIG_ERROR');
|
|
|
|
const token = jwt.sign({ sub: 'admin' }, secret, {
|
|
expiresIn: (process.env.JWT_EXPIRY ?? '8h') as jwt.SignOptions['expiresIn'],
|
|
});
|
|
|
|
res.cookie('token', token, COOKIE_OPTS);
|
|
res.json(ok({ success: true }));
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
});
|
|
|
|
authRouter.post('/logout', (_req: Request, res: Response) => {
|
|
res.clearCookie('token', COOKIE_OPTS);
|
|
res.json(ok({ success: true }));
|
|
});
|
|
|
|
authRouter.get('/me', authMiddleware, (_req: Request, res: Response) => {
|
|
res.json(ok({ authenticated: true }));
|
|
});
|