92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { auth } from "@/lib/auth";
|
|
import { db } from "@/lib/db";
|
|
import { users, invitations } from "@/lib/db/schema";
|
|
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
|
|
import { sendInvitationEmail } from "@/lib/email";
|
|
import { z } from "zod";
|
|
import { nanoid } from "nanoid";
|
|
import { eq, ne } from "drizzle-orm";
|
|
|
|
const InviteSchema = z.object({
|
|
email: z.string().email().max(254),
|
|
role: z.enum(["admin", "moderator"]),
|
|
playerUuid: z.string().optional(),
|
|
});
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const session = await auth.api.getSession({ headers: req.headers });
|
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
if (session.user.role !== "superadmin") {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
const teamUsers = await db
|
|
.select()
|
|
.from(users)
|
|
.where(ne(users.id, session.user.id));
|
|
|
|
const pendingInvites = await db
|
|
.select()
|
|
.from(invitations)
|
|
.where(eq(invitations.acceptedAt, null as unknown as number));
|
|
|
|
return NextResponse.json({ users: teamUsers, pendingInvites });
|
|
}
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const session = await auth.api.getSession({ headers: req.headers });
|
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
if (session.user.role !== "superadmin") {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
const ip = getClientIp(req);
|
|
const { allowed } = checkRateLimit(ip, 10);
|
|
if (!allowed) return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
|
|
|
let body: z.infer<typeof InviteSchema>;
|
|
try {
|
|
body = InviteSchema.parse(await req.json());
|
|
} catch {
|
|
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
|
|
}
|
|
|
|
// Check if user already exists
|
|
const existing = await db.select().from(users).where(eq(users.email, body.email)).get();
|
|
if (existing) {
|
|
return NextResponse.json({ error: "User already exists" }, { status: 409 });
|
|
}
|
|
|
|
// Create invitation
|
|
const token = nanoid(48);
|
|
const expiresAt = Date.now() + 48 * 60 * 60 * 1000; // 48 hours
|
|
|
|
await db.insert(invitations).values({
|
|
id: nanoid(),
|
|
email: body.email,
|
|
role: body.role,
|
|
invitedBy: session.user.id,
|
|
token,
|
|
expiresAt,
|
|
createdAt: Date.now(),
|
|
});
|
|
|
|
const baseUrl = process.env.BETTER_AUTH_URL ?? "http://localhost:3000";
|
|
const inviteUrl = `${baseUrl}/accept-invite?token=${token}`;
|
|
|
|
try {
|
|
await sendInvitationEmail({
|
|
to: body.email,
|
|
invitedByName: session.user.name ?? "An admin",
|
|
inviteUrl,
|
|
role: body.role,
|
|
});
|
|
} catch (err) {
|
|
// Log but don't fail — admin can resend
|
|
console.error("[Team] Failed to send invitation email:", err);
|
|
}
|
|
|
|
return NextResponse.json({ success: true, inviteUrl }, { status: 201 });
|
|
}
|