import { NextResponse } from "next/server" import { and, desc, eq, ne } from "drizzle-orm" import { z } from "zod" import { db } from "@/lib/db" import { account_members, profiles } from "@/lib/db/schema" import { getSessionUser } from "@/lib/session" import { getAccountContext } from "@/lib/account" import { PLAN_LIMITS } from "@/lib/stripe/plans" import { sendEmail, teamInviteHtml } from "@/lib/email/send" import type { Plan } from "@/types" const inviteSchema = z.object({ email: z.string().email("Enter a valid email address").max(254), role: z.enum(["member", "viewer"]).default("member"), }) const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000" /** * GET — list the members the current OWNER has invited to their account. * Team management is always scoped to the real session user acting as owner * (never getEffectiveOwnerId), so a member can't manage the owner's team. */ export async function GET() { const user = await getSessionUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) const members = await db .select({ id: account_members.id, email: account_members.email, role: account_members.role, status: account_members.status, member_id: account_members.member_id, accepted_at: account_members.accepted_at, created_at: account_members.created_at, }) .from(account_members) .where(and(eq(account_members.owner_id, user.id), ne(account_members.status, "revoked"))) .orderBy(desc(account_members.created_at)) return NextResponse.json(members) } /** * POST — invite a user (by email) to the current owner's account. Gated to * plans with team access; only the account owner may invite. */ export async function POST(request: Request) { const user = await getSessionUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) // Only an account OWNER can manage a team — a member of someone else's // account must not be able to invite people into that account. const ctx = await getAccountContext(user.id) if (!ctx.isOwner) { return NextResponse.json( { error: "Only the account owner can manage the team" }, { status: 403 } ) } // Gate on the owner's plan. const profile = await db.query.profiles.findFirst({ where: eq(profiles.id, user.id), columns: { plan: true, email: true }, }) const plan = (profile?.plan ?? "starter") as Plan if (!PLAN_LIMITS[plan].hasTeamAccess) { return NextResponse.json( { error: "Team access is available on the Landlord and Lifetime plans" }, { status: 403 } ) } const body = await request.json().catch(() => null) const parsed = inviteSchema.safeParse(body) if (!parsed.success) { return NextResponse.json( { error: parsed.error.issues[0]?.message ?? "Invalid input" }, { status: 400 } ) } const email = parsed.data.email.trim().toLowerCase() const { role } = parsed.data // Can't invite yourself. if (email === (profile?.email ?? user.email ?? "").toLowerCase()) { return NextResponse.json({ error: "You can't invite yourself" }, { status: 400 }) } // Prevent a duplicate pending/active invite for the same email. const existing = await db.query.account_members.findFirst({ where: and(eq(account_members.owner_id, user.id), eq(account_members.email, email)), }) if (existing && (existing.status === "pending" || existing.status === "active")) { return NextResponse.json( { error: existing.status === "active" ? "That person is already a member of your account" : "An invite is already pending for that email", }, { status: 409 } ) } let inviteToken: string try { // Re-invite a previously revoked email by inserting a fresh pending row. const [row] = await db .insert(account_members) .values({ owner_id: user.id, email, role, status: "pending" }) .returning({ invite_token: account_members.invite_token }) inviteToken = row.invite_token } catch (e) { return NextResponse.json({ error: (e as Error).message }, { status: 500 }) } const inviteUrl = `${APP_URL}/team/accept/${inviteToken}` const inviterName = profile?.email ?? user.email ?? "A landlord" await sendEmail({ to: email, subject: "You've been invited to a Property Management Network account", html: teamInviteHtml({ inviterName, inviteUrl, role }), }) return NextResponse.json({ ok: true }) }