94 lines
2.6 KiB
TypeScript
94 lines
2.6 KiB
TypeScript
import { Router } from "express";
|
|
import bcrypt from "bcryptjs";
|
|
import jwt from "jsonwebtoken";
|
|
import { z } from "zod";
|
|
|
|
import { prisma } from "../db";
|
|
|
|
export const authRouter = Router();
|
|
|
|
const registerSchema = z.object({
|
|
username: z.string().min(3),
|
|
password: z.string().min(6),
|
|
inviteCode: z.string().min(4),
|
|
});
|
|
|
|
authRouter.post("/register", async (req, res) => {
|
|
const parsed = registerSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
return res.status(400).json({ error: "Invalid payload" });
|
|
}
|
|
|
|
const { username, password, inviteCode } = parsed.data;
|
|
const now = new Date();
|
|
|
|
try {
|
|
const result = await prisma.$transaction(async (tx) => {
|
|
const invite = await tx.invite.findFirst({
|
|
where: {
|
|
code: inviteCode,
|
|
revokedAt: null,
|
|
expiresAt: { gt: now },
|
|
},
|
|
});
|
|
if (!invite || invite.usedCount >= invite.maxUses) {
|
|
throw new Error("Invalid invite");
|
|
}
|
|
|
|
const existing = await tx.user.findUnique({ where: { username } });
|
|
if (existing) {
|
|
throw new Error("Username taken");
|
|
}
|
|
|
|
const passwordHash = await bcrypt.hash(password, 10);
|
|
const user = await tx.user.create({
|
|
data: { username, passwordHash },
|
|
});
|
|
|
|
await tx.invite.update({
|
|
where: { id: invite.id },
|
|
data: { usedCount: invite.usedCount + 1 },
|
|
});
|
|
|
|
return user;
|
|
});
|
|
|
|
const token = jwt.sign({ userId: result.id }, process.env.JWT_SECRET || "dev-secret", {
|
|
expiresIn: "7d",
|
|
});
|
|
return res.json({ token, user: { id: result.id, username: result.username } });
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : "Register failed";
|
|
const status = message === "Invalid invite" ? 400 : 409;
|
|
return res.status(status).json({ error: message });
|
|
}
|
|
});
|
|
|
|
const loginSchema = z.object({
|
|
username: z.string().min(3),
|
|
password: z.string().min(6),
|
|
});
|
|
|
|
authRouter.post("/login", async (req, res) => {
|
|
const parsed = loginSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
return res.status(400).json({ error: "Invalid payload" });
|
|
}
|
|
|
|
const { username, password } = parsed.data;
|
|
const user = await prisma.user.findUnique({ where: { username } });
|
|
if (!user) {
|
|
return res.status(401).json({ error: "Invalid credentials" });
|
|
}
|
|
|
|
const ok = await bcrypt.compare(password, user.passwordHash);
|
|
if (!ok) {
|
|
return res.status(401).json({ error: "Invalid credentials" });
|
|
}
|
|
|
|
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET || "dev-secret", {
|
|
expiresIn: "7d",
|
|
});
|
|
return res.json({ token, user: { id: user.id, username: user.username } });
|
|
});
|