From 29c57b64faf7a3e6233854c704f8a9c2c35a06cc Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 8 Sep 2025 13:56:33 +0000 Subject: [PATCH] new user gets added woo hoo --- .../[portfolioId]/colloborators/route.ts | 105 ++++++++++++++++++ src/app/db/schema/users.ts | 2 +- .../settings/UsersPermissionsCard.tsx | 56 ++++++++-- 3 files changed, 154 insertions(+), 9 deletions(-) diff --git a/src/app/api/portfolio/[portfolioId]/colloborators/route.ts b/src/app/api/portfolio/[portfolioId]/colloborators/route.ts index 3e7d6a7..7a90192 100644 --- a/src/app/api/portfolio/[portfolioId]/colloborators/route.ts +++ b/src/app/api/portfolio/[portfolioId]/colloborators/route.ts @@ -97,4 +97,109 @@ export async function PUT( { status: 500 } ); } +} + +// POST: invite a user by email (find-or-create user, then add to portfolio with role) +export async function POST( + req: NextRequest, + props: { params: Promise<{ portfolioId: string }> } +) { + const { portfolioId } = await props.params; + + // 1) Validate payload + const bodySchema = z.object({ + email: z.string().email(), + role: z.enum(ROLE_OPTIONS), + name: 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); + + // 2) Find or create the user by email + // Try to find existing user + let existing = await db + .select({ id: user.id, firstName: user.firstName, email: user.email }) + .from(user) + .where(eq(user.email, body.email)) + .limit(1); + + let createdUserId: bigint | null = existing[0]?.id ?? null; + + // If not found, create. Prefer Postgres upsert to avoid race. + if (!createdUserId) { + // If you’re on Postgres, this is ideal: + const inserted = await db + .insert(user) + .values({ email: body.email, firstName: body.name, oauthProvider: "credentials" }) + .onConflictDoNothing() // relies on a UNIQUE(email) constraint + .returning({ id: user.id }); + + if (inserted.length > 0) { + createdUserId = inserted[0].id; + } else { + // Someone else created the user concurrently; fetch it + const fetched = await db + .select({ id: user.id }) + .from(user) + .where(eq(user.email, body.email)) + .limit(1); + if (!fetched[0]) { + return NextResponse.json( + { error: "Failed to create or fetch user" }, + { status: 500 } + ); + } + createdUserId = fetched[0].id; + } + } + + // 3) Link user to portfolio with role (upsert) + // Assumes a UNIQUE index on (portfolioId, userId) in portfolioUsers. + const linkResult = await db + .insert(portfolioUsers) + .values({ + portfolioId: pId, + userId: createdUserId!, + role: body.role, + }) + .returning({ + portfolioUserId: portfolioUsers.id, + userId: portfolioUsers.userId, + role: portfolioUsers.role, + }); + + const row = linkResult[0]; + if (!row) { + return NextResponse.json( + { error: "Failed to create portfolio user" }, + { status: 500 } + ); + } + + const collaborator = { + portfolioUserId: row.portfolioUserId?.toString() ?? null, + userId: row.userId?.toString() ?? null, + role: row.role, + name: body.name ?? null, + email: body.email, + }; + + // 201 if it was a new link, 200 if it was an update — we can’t easily + // tell from .onConflictDoUpdate return, so just use 200 OK. + return NextResponse.json({ user: collaborator }, { status: 200 }); + } catch (err) { + console.error("POST /colloborators error:", err); + return NextResponse.json( + { error: "Failed to invite user" }, + { status: 500 } + ); + } } \ No newline at end of file diff --git a/src/app/db/schema/users.ts b/src/app/db/schema/users.ts index dbc96d7..372a678 100644 --- a/src/app/db/schema/users.ts +++ b/src/app/db/schema/users.ts @@ -7,7 +7,7 @@ export const user = pgTable("user", { // At the moment, Drizzle doesn't support unique constraints email: text("email").notNull(), oauthId: text("oauth_id"), - oauthProvider: text("oauth_provider").$type<"google">(), + oauthProvider: text("oauth_provider").$type<"google" | "credentials">(), // role: text("role").$type<"admin" | "write" | "read">(), createdAt: timestamp("created_at", { precision: 6, diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx index 387bf1f..2e282fa 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx @@ -55,9 +55,28 @@ async function updatePortfolioUserRole( } } +async function invitePortfolioUser( + portfolioId: string, + email: string, + role: Role, + name: string +): Promise { + const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, role, name }), + }); + if (!res.ok) { + const msg = await res.text().catch(() => ""); + throw new Error(msg || "Failed to invite user"); + } +} + + export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { const [inviteEmail, setInviteEmail] = useState(""); const [inviteRole, setInviteRole] = useState("read"); + const [inviteName, setInviteName] = useState(""); const queryClient = useQueryClient(); @@ -115,18 +134,29 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { }, }); + // ADD: mutation for inviting a user + const inviteUserMutation = useMutation({ + mutationFn: ({ email, role, name }: { email: string; role: Role; name: string | null }) => + invitePortfolioUser(portfolioId, email, role, name), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["portfolioUsers", portfolioId] }); + setInviteEmail(""); + setInviteName(""); // clear name after success + // setInviteRole("read"); + }, + onError: (err) => { + console.error("Invite failed:", err); + }, + }); + function handleInvite() { - console.log("Inivte email", inviteEmail); - console.log("Inivte Role", inviteRole); - // TODO: POST invite -> then refetch() + inviteUserMutation.mutate({ email: inviteEmail, role: inviteRole, name: inviteName || null }); } function onChangeRole(portfolioUserId: string, role: Role) { console.log(`Change portfolioUserId ${portfolioUserId} to ${role}`); changeRoleMutation.mutate({ portfolioUserId, role }); - - // TODO: PATCH role -> then refetch() } function onRemove(portfolioUserId: string) { @@ -160,6 +190,12 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {

+ setInviteName(e.target.value)} + /> - +