new user gets added woo hoo

This commit is contained in:
Jun-te Kim 2025-09-08 13:56:33 +00:00
parent c60f09e302
commit 29c57b64fa
3 changed files with 154 additions and 9 deletions

View file

@ -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<typeof bodySchema>;
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 youre 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 cant 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 }
);
}
}

View file

@ -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,

View file

@ -55,9 +55,28 @@ async function updatePortfolioUserRole(
}
}
async function invitePortfolioUser(
portfolioId: string,
email: string,
role: Role,
name: string
): Promise<void> {
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<Role>("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 }) {
</p>
</TableHead>
<TableCell className="flex gap-2 items-center">
<Input
type="text"
placeholder="Full name"
value={inviteName}
onChange={(e) => setInviteName(e.target.value)}
/>
<Input
type="email"
placeholder="email@example.com"
@ -171,9 +207,13 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {
</div>
</TableCell>
<TableCell className="text-right">
<Button className="w-28" onClick={handleInvite} disabled={!inviteEmail}>
Invite
</Button>
<Button
className="w-28"
onClick={handleInvite}
disabled={!inviteEmail || !inviteName || inviteUserMutation.isPending}
>
{inviteUserMutation.isPending ? "Inviting..." : "Invite"}
</Button>
</TableCell>
</TableRow>