mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
new user gets added woo hoo
This commit is contained in:
parent
c60f09e302
commit
29c57b64fa
3 changed files with 154 additions and 9 deletions
|
|
@ -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 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue