Workflow/backend/src/routes/auth.routes.ts
root d58d53fa3b feat: initial workflow portal - multi-tenant architecture
- Backend: Node.js + Express + TypeScript + Prisma + PostgreSQL
- Frontend: React + TypeScript + Tailwind CSS + Vite
- Multi-tenant with role-based access (SystemAdmin, Technician, Customer)
- Workflow editor with drag-and-drop steps
- JWT authentication with refresh tokens
- German UI throughout
2026-03-20 22:48:04 +00:00

188 lines
5.0 KiB
TypeScript

import { Router, Request, Response, NextFunction } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { z } from 'zod';
import prisma from '../lib/prisma';
import { authenticate } from '../middleware/auth';
import { AppError } from '../lib/errors';
const router = Router();
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production';
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'change-me-too';
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
// Store for invalidated refresh tokens (in production, use Redis)
const invalidatedTokens = new Set<string>();
const loginSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(1, 'Password is required'),
});
const refreshSchema = z.object({
refreshToken: z.string().min(1, 'Refresh token is required'),
});
function generateTokens(user: { id: string; email: string; role: string; tenantId: string | null; firstName: string; lastName: string }) {
const payload = {
id: user.id,
email: user.email,
role: user.role,
tenantId: user.tenantId,
firstName: user.firstName,
lastName: user.lastName,
};
const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY });
const refreshToken = jwt.sign({ id: user.id, type: 'refresh' }, JWT_REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY });
return { accessToken, refreshToken };
}
// POST /api/auth/login
router.post('/login', async (req: Request, res: Response, next: NextFunction) => {
try {
const parsed = loginSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
error: 'Validation error',
details: parsed.error.flatten().fieldErrors,
});
return;
}
const { email, password } = parsed.data;
const user = await prisma.user.findUnique({
where: { email },
include: { tenant: true },
});
if (!user || !user.active) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
const validPassword = await bcrypt.compare(password, user.passwordHash);
if (!validPassword) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
const tokens = generateTokens(user);
res.json({
...tokens,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
tenantId: user.tenantId,
tenant: user.tenant ? { id: user.tenant.id, name: user.tenant.name, slug: user.tenant.slug } : null,
},
});
} catch (err) {
next(err);
}
});
// POST /api/auth/refresh
router.post('/refresh', async (req: Request, res: Response, next: NextFunction) => {
try {
const parsed = refreshSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
error: 'Validation error',
details: parsed.error.flatten().fieldErrors,
});
return;
}
const { refreshToken } = parsed.data;
if (invalidatedTokens.has(refreshToken)) {
res.status(401).json({ error: 'Token has been invalidated' });
return;
}
let decoded: any;
try {
decoded = jwt.verify(refreshToken, JWT_REFRESH_SECRET);
} catch {
res.status(401).json({ error: 'Invalid refresh token' });
return;
}
if (decoded.type !== 'refresh') {
res.status(401).json({ error: 'Invalid token type' });
return;
}
const user = await prisma.user.findUnique({
where: { id: decoded.id },
});
if (!user || !user.active) {
res.status(401).json({ error: 'User not found or inactive' });
return;
}
// Invalidate old refresh token
invalidatedTokens.add(refreshToken);
const tokens = generateTokens(user);
res.json(tokens);
} catch (err) {
next(err);
}
});
// POST /api/auth/logout
router.post('/logout', authenticate, async (req: Request, res: Response, next: NextFunction) => {
try {
const refreshToken = req.body.refreshToken;
if (refreshToken) {
invalidatedTokens.add(refreshToken);
}
res.json({ message: 'Logged out successfully' });
} catch (err) {
next(err);
}
});
// GET /api/auth/me
router.get('/me', authenticate, async (req: Request, res: Response, next: NextFunction) => {
try {
const user = await prisma.user.findUnique({
where: { id: req.user!.id },
include: { tenant: true },
});
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json({
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
tenantId: user.tenantId,
tenant: user.tenant ? { id: user.tenant.id, name: user.tenant.name, slug: user.tenant.slug, logo: user.tenant.logo } : null,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
});
} catch (err) {
next(err);
}
});
export default router;