mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-02 08:35:08 +08:00
feat: support admin assign superuser in admin ui (#12798)
### What problem does this PR solve? Allow superuser(admin) to grant or revoke other superuser. ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -2451,7 +2451,9 @@ Important structured information may include: names, dates, locations, events, k
|
|||||||
|
|
||||||
role: 'Role',
|
role: 'Role',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
|
userType: 'User type',
|
||||||
superuser: 'Superuser',
|
superuser: 'Superuser',
|
||||||
|
normalUser: 'Normal',
|
||||||
|
|
||||||
createTime: 'Create time',
|
createTime: 'Create time',
|
||||||
lastLoginTime: 'Last login time',
|
lastLoginTime: 'Last login time',
|
||||||
|
|||||||
13
web/src/pages/admin/layouts/authorized-layout.tsx
Normal file
13
web/src/pages/admin/layouts/authorized-layout.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { Navigate, Outlet } from 'react-router';
|
||||||
|
|
||||||
|
import { Routes } from '@/routes';
|
||||||
|
import authorizationUtil from '@/utils/authorization-util';
|
||||||
|
import { CurrentUserInfoContext } from './root-layout';
|
||||||
|
|
||||||
|
export default function AdminAuthorizedLayout() {
|
||||||
|
const [{ userInfo }] = useContext(CurrentUserInfoContext);
|
||||||
|
const isLoggedIn = !!authorizationUtil.getAuthorization() && userInfo;
|
||||||
|
|
||||||
|
return isLoggedIn ? <Outlet /> : <Navigate to={Routes.Admin} />;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from 'react';
|
import { useContext, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { NavLink, Outlet, useNavigate } from 'react-router';
|
import { NavLink, Outlet, useNavigate } from 'react-router';
|
||||||
|
|
||||||
@ -21,10 +21,12 @@ import authorizationUtil from '@/utils/authorization-util';
|
|||||||
|
|
||||||
import ThemeSwitch from '../components/theme-switch';
|
import ThemeSwitch from '../components/theme-switch';
|
||||||
import { IS_ENTERPRISE } from '../utils';
|
import { IS_ENTERPRISE } from '../utils';
|
||||||
|
import { CurrentUserInfoContext } from './root-layout';
|
||||||
|
|
||||||
const AdminNavigationLayout = () => {
|
const AdminNavigationLayout = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [, setCurrentUserInfo] = useContext(CurrentUserInfoContext);
|
||||||
|
|
||||||
const { data: version } = useQuery({
|
const { data: version } = useQuery({
|
||||||
queryKey: ['admin/version'],
|
queryKey: ['admin/version'],
|
||||||
@ -72,6 +74,10 @@ const AdminNavigationLayout = () => {
|
|||||||
await logout();
|
await logout();
|
||||||
authorizationUtil.removeAll();
|
authorizationUtil.removeAll();
|
||||||
navigate(Routes.Admin);
|
navigate(Routes.Admin);
|
||||||
|
setCurrentUserInfo({
|
||||||
|
userInfo: null,
|
||||||
|
source: null,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,55 @@
|
|||||||
|
import { createContext, Dispatch, SetStateAction, useState } from 'react';
|
||||||
import { Outlet } from 'react-router';
|
import { Outlet } from 'react-router';
|
||||||
|
|
||||||
|
import type { IUserInfo } from '@/interfaces/database/user-setting';
|
||||||
|
import authorizationUtil from '@/utils/authorization-util';
|
||||||
|
|
||||||
|
type LocalStoragePersistedUserInfo = {
|
||||||
|
avatar: unknown;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CurrentUserInfo =
|
||||||
|
| {
|
||||||
|
userInfo: null;
|
||||||
|
source: null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
userInfo: AdminService.LoginData | IUserInfo;
|
||||||
|
source: 'serverRequest';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
userInfo: LocalStoragePersistedUserInfo;
|
||||||
|
source: 'localStorage';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLocalStorageUserInfo = (): CurrentUserInfo => {
|
||||||
|
const userInfo = authorizationUtil.getUserInfoObject();
|
||||||
|
|
||||||
|
return userInfo
|
||||||
|
? {
|
||||||
|
userInfo: userInfo,
|
||||||
|
source: 'localStorage',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
userInfo: null,
|
||||||
|
source: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CurrentUserInfoContext = createContext<
|
||||||
|
[CurrentUserInfo, Dispatch<SetStateAction<CurrentUserInfo>>]
|
||||||
|
>([getLocalStorageUserInfo(), () => {}]);
|
||||||
|
|
||||||
const AdminRootLayout = () => {
|
const AdminRootLayout = () => {
|
||||||
return <Outlet />;
|
const userInfoCtx = useState<CurrentUserInfo>(getLocalStorageUserInfo());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CurrentUserInfoContext.Provider value={userInfoCtx}>
|
||||||
|
<Outlet context={userInfoCtx} />
|
||||||
|
</CurrentUserInfoContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminRootLayout;
|
export default AdminRootLayout;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { type AxiosResponseHeaders } from 'axios';
|
import { type AxiosResponseHeaders } from 'axios';
|
||||||
import { useEffect, useId } from 'react';
|
import { useContext, useEffect, useId } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
@ -36,8 +36,11 @@ import { login } from '@/services/admin-service';
|
|||||||
import { BgSvg } from '../login-next/bg';
|
import { BgSvg } from '../login-next/bg';
|
||||||
import ThemeSwitch from './components/theme-switch';
|
import ThemeSwitch from './components/theme-switch';
|
||||||
|
|
||||||
|
import { CurrentUserInfoContext } from './layouts/root-layout';
|
||||||
|
|
||||||
function AdminLogin() {
|
function AdminLogin() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [, setCurrentUserInfo] = useContext(CurrentUserInfoContext);
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||||
const { isLogin } = useAuth();
|
const { isLogin } = useAuth();
|
||||||
|
|
||||||
@ -59,16 +62,19 @@ function AdminLogin() {
|
|||||||
);
|
);
|
||||||
const token = req.data.access_token;
|
const token = req.data.access_token;
|
||||||
|
|
||||||
const userInfo = {
|
// Lift to global user info context
|
||||||
avatar: req.data.avatar,
|
setCurrentUserInfo({
|
||||||
name: req.data.nickname,
|
userInfo: req.data,
|
||||||
email: req.data.email,
|
source: 'serverRequest',
|
||||||
};
|
});
|
||||||
|
|
||||||
authorizationUtil.setItems({
|
authorizationUtil.setItems({
|
||||||
Authorization: authorization as string,
|
Authorization: authorization as string,
|
||||||
Token: token,
|
Token: token,
|
||||||
userInfo: JSON.stringify(userInfo),
|
userInfo: JSON.stringify({
|
||||||
|
...req.data,
|
||||||
|
name: req.data.nickname,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate('/admin/services');
|
navigate('/admin/services');
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useLayoutEffect, useMemo, useState } from 'react';
|
import { useContext, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
@ -43,6 +43,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
@ -63,7 +64,6 @@ import {
|
|||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Switch } from '@/components/ui/switch';
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -81,8 +81,10 @@ import useCreateUserForm from './forms/user-form';
|
|||||||
import {
|
import {
|
||||||
createUser,
|
createUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
grantSuperuser,
|
||||||
listRoles,
|
listRoles,
|
||||||
listUsers,
|
listUsers,
|
||||||
|
revokeSuperuser,
|
||||||
updateUserPassword,
|
updateUserPassword,
|
||||||
updateUserRole,
|
updateUserRole,
|
||||||
updateUserStatus,
|
updateUserStatus,
|
||||||
@ -96,8 +98,15 @@ import {
|
|||||||
parseBooleanish,
|
parseBooleanish,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
import { DialogDescription } from '@radix-ui/react-dialog';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import EnterpriseFeature from './components/enterprise-feature';
|
import EnterpriseFeature from './components/enterprise-feature';
|
||||||
|
import { CurrentUserInfoContext } from './layouts/root-layout';
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<AdminService.ListUsersItem>();
|
const columnHelper = createColumnHelper<AdminService.ListUsersItem>();
|
||||||
const globalFilterFn = createFuzzySearchFn<AdminService.ListUsersItem>([
|
const globalFilterFn = createFuzzySearchFn<AdminService.ListUsersItem>([
|
||||||
@ -112,6 +121,8 @@ const STATUS_FILTER_OPTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function AdminUserManagement() {
|
function AdminUserManagement() {
|
||||||
|
const [{ userInfo }] = useContext(CurrentUserInfoContext);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -200,6 +211,22 @@ function AdminUserManagement() {
|
|||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setSuperuserMutation = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
email,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
type: 'grant' | 'revoke';
|
||||||
|
}) => {
|
||||||
|
return type === 'grant' ? grantSuperuser(email) : revokeSuperuser(email);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
|
||||||
|
},
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
// Update user status mutation
|
// Update user status mutation
|
||||||
const updateUserStatusMutation = useMutation({
|
const updateUserStatusMutation = useMutation({
|
||||||
mutationFn: (data: { email: string; isActive: boolean }) =>
|
mutationFn: (data: { email: string; isActive: boolean }) =>
|
||||||
@ -217,15 +244,6 @@ function AdminUserManagement() {
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor('nickname', {
|
columnHelper.accessor('nickname', {
|
||||||
header: t('admin.nickname'),
|
header: t('admin.nickname'),
|
||||||
cell: ({ row, cell }) => (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="mr-2 empty:hidden">{cell.getValue()}</span>
|
|
||||||
|
|
||||||
{row.original.is_superuser ? (
|
|
||||||
<Badge variant="secondary">{t('admin.superuser')}</Badge>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
...(IS_ENTERPRISE
|
...(IS_ENTERPRISE
|
||||||
@ -267,37 +285,59 @@ function AdminUserManagement() {
|
|||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|
||||||
columnHelper.display({
|
|
||||||
id: 'enable',
|
|
||||||
header: t('admin.enable'),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Switch
|
|
||||||
checked={parseBooleanish(row.original.is_active)}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
updateUserStatusMutation.mutate({
|
|
||||||
email: row.original.email,
|
|
||||||
isActive: checked,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={updateUserStatusMutation.isPending}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('is_active', {
|
columnHelper.accessor('is_active', {
|
||||||
header: t('admin.status'),
|
header: t('admin.status'),
|
||||||
cell: ({ cell }) => (
|
cell: ({ cell, row }) => {
|
||||||
<Badge
|
const isMe = row.original.email === userInfo?.email;
|
||||||
variant={parseBooleanish(cell.getValue()) ? 'success' : 'secondary'}
|
|
||||||
className="pl-[.5em]"
|
if (isMe) {
|
||||||
>
|
return (
|
||||||
<LucideDot className="size-[1em] stroke-[8] mr-1" />
|
<Badge
|
||||||
{t(
|
variant={
|
||||||
parseBooleanish(cell.getValue())
|
parseBooleanish(cell.getValue()) ? 'success' : 'destructive'
|
||||||
? 'admin.active'
|
}
|
||||||
: 'admin.inactive',
|
>
|
||||||
)}
|
<LucideDot className="size-[1em] stroke-[8] mr-1" />
|
||||||
</Badge>
|
{parseBooleanish(cell.getValue())
|
||||||
),
|
? t('admin.active')
|
||||||
|
: t('admin.inactive')}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
disabled={updateUserStatusMutation.isPending}
|
||||||
|
value={cell.getValue()}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateUserStatusMutation.mutate({
|
||||||
|
email: row.original.email,
|
||||||
|
isActive: parseBooleanish(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<LucideDot className="size-[1em] stroke-[8] mr-1" />
|
||||||
|
{t('admin.inactive')}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
<SelectItem value="1">
|
||||||
|
<div className="flex items-center text-state-success">
|
||||||
|
<LucideDot className="size-[1em] stroke-[8] mr-1" />
|
||||||
|
{t('admin.active')}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
},
|
||||||
filterFn: createColumnFilterFn(
|
filterFn: createColumnFilterFn(
|
||||||
(row, id, filterValue) => row.getValue(id) === filterValue,
|
(row, id, filterValue) => row.getValue(id) === filterValue,
|
||||||
{
|
{
|
||||||
@ -307,48 +347,106 @@ function AdminUserManagement() {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor('is_superuser', {
|
||||||
|
header: t('admin.userType'),
|
||||||
|
cell: ({ cell, row }) => {
|
||||||
|
const isMe = row.original.email === userInfo?.email;
|
||||||
|
|
||||||
|
if (isMe) {
|
||||||
|
return <Badge variant="secondary">{t('admin.superuser')}</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
disabled={
|
||||||
|
setSuperuserMutation.isPending ||
|
||||||
|
row.original.email === userInfo?.email
|
||||||
|
}
|
||||||
|
value={cell.getValue() ? 'superuser' : 'normal'}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSuperuserMutation.mutate({
|
||||||
|
email: row.original.email,
|
||||||
|
type: value === 'superuser' ? 'grant' : 'revoke',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="normal">{t('admin.normalUser')}</SelectItem>
|
||||||
|
<SelectItem value="superuser">
|
||||||
|
{t('admin.superuser')}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: t('admin.actions'),
|
header: t('admin.actions'),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<div className="opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100 transition-opacity">
|
const isMe = row.original.email === userInfo?.email;
|
||||||
<Button
|
|
||||||
variant="transparent"
|
return (
|
||||||
size="icon"
|
<div className="opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100 transition-opacity">
|
||||||
className="border-0"
|
<Button
|
||||||
onClick={() =>
|
variant="transparent"
|
||||||
navigate(`${Routes.AdminUserManagement}/${row.original.email}`)
|
size="icon"
|
||||||
}
|
className="border-0"
|
||||||
>
|
onClick={() =>
|
||||||
<LucideClipboardList />
|
navigate(
|
||||||
</Button>
|
`${Routes.AdminUserManagement}/${row.original.email}`,
|
||||||
<Button
|
)
|
||||||
variant="transparent"
|
}
|
||||||
size="icon"
|
>
|
||||||
className="border-0"
|
<LucideClipboardList />
|
||||||
onClick={() => {
|
</Button>
|
||||||
setUserToMakeAction(row.original);
|
|
||||||
setPasswordModalOpen(true);
|
{!isMe && (
|
||||||
}}
|
<>
|
||||||
>
|
<Button
|
||||||
<LucideUserLock />
|
variant="transparent"
|
||||||
</Button>
|
size="icon"
|
||||||
<Button
|
className="border-0"
|
||||||
variant="danger"
|
onClick={() => {
|
||||||
size="icon"
|
setUserToMakeAction(row.original);
|
||||||
className="border-0"
|
setPasswordModalOpen(true);
|
||||||
onClick={() => {
|
}}
|
||||||
setUserToMakeAction(row.original);
|
>
|
||||||
setDeleteModalOpen(true);
|
<LucideUserLock />
|
||||||
}}
|
</Button>
|
||||||
>
|
<Button
|
||||||
<LucideTrash2 />
|
variant="danger"
|
||||||
</Button>
|
size="icon"
|
||||||
</div>
|
className="border-0"
|
||||||
),
|
onClick={() => {
|
||||||
|
setUserToMakeAction(row.original);
|
||||||
|
setDeleteModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LucideTrash2 />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[t, updateUserRoleMutation, roleList, updateUserStatusMutation, navigate],
|
[
|
||||||
|
t,
|
||||||
|
roleList,
|
||||||
|
updateUserRoleMutation,
|
||||||
|
userInfo?.email,
|
||||||
|
updateUserStatusMutation,
|
||||||
|
setSuperuserMutation,
|
||||||
|
navigate,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
@ -505,11 +603,11 @@ function AdminUserManagement() {
|
|||||||
<col className="w-[22%]" />
|
<col className="w-[22%]" />
|
||||||
|
|
||||||
<EnterpriseFeature>
|
<EnterpriseFeature>
|
||||||
{() => <col className="w-[12%]" />}
|
{() => <col className="w-24" />}
|
||||||
</EnterpriseFeature>
|
</EnterpriseFeature>
|
||||||
|
|
||||||
<col className="w-[8%]" />
|
<col className="w-40" />
|
||||||
<col className="w-[15%]" />
|
<col className="w-40" />
|
||||||
<col className="w-52" />
|
<col className="w-52" />
|
||||||
</colgroup>
|
</colgroup>
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
import { Routes } from '@/routes';
|
|
||||||
import authorizationUtil from '@/utils/authorization-util';
|
|
||||||
import { Navigate, Outlet } from 'react-router';
|
|
||||||
|
|
||||||
export default function AuthorizedAdminWrapper() {
|
|
||||||
const isLogin = !!authorizationUtil.getAuthorization();
|
|
||||||
|
|
||||||
return isLogin ? <Outlet /> : <Navigate to={Routes.Admin} />;
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
import { createBrowserRouter, Navigate } from 'react-router';
|
import { createBrowserRouter, Navigate, type RouteObject } from 'react-router';
|
||||||
import FallbackComponent from './components/fallback-component';
|
import FallbackComponent from './components/fallback-component';
|
||||||
import { IS_ENTERPRISE } from './pages/admin/utils';
|
import { IS_ENTERPRISE } from './pages/admin/utils';
|
||||||
|
|
||||||
@ -389,53 +389,58 @@ const routeConfig = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.Admin,
|
path: Routes.Admin,
|
||||||
layout: false,
|
|
||||||
Component: lazy(() => import('@/pages/admin/layouts/root-layout')),
|
Component: lazy(() => import('@/pages/admin/layouts/root-layout')),
|
||||||
|
errorElement: <FallbackComponent />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: Routes.Admin,
|
||||||
Component: lazy(() => import('@/pages/admin/login')),
|
Component: lazy(() => import('@/pages/admin/login')),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: `${Routes.AdminUserManagement}/:id`,
|
|
||||||
Component: lazy(() => import('@/pages/admin/user-detail')),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: Routes.Admin,
|
path: Routes.Admin,
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import('@/pages/admin/layouts/navigation-layout'),
|
() => import('@/pages/admin/layouts/authorized-layout'),
|
||||||
),
|
),
|
||||||
wrappers: ['@/pages/admin/wrappers/authorized'],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: Routes.AdminServices,
|
path: `${Routes.AdminUserManagement}/:id`,
|
||||||
Component: lazy(() => import('@/pages/admin/service-status')),
|
Component: lazy(() => import('@/pages/admin/user-detail')),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.AdminUserManagement,
|
Component: lazy(
|
||||||
Component: lazy(() => import('@/pages/admin/users')),
|
() => import('@/pages/admin/layouts/navigation-layout'),
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: Routes.AdminServices,
|
||||||
|
Component: lazy(() => import('@/pages/admin/service-status')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: Routes.AdminUserManagement,
|
||||||
|
Component: lazy(() => import('@/pages/admin/users')),
|
||||||
|
},
|
||||||
|
...(IS_ENTERPRISE
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: Routes.AdminWhitelist,
|
||||||
|
Component: lazy(() => import('@/pages/admin/whitelist')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: Routes.AdminRoles,
|
||||||
|
Component: lazy(() => import('@/pages/admin/roles')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: Routes.AdminMonitoring,
|
||||||
|
Component: lazy(() => import('@/pages/admin/monitoring')),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
...(IS_ENTERPRISE
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
path: Routes.AdminWhitelist,
|
|
||||||
Component: lazy(() => import('@/pages/admin/whitelist')),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: Routes.AdminRoles,
|
|
||||||
Component: lazy(() => import('@/pages/admin/roles')),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: Routes.AdminMonitoring,
|
|
||||||
Component: lazy(() => import('@/pages/admin/monitoring')),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
errorElement: <FallbackComponent />,
|
} satisfies RouteObject,
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const routers = createBrowserRouter(routeConfig, {
|
const routers = createBrowserRouter(routeConfig, {
|
||||||
|
|||||||
@ -157,6 +157,13 @@ export const createUser = (email: string, password: string) =>
|
|||||||
username: email,
|
username: email,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const grantSuperuser = (email: string) =>
|
||||||
|
request.put<ResponseData<void>>(api.adminSetSuperuser(email));
|
||||||
|
|
||||||
|
export const revokeSuperuser = (email: string) =>
|
||||||
|
request.delete<ResponseData<void>>(api.adminSetSuperuser(email));
|
||||||
|
|
||||||
export const getUserDetails = (email: string) =>
|
export const getUserDetails = (email: string) =>
|
||||||
request.get<ResponseData<[AdminService.UserDetail]>>(
|
request.get<ResponseData<[AdminService.UserDetail]>>(
|
||||||
adminGetUserDetails(email),
|
adminGetUserDetails(email),
|
||||||
|
|||||||
@ -263,6 +263,8 @@ export default {
|
|||||||
adminLogout: `${ExternalApi}${api_host}/admin/logout`,
|
adminLogout: `${ExternalApi}${api_host}/admin/logout`,
|
||||||
adminListUsers: `${ExternalApi}${api_host}/admin/users`,
|
adminListUsers: `${ExternalApi}${api_host}/admin/users`,
|
||||||
adminCreateUser: `${ExternalApi}${api_host}/admin/users`,
|
adminCreateUser: `${ExternalApi}${api_host}/admin/users`,
|
||||||
|
adminSetSuperuser: (username: string) =>
|
||||||
|
`${ExternalApi}${api_host}/admin/users/${username}/admin`,
|
||||||
adminGetUserDetails: (username: string) =>
|
adminGetUserDetails: (username: string) =>
|
||||||
`${ExternalApi}${api_host}/admin/users/${username}`,
|
`${ExternalApi}${api_host}/admin/users/${username}`,
|
||||||
adminUpdateUserStatus: (username: string) =>
|
adminUpdateUserStatus: (username: string) =>
|
||||||
|
|||||||
@ -13,7 +13,8 @@ const storage = {
|
|||||||
return localStorage.getItem(UserInfo);
|
return localStorage.getItem(UserInfo);
|
||||||
},
|
},
|
||||||
getUserInfoObject: () => {
|
getUserInfoObject: () => {
|
||||||
return JSON.parse(localStorage.getItem('userInfo') || '');
|
const userInfoStr = localStorage.getItem(UserInfo);
|
||||||
|
return userInfoStr ? JSON.parse(userInfoStr) : null;
|
||||||
},
|
},
|
||||||
setAuthorization: (value: string) => {
|
setAuthorization: (value: string) => {
|
||||||
localStorage.setItem(Authorization, value);
|
localStorage.setItem(Authorization, value);
|
||||||
|
|||||||
Reference in New Issue
Block a user