Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b79c6615b4 |
@@ -289,6 +289,7 @@ function UserInformationCard({ user }: Readonly<{ user: User }>) {
|
||||
const neverLoggedIn = dayjs(user.last_login).isBefore(
|
||||
dayjs().subtract(1000, "years"),
|
||||
);
|
||||
const isPendingApproval = user?.pending_approval;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -328,18 +329,20 @@ function UserInformationCard({ user }: Readonly<{ user: User }>) {
|
||||
|
||||
{!isServiceUser && (
|
||||
<>
|
||||
{!user.is_current && user.role != Role.Owner && (
|
||||
<Card.ListItem
|
||||
tooltip={false}
|
||||
label={
|
||||
<>
|
||||
<Ban size={16} />
|
||||
Block User
|
||||
</>
|
||||
}
|
||||
value={<UserBlockCell user={user} isUserPage={true} />}
|
||||
/>
|
||||
)}
|
||||
{!user.is_current &&
|
||||
user.role != Role.Owner &&
|
||||
!isPendingApproval && (
|
||||
<Card.ListItem
|
||||
tooltip={false}
|
||||
label={
|
||||
<>
|
||||
<Ban size={16} />
|
||||
Block User
|
||||
</>
|
||||
}
|
||||
value={<UserBlockCell user={user} isUserPage={true} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
|
||||
115
src/app/error/page.tsx
Normal file
115
src/app/error/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { useOidc } from "@axa-fr/react-oidc";
|
||||
import Button from "@components/Button";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import loadConfig from "@utils/config";
|
||||
import { ArrowRightIcon, RefreshCw } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
export default function ErrorPage() {
|
||||
const { logout, isAuthenticated } = useOidc();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [error, setError] = useState<{
|
||||
code: number;
|
||||
message: string;
|
||||
type: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Get error details from URL params
|
||||
const code = searchParams.get("code");
|
||||
const message = searchParams.get("message");
|
||||
const type = searchParams.get("type");
|
||||
|
||||
if (code && message) {
|
||||
setError({
|
||||
code: parseInt(code),
|
||||
message: decodeURIComponent(message),
|
||||
type: type || "error",
|
||||
});
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const handleLogout = () => {
|
||||
// Use the same logout pattern as OIDCError
|
||||
logout("/", { client_id: config.clientId });
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
if (!isAuthenticated) {
|
||||
// If not authenticated, redirect to home
|
||||
router.push("/");
|
||||
return null;
|
||||
}
|
||||
|
||||
const isBlockedUser =
|
||||
error?.code === 403 && error?.message?.toLowerCase().includes("blocked");
|
||||
const isPendingApproval =
|
||||
error?.code === 403 &&
|
||||
error?.message?.toLowerCase().includes("pending approval");
|
||||
|
||||
const getTitle = () => {
|
||||
if (isBlockedUser) return "User Account Blocked";
|
||||
if (isPendingApproval) return "User Approval Pending";
|
||||
return "Access Error";
|
||||
};
|
||||
|
||||
const getDescription = () => {
|
||||
if (isBlockedUser) {
|
||||
return "Your access has been blocked by the NetBird account administrator, possibly due to new user approval requirements or security policies. Please contact your administrator to regain access.";
|
||||
}
|
||||
if (isPendingApproval) {
|
||||
return "Your account is pending approval from an administrator. Please wait for approval before accessing the dashboard.";
|
||||
}
|
||||
return "An error occurred while trying to access the dashboard. Please try again or contact your administrator.";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center flex-col h-screen max-w-xl mx-auto">
|
||||
<div className="bg-nb-gray-930 mb-3 border border-nb-gray-900 h-12 w-12 rounded-md flex items-center justify-center">
|
||||
<NetBirdIcon size={23} />
|
||||
</div>
|
||||
|
||||
<h1 className="text-center mt-2">{getTitle()}</h1>
|
||||
|
||||
<Paragraph className="text-center mt-2 block">
|
||||
{getDescription()}
|
||||
</Paragraph>
|
||||
|
||||
{error && (
|
||||
<div className="bg-nb-gray-930 border border-nb-gray-800 rounded-md p-4 mt-4 max-w-md font-mono mb-2">
|
||||
<div className="text-center text-sm text-netbird">
|
||||
<div>response_message: {error.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Paragraph className="text-center mt-2 text-sm">
|
||||
If you believe this is an error, please contact your administrator.
|
||||
</Paragraph>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{!isBlockedUser && !isPendingApproval && (
|
||||
<Button variant="default-outline" size="sm" onClick={handleRetry}>
|
||||
<RefreshCw size={16} className="mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button variant="primary" size="sm" onClick={handleLogout}>
|
||||
{isBlockedUser || isPendingApproval ? "Sign Out" : "Logout"}
|
||||
<ArrowRightIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,12 +8,12 @@ export const NotificationCountBadge = ({ count = 0 }: Props) => {
|
||||
return count ? (
|
||||
<div
|
||||
className={cn(
|
||||
count <= 9 ? "w-5 h-5" : "py-2.5 px-2",
|
||||
"relative bg-netbird flex items-center justify-center rounded-full text-white !leading-[0] text-xs font-semibold",
|
||||
count <= 9 ? "w-4 h-4" : "py-2 px-1.5",
|
||||
"relative bg-netbird flex items-center justify-center rounded-full text-white !leading-[0] text-[0.6rem] font-semibold",
|
||||
)}
|
||||
>
|
||||
<span className="animate-ping absolute left-0 inline-flex h-full w-full rounded-full bg-netbird opacity-20"></span>
|
||||
{count || 0}
|
||||
<span className={"relative -left-[0.5px]"}>{count || 0}</span>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -62,18 +62,24 @@ const UserProfileProvider = ({ children }: Props) => {
|
||||
}
|
||||
}, [user, error, users, isLoading, isAllUsersLoading]);
|
||||
|
||||
|
||||
const data = useMemo(() => {
|
||||
return {
|
||||
loggedInUser,
|
||||
};
|
||||
}, [loggedInUser]);
|
||||
|
||||
return !isLoading && loggedInUser ? (
|
||||
// Show loading only when we're still loading and don't have user data
|
||||
if (isLoading || !loggedInUser) {
|
||||
return <FullScreenLoading />;
|
||||
}
|
||||
|
||||
// For blocked or pending approval users, we still need to provide the context
|
||||
// so they can access their user data on the blocked page
|
||||
return (
|
||||
<UserProfileContext.Provider value={data}>
|
||||
<PermissionsProvider user={loggedInUser}>{children}</PermissionsProvider>
|
||||
</UserProfileContext.Provider>
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface Account {
|
||||
settings: {
|
||||
extra: {
|
||||
peer_approval_enabled: boolean;
|
||||
user_approval_required: boolean;
|
||||
};
|
||||
peer_login_expiration_enabled: boolean;
|
||||
peer_login_expiration: number;
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface User {
|
||||
is_current?: boolean;
|
||||
is_service_user?: boolean;
|
||||
is_blocked?: boolean;
|
||||
pending_approval?: boolean;
|
||||
last_login?: Date;
|
||||
permissions: Permissions;
|
||||
}
|
||||
|
||||
@@ -253,6 +253,22 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.approve")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
User <Value>{event.meta.username}</Value>{" "}
|
||||
<Value>{event.meta.email}</Value> was approved
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.reject")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
User <Value>{event.meta.username}</Value>{" "}
|
||||
<Value>{event.meta.email}</Value> was rejected
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Service User
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,7 @@ const ACTION_COLOR_MAPPING: Record<string, ActionStatus> = {
|
||||
delete: ActionStatus.ERROR,
|
||||
revoke: ActionStatus.ERROR,
|
||||
block: ActionStatus.ERROR,
|
||||
reject: ActionStatus.ERROR,
|
||||
|
||||
// Warning actions
|
||||
overuse: ActionStatus.WARNING,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
CalendarClock,
|
||||
ExternalLinkIcon,
|
||||
ShieldIcon,
|
||||
ShieldUserIcon,
|
||||
TimerResetIcon,
|
||||
} from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
@@ -52,6 +53,19 @@ export default function AuthenticationTab({ account }: Readonly<Props>) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* User approval required
|
||||
*/
|
||||
const [userApprovalRequired, setUserApprovalRequired] = useState<boolean>(
|
||||
() => {
|
||||
try {
|
||||
return account?.settings?.extra?.user_approval_required || false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Peer Expiration
|
||||
const [
|
||||
loginExpiration,
|
||||
@@ -86,6 +100,7 @@ export default function AuthenticationTab({ account }: Readonly<Props>) {
|
||||
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
peerApproval,
|
||||
userApprovalRequired,
|
||||
loginExpiration,
|
||||
expiresIn,
|
||||
expireInterval,
|
||||
@@ -118,6 +133,7 @@ export default function AuthenticationTab({ account }: Readonly<Props>) {
|
||||
extra: {
|
||||
...account.settings?.extra,
|
||||
peer_approval_enabled: peerApproval,
|
||||
user_approval_required: userApprovalRequired,
|
||||
},
|
||||
},
|
||||
} as Account)
|
||||
@@ -125,6 +141,7 @@ export default function AuthenticationTab({ account }: Readonly<Props>) {
|
||||
mutate("/accounts");
|
||||
updateRef([
|
||||
peerApproval,
|
||||
userApprovalRequired,
|
||||
loginExpiration,
|
||||
expiresIn,
|
||||
expireInterval,
|
||||
@@ -181,6 +198,27 @@ export default function AuthenticationTab({ account }: Readonly<Props>) {
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-6 w-full mt-8 mb-3"}>
|
||||
<div className={"flex flex-col"}>
|
||||
<FancyToggleSwitch
|
||||
value={userApprovalRequired}
|
||||
onChange={setUserApprovalRequired}
|
||||
dataCy={"user-approval-required"}
|
||||
label={
|
||||
<>
|
||||
<ShieldUserIcon size={15} />
|
||||
User Approval Required
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Require manual approval for new users joining via <br />
|
||||
domain matching. Users will be blocked until approved.
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col"}>
|
||||
<FancyToggleSwitch
|
||||
value={loginExpiration}
|
||||
|
||||
@@ -6,6 +6,7 @@ import DataTableHeader from "@components/table/DataTableHeader";
|
||||
import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
|
||||
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
|
||||
import GetStartedTest from "@components/ui/GetStartedTest";
|
||||
import { NotificationCountBadge } from "@components/ui/NotificationCountBadge";
|
||||
import { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
|
||||
@@ -88,6 +89,12 @@ export const UsersTableColumns: ColumnDef<User>[] = [
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "approval_required",
|
||||
accessorKey: "approval_required",
|
||||
sortingFn: "basic",
|
||||
accessorFn: (u) => u?.pending_approval,
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
@@ -127,6 +134,8 @@ export default function UsersTable({
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
const pendingApprovalCount =
|
||||
users?.filter((u) => u.pending_approval).length || 0;
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
@@ -139,6 +148,7 @@ export default function UsersTable({
|
||||
data={users}
|
||||
columnVisibility={{
|
||||
is_current: false,
|
||||
approval_required: false,
|
||||
}}
|
||||
onRowClick={(row) => {
|
||||
router.push(`/team/user?id=${row.original.id}`);
|
||||
@@ -185,18 +195,56 @@ export default function UsersTable({
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{(table) => (
|
||||
<>
|
||||
<DataTableRowsPerPage table={table} disabled={users?.length == 0} />
|
||||
<DataTableRefreshButton
|
||||
isDisabled={users?.length == 0}
|
||||
onClick={() => {
|
||||
mutate("/users?service_user=false");
|
||||
mutate("/groups");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(table) => {
|
||||
if (
|
||||
pendingApprovalCount == 0 &&
|
||||
table.getColumn("approval_required")?.getFilterValue() === true
|
||||
) {
|
||||
table.setColumnFilters([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{pendingApprovalCount > 0 && (
|
||||
<Button
|
||||
disabled={users?.length == 0}
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
let current =
|
||||
table.getColumn("approval_required")?.getFilterValue() ===
|
||||
undefined
|
||||
? true
|
||||
: undefined;
|
||||
|
||||
table.setColumnFilters([
|
||||
{
|
||||
id: "approval_required",
|
||||
value: current,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
variant={
|
||||
table.getColumn("approval_required")?.getFilterValue() ===
|
||||
true
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
Pending Approvals
|
||||
<NotificationCountBadge count={pendingApprovalCount} />
|
||||
</Button>
|
||||
)}
|
||||
<DataTableRowsPerPage table={table} disabled={users?.length == 0} />
|
||||
<DataTableRefreshButton
|
||||
isDisabled={users?.length == 0}
|
||||
onClick={() => {
|
||||
mutate("/users?service_user=false");
|
||||
mutate("/groups");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Button from "@components/Button";
|
||||
import { notify } from "@components/Notification";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { isNetBirdHosted } from "@utils/netbird";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Trash2, XCircle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -36,6 +36,42 @@ export default function UserActionCell({
|
||||
});
|
||||
};
|
||||
|
||||
const approveUser = async () => {
|
||||
const name = user.name || "User";
|
||||
notify({
|
||||
title: `'${name}' approved`,
|
||||
description: "User was successfully approved.",
|
||||
promise: userRequest.post({}, `/${user.id}/approve`).then(() => {
|
||||
mutate(`/users?service_user=${serviceUser}`);
|
||||
}),
|
||||
loadingMessage: "Approving the user...",
|
||||
});
|
||||
};
|
||||
|
||||
const rejectUser = async () => {
|
||||
const name = user.name || "User";
|
||||
const choice = await confirm({
|
||||
title: `Reject '${name}'?`,
|
||||
description:
|
||||
"Rejecting this user will remove them from the account permanently. This action cannot be undone.",
|
||||
confirmText: "Reject",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
maxWidthClass: "max-w-md",
|
||||
});
|
||||
if (!choice) return;
|
||||
|
||||
notify({
|
||||
title: `'${name}' rejected`,
|
||||
description: "User was successfully rejected and removed.",
|
||||
promise: userRequest.del("", `/${user.id}/reject`).then(() => {
|
||||
mutate(`/users?service_user=${serviceUser}`);
|
||||
}),
|
||||
|
||||
loadingMessage: "Rejecting the user...",
|
||||
});
|
||||
};
|
||||
|
||||
const openConfirm = async () => {
|
||||
const name = user.name || "User";
|
||||
const choice = await confirm({
|
||||
@@ -44,6 +80,7 @@ export default function UserActionCell({
|
||||
"Deleting this user will remove their devices and remove dashboard access. This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
maxWidthClass: "max-w-md",
|
||||
type: "danger",
|
||||
});
|
||||
if (!choice) return;
|
||||
@@ -55,21 +92,50 @@ export default function UserActionCell({
|
||||
return user.is_current;
|
||||
}, [permission.users.delete, user.is_current]);
|
||||
|
||||
const isPendingApproval = user.pending_approval;
|
||||
const canManageUsers = permission.users.update;
|
||||
|
||||
return (
|
||||
<div className={"flex justify-end pr-4 items-center gap-4"}>
|
||||
{!serviceUser && isNetBirdHosted() && (
|
||||
<div className={"flex justify-end pr-4 items-center gap-2"}>
|
||||
{!serviceUser && isNetBirdHosted() && !isPendingApproval && (
|
||||
<UserResendInviteButton user={user} />
|
||||
)}
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"sm"}
|
||||
onClick={openConfirm}
|
||||
data-cy={"delete-user"}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{isPendingApproval && canManageUsers && (
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"xs"}
|
||||
onClick={approveUser}
|
||||
data-cy={"approve-user"}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"xs"}
|
||||
className={"!px-3"}
|
||||
onClick={rejectUser}
|
||||
data-cy={"reject-user"}
|
||||
>
|
||||
<XCircle size={14} />
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isPendingApproval && (
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"sm"}
|
||||
onClick={openConfirm}
|
||||
data-cy={"delete-user"}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ export default function UserBlockCell({ user, isUserPage = false }: Props) {
|
||||
});
|
||||
};
|
||||
|
||||
if (user?.pending_approval) return;
|
||||
|
||||
return !disabled ? (
|
||||
<div className={"flex"}>
|
||||
<ToggleSwitch
|
||||
|
||||
@@ -10,6 +10,27 @@ export default function UserNameCell({ user }: Readonly<Props>) {
|
||||
const status = user.status;
|
||||
const isCurrent = user.is_current;
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (user?.pending_approval) {
|
||||
return {
|
||||
color: "bg-netbird text-netbird-950",
|
||||
icon: <Clock size={12} />,
|
||||
};
|
||||
}
|
||||
if (status === "blocked") {
|
||||
return { color: "bg-red-500 text-red-100", icon: <Ban size={12} /> };
|
||||
}
|
||||
if (status === "invited") {
|
||||
return {
|
||||
color: "bg-yellow-400 text-yellow-900",
|
||||
icon: <Clock size={12} />,
|
||||
};
|
||||
}
|
||||
return { color: "bg-gray-400", icon: <Clock size={12} /> };
|
||||
};
|
||||
|
||||
const { color, icon } = getStatusIcon();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex gap-4 px-2 py-1 items-center")}
|
||||
@@ -29,12 +50,10 @@ export default function UserNameCell({ user }: Readonly<Props>) {
|
||||
<div
|
||||
className={cn(
|
||||
"w-5 h-5 absolute -right-1 -bottom-1 bg-nb-gray-930 rounded-full flex items-center justify-center border-2 border-nb-gray-950",
|
||||
status == "invited" && "bg-yellow-400 text-yellow-900",
|
||||
status == "blocked" && "bg-red-500 text-red-100",
|
||||
color,
|
||||
)}
|
||||
>
|
||||
{status == "invited" && <Clock size={12} />}
|
||||
{status == "blocked" && <Ban size={12} />}
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { ExternalLinkIcon, HelpCircle } from "lucide-react";
|
||||
import React from "react";
|
||||
import { User } from "@/interfaces/User";
|
||||
|
||||
@@ -8,23 +11,73 @@ type Props = {
|
||||
|
||||
export default function UserStatusCell({ user }: Readonly<Props>) {
|
||||
const status = user.status;
|
||||
const isPendingApproval = user.pending_approval;
|
||||
|
||||
const getStatusDisplay = () => {
|
||||
if (isPendingApproval) {
|
||||
return { text: "Pending Approval", color: "bg-netbird" };
|
||||
}
|
||||
if (status === "blocked") {
|
||||
return { text: "Blocked", color: "bg-red-500" };
|
||||
}
|
||||
if (status === "invited") {
|
||||
return { text: "Pending", color: "bg-yellow-400" };
|
||||
}
|
||||
if (status === "active") {
|
||||
return { text: "Active", color: "bg-green-500" };
|
||||
}
|
||||
return { text: status || "Unknown", color: "bg-gray-400" };
|
||||
};
|
||||
|
||||
const { text, color } = getStatusDisplay();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex gap-2.5 items-center text-nb-gray-300 text-sm")}
|
||||
data-cy={"user-status-cell"}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full",
|
||||
status == "invited" && "bg-yellow-400",
|
||||
status == "blocked" && "bg-red-500",
|
||||
status == "active" && "bg-green-500",
|
||||
)}
|
||||
></span>
|
||||
{status == "invited" && "Pending"}
|
||||
{status == "blocked" && "Blocked"}
|
||||
{status == "active" && "Active"}
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"max-w-xs text-xs flex flex-col gap-2"}>
|
||||
<div>
|
||||
This user needs to be approved by an administrator before it can
|
||||
join your organization.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
If you want to disable approval for new users, go to{" "}
|
||||
<InlineLink href={"/settings?tab=authentication"}>
|
||||
Settings
|
||||
</InlineLink>{" "}
|
||||
and disable{" "}
|
||||
<span className={"font-medium text-white"}>
|
||||
{"'User Approval Required'"}
|
||||
</span>
|
||||
.
|
||||
</div>
|
||||
<div>
|
||||
Learn more about{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/approve-users"}
|
||||
target={"_blank"}
|
||||
>
|
||||
User Approval <ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
interactive={true}
|
||||
side="right"
|
||||
disabled={!isPendingApproval}
|
||||
>
|
||||
<div
|
||||
className={cn("flex gap-2.5 items-center text-nb-gray-300 text-sm")}
|
||||
data-cy={"user-status-cell"}
|
||||
>
|
||||
<span className={cn("h-2 w-2 rounded-full", color)}></span>
|
||||
{text}
|
||||
{isPendingApproval && (
|
||||
<HelpCircle size={14} className="text-netbird cursor-help" />
|
||||
)}
|
||||
</div>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ export function useApiErrorHandling(ignoreError = false) {
|
||||
const { login } = useOidc();
|
||||
const currentPath = usePathname();
|
||||
const { setError } = useErrorBoundary();
|
||||
|
||||
if (ignoreError)
|
||||
return (err: ErrorResponse) => {
|
||||
console.log(err);
|
||||
@@ -231,6 +232,21 @@ export function useApiErrorHandling(ignoreError = false) {
|
||||
if (err.code == 401 && err.message == "token invalid") {
|
||||
setError(err);
|
||||
}
|
||||
|
||||
// Handle user blocked/pending approval responses
|
||||
if (err.code == 403 && (
|
||||
err.message?.toLowerCase().includes("blocked") ||
|
||||
err.message?.toLowerCase().includes("pending")
|
||||
)) {
|
||||
const params = new URLSearchParams({
|
||||
code: err.code.toString(),
|
||||
message: encodeURIComponent(err.message),
|
||||
type: "user-status"
|
||||
});
|
||||
window.location.href = `/error?${params.toString()}`;
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
if (err.code == 500 && err.message == "internal server error") {
|
||||
setError(err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user