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)}
+ />
-
+