mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-19 08:48:34 +02:00
show owner in users table, list roles query in invite form, and more
This commit is contained in:
parent
458de04fcf
commit
9c2e481d2b
13 changed files with 145 additions and 81 deletions
|
@ -1,6 +1,6 @@
|
||||||
// import { orgs, sites, resources, exitNodes, targets } from "@server/db/schema";
|
// import { orgs, sites, resources, exitNodes, targets } from "@server/db/schema";
|
||||||
// import db from "@server/db";
|
// import db from "@server/db";
|
||||||
// import { crateAdminRole } from "@server/db/ensureActions";
|
// import { createAdminRole } from "@server/db/ensureActions";
|
||||||
|
|
||||||
// async function insertDummyData() {
|
// async function insertDummyData() {
|
||||||
// const org1 = db
|
// const org1 = db
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
// .returning()
|
// .returning()
|
||||||
// .get();
|
// .get();
|
||||||
|
|
||||||
// await crateAdminRole(org1.orgId!);
|
// await createAdminRole(org1.orgId!);
|
||||||
|
|
||||||
// // Insert dummy exit nodes
|
// // Insert dummy exit nodes
|
||||||
// const exitNode1 = db
|
// const exitNode1 = db
|
||||||
|
|
|
@ -37,7 +37,7 @@ export const SendInviteLink = ({
|
||||||
<Body className="font-sans">
|
<Body className="font-sans">
|
||||||
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8">
|
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8">
|
||||||
<Heading className="text-2xl font-semibold text-gray-800 text-center">
|
<Heading className="text-2xl font-semibold text-gray-800 text-center">
|
||||||
You're invite to join a Fossorial organization
|
You're invited to join a Fossorial organization
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text className="text-base text-gray-700 mt-4">
|
<Text className="text-base text-gray-700 mt-4">
|
||||||
Hi {email || "there"},
|
Hi {email || "there"},
|
||||||
|
@ -45,12 +45,15 @@ export const SendInviteLink = ({
|
||||||
<Text className="text-base text-gray-700 mt-2">
|
<Text className="text-base text-gray-700 mt-2">
|
||||||
You’ve been invited to join the organization{" "}
|
You’ve been invited to join the organization{" "}
|
||||||
{orgName}
|
{orgName}
|
||||||
{inviterName ? ` by ${inviterName}.` : ""}. Please
|
{inviterName ? ` by ${inviterName}.` : "."} Please
|
||||||
access the link below to accept the invite.
|
access the link below to accept the invite.
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-base text-gray-700 mt-2">
|
<Text className="text-base text-gray-700 mt-2">
|
||||||
This invite will expire in{" "}
|
This invite will expire in{" "}
|
||||||
<b>{expiresInDays} days.</b>
|
<b>
|
||||||
|
{expiresInDays}{" "}
|
||||||
|
{expiresInDays === "1" ? "day" : "days"}.
|
||||||
|
</b>
|
||||||
</Text>
|
</Text>
|
||||||
<Section className="text-center my-6">
|
<Section className="text-center my-6">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -199,12 +199,12 @@ authenticated.delete(
|
||||||
// verifyUserHasAction(ActionsEnum.createRole),
|
// verifyUserHasAction(ActionsEnum.createRole),
|
||||||
// role.createRole
|
// role.createRole
|
||||||
// );
|
// );
|
||||||
// authenticated.get(
|
authenticated.get(
|
||||||
// "/org/:orgId/roles",
|
"/org/:orgId/roles",
|
||||||
// verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
// verifyUserHasAction(ActionsEnum.listRoles),
|
verifyUserHasAction(ActionsEnum.listRoles),
|
||||||
// role.listRoles
|
role.listRoles
|
||||||
// );
|
);
|
||||||
// authenticated.get(
|
// authenticated.get(
|
||||||
// "/role/:roleId",
|
// "/role/:roleId",
|
||||||
// verifyRoleAccess,
|
// verifyRoleAccess,
|
||||||
|
|
|
@ -99,6 +99,7 @@ export async function createOrg(
|
||||||
userId: req.user!.userId,
|
userId: req.user!.userId,
|
||||||
orgId: newOrg[0].orgId,
|
orgId: newOrg[0].orgId,
|
||||||
roleId: roleId,
|
roleId: roleId,
|
||||||
|
isOwner: true,
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import createHttpError from "http-errors";
|
||||||
import { sql, eq } from "drizzle-orm";
|
import { sql, eq } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import stoi from "@server/utils/stoi";
|
||||||
|
|
||||||
const listRolesParamsSchema = z.object({
|
const listRolesParamsSchema = z.object({
|
||||||
orgId: z.string(),
|
orgId: z.string(),
|
||||||
|
@ -17,20 +18,43 @@ const listRolesSchema = z.object({
|
||||||
limit: z
|
limit: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
.default("1000")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.number().int().positive().default(10)),
|
.pipe(z.number().int().nonnegative()),
|
||||||
offset: z
|
offset: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
.default("0")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.number().int().nonnegative().default(0)),
|
.pipe(z.number().int().nonnegative()),
|
||||||
orgId: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().positive()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function queryRoles(orgId: string, limit: number, offset: number) {
|
||||||
|
return await db
|
||||||
|
.select({
|
||||||
|
roleId: roles.roleId,
|
||||||
|
orgId: roles.orgId,
|
||||||
|
isAdmin: roles.isAdmin,
|
||||||
|
name: roles.name,
|
||||||
|
description: roles.description,
|
||||||
|
orgName: orgs.name,
|
||||||
|
})
|
||||||
|
.from(roles)
|
||||||
|
.leftJoin(orgs, eq(roles.orgId, orgs.orgId))
|
||||||
|
.where(eq(roles.orgId, orgId))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ListRolesResponse = {
|
||||||
|
roles: NonNullable<Awaited<ReturnType<typeof queryRoles>>>;
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export async function listRoles(
|
export async function listRoles(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
@ -61,25 +85,12 @@ export async function listRoles(
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
let baseQuery: any = db
|
|
||||||
.select({
|
|
||||||
roleId: roles.roleId,
|
|
||||||
orgId: roles.orgId,
|
|
||||||
isAdmin: roles.isAdmin,
|
|
||||||
name: roles.name,
|
|
||||||
description: roles.description,
|
|
||||||
orgName: orgs.name,
|
|
||||||
})
|
|
||||||
.from(roles)
|
|
||||||
.leftJoin(orgs, eq(roles.orgId, orgs.orgId))
|
|
||||||
.where(eq(roles.orgId, orgId));
|
|
||||||
|
|
||||||
let countQuery: any = db
|
let countQuery: any = db
|
||||||
.select({ count: sql<number>`cast(count(*) as integer)` })
|
.select({ count: sql<number>`cast(count(*) as integer)` })
|
||||||
.from(roles)
|
.from(roles)
|
||||||
.where(eq(roles.orgId, orgId));
|
.where(eq(roles.orgId, orgId));
|
||||||
|
|
||||||
const rolesList = await baseQuery.limit(limit).offset(offset);
|
const rolesList = await queryRoles(orgId, limit, offset);
|
||||||
const totalCountResult = await countQuery;
|
const totalCountResult = await countQuery;
|
||||||
const totalCount = totalCountResult[0].count;
|
const totalCount = totalCountResult[0].count;
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ export async function inviteUser(
|
||||||
email,
|
email,
|
||||||
inviteLink,
|
inviteLink,
|
||||||
expiresInDays: (validHours / 24).toString(),
|
expiresInDays: (validHours / 24).toString(),
|
||||||
orgName: orgId,
|
orgName: org[0].name || orgId,
|
||||||
inviterName: req.user?.email,
|
inviterName: req.user?.email,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|
|
@ -37,6 +37,7 @@ async function queryUsers(orgId: string, limit: number, offset: number) {
|
||||||
orgId: userOrgs.orgId,
|
orgId: userOrgs.orgId,
|
||||||
roleId: userOrgs.roleId,
|
roleId: userOrgs.roleId,
|
||||||
roleName: roles.name,
|
roleName: roles.name,
|
||||||
|
isOwner: userOrgs.isOwner,
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.leftJoin(userOrgs, sql`${users.userId} = ${userOrgs.userId}`)
|
.leftJoin(userOrgs, sql`${users.userId} = ${userOrgs.userId}`)
|
||||||
|
|
|
@ -29,7 +29,7 @@ export function TopbarNav({
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex overflow-x-auto space-x-4 lg:space-x-6",
|
"flex overflow-x-auto space-x-4 lg:space-x-6",
|
||||||
disabled && "opacity-50 pointer-events-none",
|
disabled && "opacity-50 pointer-events-none",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
@ -38,22 +38,23 @@ export function TopbarNav({
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href.replace("{orgId}", orgId)}
|
href={item.href.replace("{orgId}", orgId)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-3 text-md",
|
"relative px-3 py-3 text-md",
|
||||||
pathname.startsWith(item.href.replace("{orgId}", orgId))
|
pathname.startsWith(item.href.replace("{orgId}", orgId))
|
||||||
? "border-b-2 border-primary text-primary font-medium"
|
? "border-b-2 border-primary text-primary font-medium"
|
||||||
: "hover:text-primary text-muted-foreground font-medium",
|
: "hover:text-primary text-muted-foreground font-medium",
|
||||||
"whitespace-nowrap",
|
"whitespace-nowrap",
|
||||||
disabled && "cursor-not-allowed",
|
disabled && "cursor-not-allowed"
|
||||||
)}
|
)}
|
||||||
onClick={disabled ? (e) => e.preventDefault() : undefined}
|
onClick={disabled ? (e) => e.preventDefault() : undefined}
|
||||||
tabIndex={disabled ? -1 : undefined}
|
tabIndex={disabled ? -1 : undefined}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 relative px-2 py-0.5 rounded-md">
|
||||||
{item.icon && (
|
{item.icon && (
|
||||||
<div className="hidden md:block">{item.icon}</div>
|
<div className="hidden md:block">{item.icon}</div>
|
||||||
)}
|
)}
|
||||||
{item.title}
|
<span className="relative z-10">{item.title}</span>
|
||||||
|
<span className="absolute inset-x-0 bottom-0 border-b-2 border-transparent group-hover:border-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full bg-muted mb-6 select-none sm:px-0 px-3 pt-3">
|
<div className="w-full border-b bg-neutral-100 dark:bg-neutral-900 mb-6 select-none sm:px-0 px-3 pt-3">
|
||||||
<div className="container mx-auto flex flex-col content-between gap-4 ">
|
<div className="container mx-auto flex flex-col content-between gap-4 ">
|
||||||
<Header
|
<Header
|
||||||
email={user.email}
|
email={user.email}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { useParams } from "next/navigation";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { PickSiteDefaultsResponse } from "@server/routers/site";
|
import { PickSiteDefaultsResponse } from "@server/routers/site";
|
||||||
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
|
|
||||||
const method = [
|
const method = [
|
||||||
{ label: "Wireguard", value: "wg" },
|
{ label: "Wireguard", value: "wg" },
|
||||||
|
@ -188,19 +189,11 @@ sh get-docker.sh`;
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{form.watch("method") === "wg" && !isLoading ? (
|
{form.watch("method") === "wg" && !isLoading ? (
|
||||||
<pre className="mt-2 w-full rounded-md bg-muted p-4 overflow-x-auto">
|
<CopyTextBox text={wgConfig} />
|
||||||
<code className="whitespace-pre-wrap font-mono">
|
|
||||||
{wgConfig}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
) : form.watch("method") === "wg" && isLoading ? (
|
) : form.watch("method") === "wg" && isLoading ? (
|
||||||
<p>Loading WireGuard configuration...</p>
|
<p>Loading WireGuard configuration...</p>
|
||||||
) : (
|
) : (
|
||||||
<pre className="mt-2 w-full rounded-md bg-muted p-4 overflow-x-auto">
|
<CopyTextBox text={newtConfig} wrapText={false} />
|
||||||
<code className="whitespace-pre-wrap">
|
|
||||||
{newtConfig}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { useToast } from "@app/hooks/useToast";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { InviteUserBody, InviteUserResponse } from "@server/routers/user";
|
import { InviteUserBody, InviteUserResponse } from "@server/routers/user";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
|
@ -37,6 +37,7 @@ import {
|
||||||
CredenzaTitle,
|
CredenzaTitle,
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
|
|
||||||
type InviteUserFormProps = {
|
type InviteUserFormProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -45,8 +46,8 @@ type InviteUserFormProps = {
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.string().email({ message: "Invalid email address" }),
|
email: z.string().email({ message: "Invalid email address" }),
|
||||||
validForHours: z.string(),
|
validForHours: z.string().min(1, { message: "Please select a duration" }),
|
||||||
roleId: z.string(),
|
roleId: z.string().min(1, { message: "Please select a role" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||||
|
@ -57,7 +58,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [expiresInDays, setExpiresInDays] = useState(1);
|
const [expiresInDays, setExpiresInDays] = useState(1);
|
||||||
|
|
||||||
const roles = [{ roleId: 1, name: "Admin" }];
|
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
||||||
|
|
||||||
const validFor = [
|
const validFor = [
|
||||||
{ hours: 24, name: "1 day" },
|
{ hours: 24, name: "1 day" },
|
||||||
|
@ -73,11 +74,44 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
validForHours: "168",
|
validForHours: "72",
|
||||||
roleId: "4",
|
roleId: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRoles() {
|
||||||
|
const res = await api
|
||||||
|
.get<AxiosResponse<ListRolesResponse>>(
|
||||||
|
`/org/${org?.org.orgId}/roles`
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to fetch roles",
|
||||||
|
description:
|
||||||
|
e.message ||
|
||||||
|
"An error occurred while fetching the roles",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.status === 200) {
|
||||||
|
setRoles(res.data.data.roles);
|
||||||
|
// form.setValue(
|
||||||
|
// "roleId",
|
||||||
|
// res.data.data.roles[0].roleId.toString()
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRoles();
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
@ -167,7 +201,6 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||||
onValueChange={
|
onValueChange={
|
||||||
field.onChange
|
field.onChange
|
||||||
}
|
}
|
||||||
defaultValue={field.value.toString()}
|
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
|
@ -8,11 +8,10 @@ import {
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@app/components/ui/dropdown-menu";
|
} from "@app/components/ui/dropdown-menu";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
import { ArrowUpDown, Crown, MoreHorizontal } from "lucide-react";
|
||||||
import { UsersDataTable } from "./UsersDataTable";
|
import { UsersDataTable } from "./UsersDataTable";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import InviteUserForm from "./InviteUserForm";
|
import InviteUserForm from "./InviteUserForm";
|
||||||
import { Badge } from "@app/components/ui/badge";
|
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
import api from "@app/api";
|
import api from "@app/api";
|
||||||
|
@ -24,6 +23,7 @@ export type UserRow = {
|
||||||
email: string;
|
email: string;
|
||||||
status: string;
|
status: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
isOwner: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UsersTableProps = {
|
type UsersTableProps = {
|
||||||
|
@ -87,6 +87,16 @@ export default function UsersTable({ users }: UsersTableProps) {
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const userRow = row.original;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center gap-1">
|
||||||
|
{userRow.isOwner && <Crown className="w-4 h-4" />}
|
||||||
|
<span>{userRow.role}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
|
@ -95,30 +105,39 @@ export default function UsersTable({ users }: UsersTableProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
{!userRow.isOwner && (
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<DropdownMenuTrigger asChild>
|
||||||
<span className="sr-only">Open menu</span>
|
<Button
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
variant="ghost"
|
||||||
</Button>
|
className="h-8 w-8 p-0"
|
||||||
</DropdownMenuTrigger>
|
>
|
||||||
<DropdownMenuContent align="end">
|
<span className="sr-only">
|
||||||
<DropdownMenuItem>Manage user</DropdownMenuItem>
|
Open menu
|
||||||
{userRow.email !== user?.email && (
|
</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<button
|
Manage user
|
||||||
className="text-red-600 hover:text-red-800"
|
|
||||||
onClick={() => {
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
setUserToRemove(userRow);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove User
|
|
||||||
</button>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
{userRow.email !== user?.email && (
|
||||||
</DropdownMenuContent>
|
<DropdownMenuItem>
|
||||||
</DropdownMenu>
|
<button
|
||||||
|
className="text-red-600 hover:text-red-800"
|
||||||
|
onClick={() => {
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
setUserToRemove(userRow);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove User
|
||||||
|
</button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { cache } from "react";
|
||||||
import OrgProvider from "@app/providers/OrgProvider";
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
import UserProvider from "@app/providers/UserProvider";
|
import UserProvider from "@app/providers/UserProvider";
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
|
|
||||||
type UsersPageProps = {
|
type UsersPageProps = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
|
@ -55,7 +56,8 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
status: "Confirmed",
|
status: "Confirmed",
|
||||||
role: user.roleName || "",
|
role: user.isOwner ? "Owner" : user.roleName || "Member",
|
||||||
|
isOwner: user.isOwner || false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue