Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
130dc0c32c | ||
|
|
f5824d6ddb | ||
|
|
829395f908 |
@@ -6,24 +6,26 @@ export const ListItem = ({
|
||||
label,
|
||||
value,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
value: string | React.ReactNode;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-between gap-12 border-b border-nb-gray-920 py-2 px-4 last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
className={cn(" border-b border-nb-gray-920 last:border-b-0", className)}
|
||||
>
|
||||
<div className={"flex items-center gap-2 text-nb-gray-100 font-medium"}>
|
||||
{icon}
|
||||
{label}
|
||||
<div className={cn("flex justify-between gap-12 py-2 px-4")}>
|
||||
<div className={"flex items-center gap-2 text-nb-gray-100 font-medium"}>
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
<div className={"text-nb-gray-300"}>{value}</div>
|
||||
</div>
|
||||
<div className={"text-nb-gray-300"}>{value}</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ interface MultiSelectProps {
|
||||
closeOnSelect?: boolean;
|
||||
resource?: PolicyRuleResource;
|
||||
onResourceChange?: (resource?: PolicyRuleResource) => void;
|
||||
placeholder?: string;
|
||||
placeholder?: React.ReactNode | string;
|
||||
customTrigger?: React.ReactNode;
|
||||
align?: "start" | "end";
|
||||
side?: "top" | "bottom";
|
||||
@@ -397,7 +397,9 @@ export function PeerGroupSelector({
|
||||
})}
|
||||
|
||||
{values.length == 0 && !resource && (
|
||||
<span className={"pl-1"}>{placeholder}</span>
|
||||
<span className={cn(typeof placeholder === "string" && "pl-1")}>
|
||||
{placeholder}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,14 +28,30 @@ const UserProfileContext = React.createContext(
|
||||
);
|
||||
|
||||
export default function UsersProvider({ children }: Readonly<Props>) {
|
||||
const { data: users, mutate, isLoading } = useFetchApi<User[]>("/users");
|
||||
const { data: users, mutate, isLoading } = useFetchApi<User[]>(
|
||||
"/users?service_user=false",
|
||||
);
|
||||
const { data: serviceUsers, mutate: mutateServiceUsers, isLoading: isLoadingServiceUsers } = useFetchApi<
|
||||
User[]
|
||||
>("/users?service_user=true");
|
||||
|
||||
const refresh = () => {
|
||||
mutate().then();
|
||||
mutateServiceUsers().then();
|
||||
};
|
||||
|
||||
const allUsers = useMemo(() => {
|
||||
return [...(users ?? []), ...(serviceUsers ?? [])];
|
||||
}, [users, serviceUsers]);
|
||||
|
||||
return (
|
||||
<UsersContext.Provider value={{ users, refresh, isLoading }}>
|
||||
<UsersContext.Provider
|
||||
value={{
|
||||
users: allUsers,
|
||||
refresh,
|
||||
isLoading: isLoading || isLoadingServiceUsers,
|
||||
}}
|
||||
>
|
||||
<UserProfileProvider>{children}</UserProfileProvider>
|
||||
</UsersContext.Provider>
|
||||
);
|
||||
|
||||
@@ -240,7 +240,7 @@ export const GroupsTableColumns: ColumnDef<GroupUsage>[] = [
|
||||
row.setup_keys_count > 0 ||
|
||||
row.users_count > 0 ||
|
||||
row.resources_count > 0 ||
|
||||
row.zones_count
|
||||
row.zones_count > 0
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -791,6 +791,7 @@ export default function ReverseProxyModal({
|
||||
onOpenChange={setSsoModalOpen}
|
||||
key={ssoModalOpen ? "sso1" : "sso0"}
|
||||
currentGroups={bearerGroups}
|
||||
isEnabled={bearerEnabled}
|
||||
onSave={(groups) => {
|
||||
setTimeout(() => {
|
||||
setBearerGroups(groups);
|
||||
|
||||
@@ -5,11 +5,15 @@ import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
|
||||
import React, { useState } from "react";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import Badge from "@components/Badge";
|
||||
import { CircleUser } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
currentGroups: Group[];
|
||||
isEnabled: boolean;
|
||||
onSave: (groups: Group[]) => void;
|
||||
onRemove: () => void;
|
||||
};
|
||||
@@ -18,17 +22,17 @@ export default function AuthSSOModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
currentGroups,
|
||||
isEnabled,
|
||||
onSave,
|
||||
onRemove,
|
||||
}: Readonly<Props>) {
|
||||
const { users } = useUsers();
|
||||
const [groups, setGroups] = useState<Group[]>(currentGroups);
|
||||
const isEditing = currentGroups.length > 0;
|
||||
const isEditing = isEnabled;
|
||||
|
||||
const handleSave = () => {
|
||||
if (groups.length > 0) {
|
||||
onOpenChange(false);
|
||||
onSave(groups);
|
||||
}
|
||||
onOpenChange(false);
|
||||
onSave(groups);
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
@@ -51,7 +55,17 @@ export default function AuthSSOModal({
|
||||
<PeerGroupSelector
|
||||
values={groups}
|
||||
onChange={setGroups}
|
||||
placeholder="Select distribution groups..."
|
||||
placeholder={
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<Badge className={"py-[3px]"} variant={"gray-ghost"}>
|
||||
<CircleUser size={12} />
|
||||
All Users
|
||||
</Badge>
|
||||
Select user groups...
|
||||
</div>
|
||||
}
|
||||
users={users}
|
||||
hideAllGroup={true}
|
||||
/>
|
||||
<div className="flex gap-3 w-full justify-between mt-6">
|
||||
{isEditing ? (
|
||||
@@ -63,11 +77,7 @@ export default function AuthSSOModal({
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
disabled={groups.length === 0}
|
||||
>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
@@ -79,12 +89,8 @@ export default function AuthSSOModal({
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
disabled={groups.length === 0}
|
||||
>
|
||||
Add Groups
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
Add SSO
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,11 +1,51 @@
|
||||
import Badge from "@components/Badge";
|
||||
import Button from "@components/Button";
|
||||
import { Settings, ShieldCheck, ShieldOff } from "lucide-react";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@components/HoverCard";
|
||||
import { ListItem } from "@components/ListItem";
|
||||
import GroupBadge from "@components/ui/GroupBadge";
|
||||
import { UserCountStack } from "@components/ui/MultipleGroups";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
Binary,
|
||||
LucideIcon,
|
||||
RectangleEllipsis,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
ShieldOff,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useReverseProxies } from "@/contexts/ReverseProxiesProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { ReverseProxy } from "@/interfaces/ReverseProxy";
|
||||
|
||||
const AUTH_METHODS: {
|
||||
key: "password_auth" | "pin_auth" | "bearer_auth";
|
||||
label: string;
|
||||
hoverLabel: string;
|
||||
Icon: LucideIcon;
|
||||
}[] = [
|
||||
{
|
||||
key: "password_auth",
|
||||
label: "Password",
|
||||
hoverLabel: "Password",
|
||||
Icon: RectangleEllipsis,
|
||||
},
|
||||
{ key: "pin_auth", label: "PIN Code", hoverLabel: "PIN Code", Icon: Binary },
|
||||
{
|
||||
key: "bearer_auth",
|
||||
label: "SSO",
|
||||
hoverLabel: "SSO (Single Sign On)",
|
||||
Icon: Users,
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
reverseProxy: ReverseProxy;
|
||||
};
|
||||
@@ -15,14 +55,36 @@ export default function ReverseProxyAuthCell({
|
||||
}: Readonly<Props>) {
|
||||
const { permission } = usePermissions();
|
||||
const { openModal } = useReverseProxies();
|
||||
const { groups } = useGroups();
|
||||
const auth = reverseProxy.auth;
|
||||
|
||||
const enabledCount = [
|
||||
auth?.bearer_auth?.enabled,
|
||||
auth?.link_auth?.enabled,
|
||||
auth?.password_auth?.enabled,
|
||||
auth?.pin_auth?.enabled,
|
||||
].filter(Boolean).length;
|
||||
const enabled = AUTH_METHODS.filter((m) => auth?.[m.key]?.enabled);
|
||||
|
||||
const ssoGroups = auth?.bearer_auth?.enabled
|
||||
? (auth.bearer_auth.distribution_groups ?? [])
|
||||
.map((groupId) => groups?.find((g) => g.id === groupId))
|
||||
.filter((g): g is Group => g != undefined)
|
||||
: [];
|
||||
|
||||
const showHoverContent =
|
||||
enabled.length > 1 || (enabled.length === 1 && auth?.bearer_auth?.enabled);
|
||||
|
||||
const SingleIcon = enabled.length === 1 ? enabled[0].Icon : null;
|
||||
|
||||
const badgeContent =
|
||||
SingleIcon ? (
|
||||
<>
|
||||
<SingleIcon size={12} className="text-green-500" />
|
||||
<span className={"font-medium text-xs"}>{enabled[0].label}</span>
|
||||
</>
|
||||
) : enabled.length > 1 ? (
|
||||
<>
|
||||
<ShieldCheck size={12} className="text-green-400" />
|
||||
<span className={"font-medium text-xs"}>
|
||||
{enabled.length} Enabled
|
||||
</span>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -32,19 +94,66 @@ export default function ReverseProxyAuthCell({
|
||||
openModal({ proxy: reverseProxy, initialTab: "auth" });
|
||||
}}
|
||||
>
|
||||
{enabledCount > 0 ? (
|
||||
<Badge variant={"gray"} useHover={false} className={"cursor-pointer"}>
|
||||
<ShieldCheck size={12} className="text-green-400" />
|
||||
<div>
|
||||
<span className={"font-medium text-xs"}>Enabled</span>
|
||||
</div>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant={"gray"}>
|
||||
<ShieldOff size={12} className="text-red-500" />
|
||||
<span className={"font-medium text-xs"}>None</span>
|
||||
</Badge>
|
||||
)}
|
||||
<HoverCard openDelay={200} closeDelay={100}>
|
||||
<HoverCardTrigger asChild={true}>
|
||||
{badgeContent ? (
|
||||
<Badge
|
||||
variant={"gray"}
|
||||
useHover={false}
|
||||
className={"cursor-pointer"}
|
||||
>
|
||||
{badgeContent}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant={"gray"}>
|
||||
<ShieldOff size={12} className="text-red-500" />
|
||||
<span className={"font-medium text-xs"}>None</span>
|
||||
</Badge>
|
||||
)}
|
||||
</HoverCardTrigger>
|
||||
{showHoverContent && (
|
||||
<HoverCardContent
|
||||
className={"p-0"}
|
||||
sideOffset={14}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className={"text-xs"}>
|
||||
{enabled.map(({ key, hoverLabel, Icon }) => (
|
||||
<ListItem
|
||||
key={key}
|
||||
className={"py-0.5"}
|
||||
icon={<Icon size={14} />}
|
||||
label={hoverLabel}
|
||||
value={
|
||||
<div className={"text-green-500"}>
|
||||
{key === "bearer_auth" && ssoGroups.length === 0
|
||||
? "All Users"
|
||||
: "Enabled"}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{key === "bearer_auth" && ssoGroups.length > 0 && (
|
||||
<div className={"flex flex-col gap-2 px-4 pt-2 pb-3"}>
|
||||
{ssoGroups.map((group) => (
|
||||
<div
|
||||
key={group.id}
|
||||
className={
|
||||
"flex gap-2 items-center justify-between"
|
||||
}
|
||||
>
|
||||
<GroupBadge group={group} />
|
||||
<ArrowRightIcon size={14} />
|
||||
<UserCountStack group={group} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ListItem>
|
||||
))}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
)}
|
||||
</HoverCard>
|
||||
|
||||
<Button
|
||||
size={"xs"}
|
||||
|
||||
@@ -75,7 +75,7 @@ const ReverseProxyColumns: ColumnDef<ReverseProxy>[] = [
|
||||
{
|
||||
accessorKey: "auth",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Authentication</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>Auth Methods</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <ReverseProxyAuthCell reverseProxy={row.original} />,
|
||||
},
|
||||
|
||||
@@ -104,7 +104,7 @@ const FlatTargetsTableColumns: ColumnDef<ReverseProxyFlatTarget>[] = [
|
||||
{
|
||||
accessorKey: "auth",
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>Authentication</DataTableHeader>
|
||||
<DataTableHeader column={column}>Auth Methods</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ReverseProxyAuthCell reverseProxy={row.original.proxy} />
|
||||
|
||||
Reference in New Issue
Block a user