Initial push
This commit is contained in:
91
app/api/team/route.ts
Normal file
91
app/api/team/route.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
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 });
|
||||
}
|
||||
Reference in New Issue
Block a user