Files
pos/server/src/routes/auth.ts

199 lines
5.5 KiB
TypeScript
Raw Normal View History

import { Router, Request, Response, NextFunction } from "express";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { z } from "zod";
import { prisma } from "../lib/prisma.js";
import { AppError } from "../middleware/errorHandler.js";
import { requireAuth } from "../middleware/auth.js";
import { AuthenticatedRequest } from "../types/index.js";
const router = Router();
const ACCESS_TOKEN_TTL = "15m";
const REFRESH_TOKEN_TTL = "7d";
const REFRESH_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
function signAccessToken(payload: object): string {
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error("JWT_SECRET not configured");
return jwt.sign(payload, secret, { expiresIn: ACCESS_TOKEN_TTL });
}
function signRefreshToken(payload: object): string {
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error("JWT_SECRET not configured");
return jwt.sign(payload, secret, { expiresIn: REFRESH_TOKEN_TTL });
}
// POST /api/v1/auth/login
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
router.post(
"/login",
async (req: Request, res: Response, next: NextFunction) => {
try {
const body = LoginSchema.parse(req.body);
const user = await prisma.user.findUnique({
where: { email: body.email },
include: { role: true, vendor: true },
});
if (!user || !(await bcrypt.compare(body.password, user.passwordHash))) {
throw new AppError(401, "INVALID_CREDENTIALS", "Invalid email or password");
}
const tokenPayload = {
userId: user.id,
vendorId: user.vendorId,
roleId: user.roleId,
roleName: user.role.name,
};
const accessToken = signAccessToken(tokenPayload);
const refreshToken = signRefreshToken({ userId: user.id });
await prisma.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS),
},
});
res.json({
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role.name,
vendorId: user.vendorId,
vendorName: user.vendor.name,
},
});
} catch (err) {
next(err);
}
}
);
// POST /api/v1/auth/refresh
const RefreshSchema = z.object({
refreshToken: z.string().min(1),
});
router.post(
"/refresh",
async (req: Request, res: Response, next: NextFunction) => {
try {
const { refreshToken } = RefreshSchema.parse(req.body);
const secret = process.env.JWT_SECRET;
if (!secret) throw new AppError(500, "CONFIG_ERROR", "JWT secret not configured");
let decoded: { userId: string };
try {
decoded = jwt.verify(refreshToken, secret) as { userId: string };
} catch {
throw new AppError(401, "INVALID_TOKEN", "Invalid or expired refresh token");
}
const stored = await prisma.refreshToken.findUnique({
where: { token: refreshToken },
include: { user: { include: { role: true } } },
});
if (!stored || stored.userId !== decoded.userId || stored.expiresAt < new Date()) {
throw new AppError(401, "INVALID_TOKEN", "Refresh token not found or expired");
}
// Rotate refresh token
await prisma.refreshToken.delete({ where: { id: stored.id } });
const user = stored.user;
const tokenPayload = {
userId: user.id,
vendorId: user.vendorId,
roleId: user.roleId,
roleName: user.role.name,
};
const newAccessToken = signAccessToken(tokenPayload);
const newRefreshToken = signRefreshToken({ userId: user.id });
await prisma.refreshToken.create({
data: {
token: newRefreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS),
},
});
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
} catch (err) {
next(err);
}
}
);
// POST /api/v1/auth/logout
router.post(
"/logout",
requireAuth as unknown as (req: Request, res: Response, next: NextFunction) => void,
async (req: Request, res: Response, next: NextFunction) => {
try {
const authReq = req as AuthenticatedRequest;
const { refreshToken } = z.object({ refreshToken: z.string().optional() }).parse(req.body);
if (refreshToken) {
await prisma.refreshToken.deleteMany({
where: { token: refreshToken, userId: authReq.auth.userId },
});
} else {
// Logout all devices
await prisma.refreshToken.deleteMany({
where: { userId: authReq.auth.userId },
});
}
res.json({ message: "Logged out successfully" });
} catch (err) {
next(err);
}
}
);
// GET /api/v1/auth/me
router.get(
"/me",
requireAuth as unknown as (req: Request, res: Response, next: NextFunction) => void,
async (req: Request, res: Response, next: NextFunction) => {
try {
const authReq = req as AuthenticatedRequest;
const user = await prisma.user.findUnique({
where: { id: authReq.auth.userId },
include: { role: true, vendor: true },
});
if (!user) throw new AppError(404, "NOT_FOUND", "User not found");
res.json({
id: user.id,
email: user.email,
name: user.name,
role: user.role.name,
vendorId: user.vendorId,
vendorName: user.vendor.name,
});
} catch (err) {
next(err);
}
}
);
export default router;