diff --git a/src/app/api/portfolio/[portfolioId]/colloborators/route.ts b/src/app/api/portfolio/[portfolioId]/colloborators/route.ts index 5420c4c..b48c61f 100644 --- a/src/app/api/portfolio/[portfolioId]/colloborators/route.ts +++ b/src/app/api/portfolio/[portfolioId]/colloborators/route.ts @@ -65,6 +65,69 @@ export async function GET( } } +// DELETE: remove a collaborator from this portfolio. +export async function DELETE( + req: NextRequest, + props: { params: Promise<{ portfolioId: string }> } +) { + const { portfolioId } = await props.params; + + const session = await getServerSession(AuthOptions); + if (!session?.user?.dbId) { + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + } + + const bodySchema = z.object({ portfolioUserId: z.string() }); + let body: z.infer; + try { + body = bodySchema.parse(await req.json()); + } catch { + return NextResponse.json({ error: "Invalid body" }, { status: 400 }); + } + + try { + const pId = BigInt(portfolioId); + const puId = BigInt(body.portfolioUserId); + + // Refuse to remove the creator — they own the portfolio. + const [target] = await db + .select({ id: portfolioUsers.id, role: portfolioUsers.role }) + .from(portfolioUsers) + .where( + and( + eq(portfolioUsers.id, puId), + eq(portfolioUsers.portfolioId, pId), + ), + ) + .limit(1); + if (!target) { + return NextResponse.json( + { error: "Membership not found in this portfolio" }, + { status: 404 }, + ); + } + if (target.role === "creator") { + return NextResponse.json( + { error: "Cannot remove the portfolio creator" }, + { status: 400 }, + ); + } + + await db.delete(portfolioUsers).where(eq(portfolioUsers.id, puId)); + + return NextResponse.json( + { success: true, portfolioUserId: body.portfolioUserId }, + { status: 200 }, + ); + } catch (err) { + console.error("DELETE /colloborators error:", err); + return NextResponse.json( + { error: "Failed to remove user" }, + { status: 500 }, + ); + } +} + // PUT: update a collaborator’s role export async function PUT( req: NextRequest, diff --git a/src/app/lib/email.test.ts b/src/app/lib/email.test.ts new file mode 100644 index 0000000..e387a57 --- /dev/null +++ b/src/app/lib/email.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { normaliseEmail } from "./email"; + +describe("normaliseEmail", () => { + it("lowercases mixed-case addresses", () => { + expect(normaliseEmail("Craig.Williams@Example.com")).toBe( + "craig.williams@example.com", + ); + }); + + it("trims surrounding whitespace (common from copy-paste into invite forms)", () => { + expect(normaliseEmail(" user@example.com ")).toBe("user@example.com"); + expect(normaliseEmail("\tuser@example.com\n")).toBe("user@example.com"); + }); +}); diff --git a/src/app/lib/email.ts b/src/app/lib/email.ts new file mode 100644 index 0000000..4cb5d73 --- /dev/null +++ b/src/app/lib/email.ts @@ -0,0 +1,3 @@ +export function normaliseEmail(email: string): string { + return email.trim().toLowerCase(); +}