This commit is contained in:
Kilian Hofmann 2024-07-30 21:20:51 +02:00
parent b1061e67ac
commit 0fbbfdc997
30 changed files with 394 additions and 62 deletions

File diff suppressed because one or more lines are too long

9
exam/dist/assets/index-SI5snNZz.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,10 +23,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GuestBook</title>
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-Cag5GO1b.js"></script>
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-SI5snNZz.js"></script>
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C9_qfvjK.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-BnAUJOoN.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-BqkrhB-y.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DpDh5IPY.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/zustand-DKxXQGKw.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-Be01V9yD.js">
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">

View File

@ -20,6 +20,9 @@
"NotAllowed_postUpdate": "Keine Berechtigung",
"NotFound_post:postUpdate": "Post nicht gefunden",
"NotAllowed_delete|PermissionsUser": "Keine Berechtigung",
"NotFound_user:delete|PermissionsUser": "Benutzer nicht gefunden",
"Duplicate_user:register": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert schon",
"username": "Benutzername",
@ -101,5 +104,13 @@
"Password confirm": "Passwort bestätigen",
"Password match": "Passwörter stimmen nicht überein",
"Change password": "Passwort ändern"
"Change password": "Passwort ändern",
"Confirm user delete title": "Diesen User löschen?",
"Confirm user delete body": "Möchtest du {{name}} wirklich Löschen?",
"Manage users": "Benutzer verwalten",
"Admin": "Administration",
"Make Admin": "Administratorrecht erteilen",
"Demote Admin": "Administratorrecht entziehen"
}

View File

@ -20,6 +20,9 @@
"NotAllowed_postUpdate": "Not allowed",
"NotFound_post:postUpdate": "Post not found",
"NotAllowed_delete|PermissionsUser": "Not allowed",
"NotFound_user:deleteUserdelete|PermissionsUser": "User not found",
"Duplicate_user:register": "A user with this username or email already exists",
"username": "username",
@ -102,5 +105,13 @@
"Password confirm": "Confirm password",
"Password match": "Password do not match",
"Change password": "Change password"
"Change password": "Change password",
"Confirm user delete title": "Diesen User löschen?",
"Confirm user delete body": "Möchtest du {{name}} wirklich Löschen?",
"Manage users": "Manage users",
"Admin": "Administration",
"Make Admin": "Make Admin",
"Demote Admin": "Demote Admin"
}

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
{
"name": "react",
"private": true,
"version": "1.1.1",
"version": "3.0.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -20,6 +20,9 @@
"NotAllowed_postUpdate": "Keine Berechtigung",
"NotFound_post:postUpdate": "Post nicht gefunden",
"NotAllowed_delete|PermissionsUser": "Keine Berechtigung",
"NotFound_user:delete|PermissionsUser": "Benutzer nicht gefunden",
"Duplicate_user:register": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert schon",
"username": "Benutzername",
@ -101,5 +104,13 @@
"Password confirm": "Passwort bestätigen",
"Password match": "Passwörter stimmen nicht überein",
"Change password": "Passwort ändern"
"Change password": "Passwort ändern",
"Confirm user delete title": "Diesen User löschen?",
"Confirm user delete body": "Möchtest du {{name}} wirklich Löschen?",
"Manage users": "Benutzer verwalten",
"Admin": "Administration",
"Make Admin": "Administratorrecht erteilen",
"Demote Admin": "Administratorrecht entziehen"
}

View File

@ -20,6 +20,9 @@
"NotAllowed_postUpdate": "Not allowed",
"NotFound_post:postUpdate": "Post not found",
"NotAllowed_delete|PermissionsUser": "Not allowed",
"NotFound_user:deleteUserdelete|PermissionsUser": "User not found",
"Duplicate_user:register": "A user with this username or email already exists",
"username": "username",
@ -102,5 +105,13 @@
"Password confirm": "Confirm password",
"Password match": "Password do not match",
"Change password": "Change password"
"Change password": "Change password",
"Confirm user delete title": "Diesen User löschen?",
"Confirm user delete body": "Möchtest du {{name}} wirklich Löschen?",
"Manage users": "Manage users",
"Admin": "Administration",
"Make Admin": "Make Admin",
"Demote Admin": "Demote Admin"
}

View File

@ -1,8 +1,17 @@
import { createContext, FC, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react';
import { ERRORS } from '../components/Error/Errors';
import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
import useGuestBookStore from '../store/store';
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
import { User, UserCreate, UserImageUpdate, UserUpdate } from '../types/User';
import {
User,
UserCreate,
UserDelete,
UserImageUpdate,
UserList,
UserPermissionsUpdate,
UserUpdate,
} from '../types/User';
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
@ -19,11 +28,14 @@ interface ApiContext {
updatePost?: (data: PostUpdate, id: number) => Promise<PostAuth>;
deletePost?: (id: number) => Promise<PostDelete>;
users?: (page?: number) => Promise<UserList>;
user?: (id?: number) => Promise<User>;
createUser?: (data: UserCreate) => Promise<User>;
confirmUser?: (code: string) => Promise<User>;
updateUser?: (data: UserUpdate, id?: number) => Promise<User>;
updateUserImage?: (data: UserImageUpdate, id?: number) => Promise<User>;
updateUserPermissions?: (data: UserPermissionsUpdate, id: number) => Promise<User>;
deleteUser?: (id: number) => Promise<UserDelete>;
userPosts?: (id?: number) => Promise<PostListAuth>;
}
@ -47,11 +59,14 @@ export const useApi = () => {
updatePost,
deletePost,
users,
user,
createUser,
confirmUser,
updateUser,
updateUserImage,
updateUserPermissions,
deleteUser,
userPosts,
} = useContext(ApiContext);
@ -63,11 +78,14 @@ export const useApi = () => {
newPost &&
updatePost &&
deletePost &&
users &&
user &&
createUser &&
confirmUser &&
updateUser &&
updateUserImage &&
updateUserPermissions &&
deleteUser &&
userPosts
) {
return {
@ -82,11 +100,14 @@ export const useApi = () => {
updatePost,
deletePost,
users,
user,
createUser,
confirmUser,
updateUser,
updateUserImage,
updateUserPermissions,
deleteUser,
userPosts,
};
@ -157,6 +178,12 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
return await (await reAuth(() => _delete(`posts/${id}?l=${POST_LIMIT}`))).json();
};
const users = async (page?: number): Promise<UserList> => {
const url = `users?p=${page ?? 0}&l=${POST_LIMIT}`;
return await (await reAuth(() => getAuth(url))).json();
};
const user = async (id?: number): Promise<User> => {
return await (await reAuth(() => getAuth(`users/${id ?? authenticatedUser?.id}`))).json();
};
@ -187,10 +214,21 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
return _user;
};
const updateUserPermissions = async (data: UserPermissionsUpdate, id: number): Promise<User> => {
const _user = await (
await reAuth(() => patchAuth(`users/${id}/permissions`, data as Record<string, unknown>))
).json();
return _user;
};
const userPosts = async (id?: number): Promise<PostListAuth> => {
return await (await reAuth(() => getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`))).json();
};
const deleteUser = async (id: number): Promise<UserDelete> => {
return await (await reAuth(() => _delete(`users/${id}?l=${POST_LIMIT}`))).json();
};
/* IMPL */
const post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
@ -284,9 +322,13 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
const ret = await callback();
return ret;
} catch {
//eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log('[REAUTH] failed once', error);
if (error.code !== ERRORS.UNAUTHORIZED) throw error;
try {
console.log('[REAUTH] fail, refreshing');
console.log('[REAUTH] failed due to authentication, try refreshing session');
// REAUTH
await refresh();
// DO AGAIN
@ -294,13 +336,13 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
const ret = await callback();
return ret;
} catch (error) {
console.log('[REAUTH] terminating session', error);
} catch (_error) {
console.log('[REAUTH] terminating session', _error);
setAuthenticatedUser(undefined);
setHasAuth(false);
setCurrentSession([undefined, undefined]);
token.current = undefined;
throw error;
throw _error;
}
}
};
@ -332,11 +374,14 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
updatePost,
deletePost,
users,
user,
createUser,
confirmUser,
updateUser,
updateUserImage,
updateUserPermissions,
deleteUser,
userPosts,
}}

View File

@ -35,9 +35,7 @@ const PostEditDialog: FC<Props> = ({ post, open, onClose }) => {
const Api = useApi();
const updateMutation = useMutation({
mutationFn: ({ data, id }: { data: PostUpdate; id: number }) => {
return Api.updatePost(data, id);
},
mutationFn: ({ data, id }: { data: PostUpdate; id: number }) => Api.updatePost(data, id),
});
const form = useForm<PostUpdate>({

View File

@ -35,9 +35,7 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
const Api = useApi();
const createMutation = useMutation({
mutationFn: ({ data }: { data: UserCreate }) => {
return Api.createUser(data);
},
mutationFn: ({ data }: { data: UserCreate }) => Api.createUser(data),
});
const form = useForm<UserCreate & { passwordConfirm: string }>({

View File

@ -34,9 +34,7 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
const Api = useApi();
const updateMutation = useMutation({
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => {
return Api.updateUser(data, id);
},
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => Api.updateUser(data, id),
});
const form = useForm<UserUpdate>({

View File

@ -44,9 +44,7 @@ const UserImageDialog: FC<Props> = ({ user, open, onClose }) => {
const Api = useApi();
const updateMutation = useMutation({
mutationFn: ({ data, id }: { data: UserImageUpdate; id?: number }) => {
return Api.updateUserImage(data, id);
},
mutationFn: ({ data, id }: { data: UserImageUpdate; id?: number }) => Api.updateUserImage(data, id),
});
const form = useForm<UserImageUpdate>({

View File

@ -34,9 +34,7 @@ const UserPasswordDialog: FC<Props> = ({ user, open, onClose }) => {
const Api = useApi();
const updateMutation = useMutation({
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => {
return Api.updateUser(data, id);
},
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => Api.updateUser(data, id),
});
const form = useForm<UserUpdate & { passwordConfirm: string }>({

View File

@ -1,6 +1,6 @@
import { ErrorOutline } from '@mui/icons-material';
import { Grid, Link as MUILink, Typography } from '@mui/material';
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
import { useQueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorRouteComponent as TSErrorRouteComponent, useNavigate, useRouter } from '@tanstack/react-router';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
@ -13,10 +13,11 @@ const ErrorRouterComponent: TSErrorRouteComponent = ({ error }) => {
const router = useRouter();
const navigate = useNavigate();
const queryErrorResetBoundary = useQueryErrorResetBoundary();
const queryClient = useQueryClient();
useEffect(() => {
// Reset the query error boundary
console.log(queryErrorResetBoundary.isReset());
console.log(queryErrorResetBoundary);
queryErrorResetBoundary.reset();
}, [queryErrorResetBoundary]);
@ -34,10 +35,9 @@ const ErrorRouterComponent: TSErrorRouteComponent = ({ error }) => {
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
<MUILink
variant="h6"
underline="hover"
sx={{ cursor: 'pointer' }}
onClick={() => {
console.log('CLICK AS WELL');
queryClient.clear();
router.invalidate();
navigate({ to: ROUTES.INDEX });
}}

View File

@ -19,9 +19,7 @@ const PostForm: FC = () => {
const Api = useApi();
const newMutation = useMutation({
mutationFn: ({ data }: { data: PostCreate }) => {
return Api.newPost(data);
},
mutationFn: ({ data }: { data: PostCreate }) => Api.newPost(data),
});
const form = useForm<PostCreate>({

View File

@ -1,4 +1,4 @@
import { Box, Link, Menu, MenuItem, Typography } from '@mui/material';
import { Box, Divider, Link, Menu, MenuItem, Typography } from '@mui/material';
import { useMatch, useNavigate, useRouter } from '@tanstack/react-router';
import { t } from 'i18next';
import { FC, useState } from 'react';
@ -69,6 +69,14 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
>
{t('Log out')}
</MenuItem>,
Api.authenticatedUser.isAdmin && [
<Divider>
<Typography variant="caption">{t('Admin')}</Typography>
</Divider>,
<MenuItem key="users" onClick={() => navigate({ to: ROUTES.USERS })}>
{t('Manage users')}
</MenuItem>,
],
]
) : register ? (
<RegisterDialog open={register} onClose={() => setRegister(false)} />
@ -81,7 +89,6 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
<Link
sx={{ cursor: 'pointer' }}
variant="body1"
underline="hover"
color="secondary.main"
onClick={() => {
setRegister(true);

View File

@ -44,9 +44,7 @@ const Post: FC<Props> = ({ page = 0, post, disableActions }) => {
const Api = useApi();
const deleteMutation = useMutation({
mutationFn: (id: number) => {
return Api.deletePost(id);
},
mutationFn: (id: number) => Api.deletePost(id),
});
return (

View File

@ -41,11 +41,17 @@ const Profile: FC<Props> = ({ user, posts, canEdit }) => {
<CardContent>
<Grid container spacing={2}>
<Grid item sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center' }}>
<IconButton onClick={() => setImageOpen(true)}>
{canEdit ? (
<IconButton onClick={() => setImageOpen(true)}>
<Avatar alt={user.username} src={`${user.image}`} sx={{ width: '100px', height: '100px' }}>
<Person sx={{ width: '60px', height: '60px' }} />
</Avatar>
</IconButton>
) : (
<Avatar alt={user.username} src={`${user.image}`} sx={{ width: '100px', height: '100px' }}>
<Person sx={{ width: '60px', height: '60px' }} />
</Avatar>
</IconButton>
)}
</Grid>
<Grid item sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', columnGap: 1 }}>

View File

@ -0,0 +1,10 @@
import { queryOptions } from '@tanstack/react-query';
import { useApi } from '../api/Api';
import { ERRORS } from '../components/Error/Errors';
export const usersQueryOptions = (Api: ReturnType<typeof useApi>, page?: number) =>
queryOptions({
queryKey: ['users', { page: page ?? 0 }],
queryFn: async () => await Api.users(page),
retry: (count, error) => ('code' in error && error.code !== ERRORS.UNAUTHORIZED ? count < 3 : false),
});

View File

@ -12,6 +12,7 @@
import { Route as rootRoute } from './routes/__root'
import { Route as IndexImport } from './routes/index'
import { Route as UsersIndexImport } from './routes/users/index'
import { Route as ProfileIndexImport } from './routes/profile/index'
import { Route as ConfirmIndexImport } from './routes/confirm/index'
import { Route as ProfileIdImport } from './routes/profile/$id'
@ -23,6 +24,11 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute,
} as any)
const UsersIndexRoute = UsersIndexImport.update({
path: '/users/',
getParentRoute: () => rootRoute,
} as any)
const ProfileIndexRoute = ProfileIndexImport.update({
path: '/profile/',
getParentRoute: () => rootRoute,
@ -70,6 +76,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProfileIndexImport
parentRoute: typeof rootRoute
}
'/users/': {
id: '/users/'
path: '/users'
fullPath: '/users'
preLoaderRoute: typeof UsersIndexImport
parentRoute: typeof rootRoute
}
}
}
@ -80,6 +93,7 @@ export const routeTree = rootRoute.addChildren({
ProfileIdRoute,
ConfirmIndexRoute,
ProfileIndexRoute,
UsersIndexRoute,
})
/* prettier-ignore-end */
@ -93,7 +107,8 @@ export const routeTree = rootRoute.addChildren({
"/",
"/profile/$id",
"/confirm/",
"/profile/"
"/profile/",
"/users/"
]
},
"/": {
@ -107,6 +122,9 @@ export const routeTree = rootRoute.addChildren({
},
"/profile/": {
"filePath": "profile/index.tsx"
},
"/users/": {
"filePath": "users/index.tsx"
}
}
}

View File

@ -3,6 +3,8 @@ import { createRouter, ErrorRouteComponent, RouterProvider, useRouter } from '@t
import { useEffect } from 'react';
import { useApi } from './api/Api';
import ErrorRouterComponent from './components/Error/ErrorRouterComponent';
import NotFoundComponent from './components/NotFound/NotFoundComponent';
import { routeTree } from './routeTree.gen';
//TODO: REAUTH HERE
@ -51,6 +53,8 @@ const router = createRouter({
defaultPreloadStaleTime: 0,
basepath: process.env.NODE_ENV === 'development' ? 'phpCourse/exam/dist' : '/phpCourse/exam',
//defaultErrorComponent: Error,
defaultNotFoundComponent: NotFoundComponent,
defaultErrorComponent: ErrorRouterComponent,
});
// Register the router instance for type safety

View File

@ -3,8 +3,6 @@ import { QueryClient } from '@tanstack/react-query';
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
import { useApi } from '../api/Api';
import ErrorRouterComponent from '../components/Error/ErrorRouterComponent';
import NotFoundComponent from '../components/NotFound/NotFoundComponent';
const Root = () => {
return (
@ -17,6 +15,4 @@ const Root = () => {
export const Route = createRootRouteWithContext<{ queryClient: QueryClient; Api: ReturnType<typeof useApi> }>()({
component: Root,
notFoundComponent: NotFoundComponent,
errorComponent: ErrorRouterComponent,
});

View File

@ -15,9 +15,7 @@ const Home = () => {
const navigate = useNavigate();
const confirmMutation = useMutation({
mutationFn: ({ code: _code }: { code: string }) => {
return Api.confirmUser(_code);
},
mutationFn: ({ code: _code }: { code: string }) => Api.confirmUser(_code),
});
useEffect(() => {

View File

@ -30,7 +30,7 @@ const Home = () => {
useEffect(() => {
if ((page ?? 0) >= postsQuery.pages) {
navigate({ to: '/', search: { page: postsQuery.pages - 1 } });
navigate({ to: ROUTES.INDEX, search: { page: postsQuery.pages - 1 } });
}
}, [page]); //eslint-disable-line react-hooks/exhaustive-deps

View File

@ -0,0 +1,203 @@
import {
Alert,
Button,
Card,
CardActions,
CardContent,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
Grid,
Link as MUILink,
Pagination,
PaginationItem,
Snackbar,
Typography,
useMediaQuery,
useTheme,
} from '@mui/material';
import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute, Link, redirect, useNavigate } from '@tanstack/react-router';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useApi } from '../../api/Api';
import ErrorComponent from '../../components/Error/ErrorComponent';
import { ERRORS } from '../../components/Error/Errors';
import HeaderLayout from '../../components/Layouts/HeaderLayout';
import { usersQueryOptions } from '../../queries/usersQuery';
import { ROUTES } from '../../types/Routes';
import { UserPermissionsUpdate } from '../../types/User';
const Users = () => {
const [deleteOpen, setDeleteOpen] = useState(false);
//eslint-disable-next-line @typescript-eslint/no-explicit-any
const [_error, setError] = useState<any>();
const Api = useApi();
const { page } = Route.useSearch();
const { data: usersQuery, isFetching, failureReason, error } = useSuspenseQuery(usersQueryOptions(Api, page));
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const theme = useTheme();
const oneSibling = useMediaQuery(theme.breakpoints.not('xs'), { noSsr: true });
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
throw failureReason;
}
if (error && 'code' in error && error.code === ERRORS.UNAUTHORIZED) {
throw error;
}
const deleteMutation = useMutation({
mutationFn: (id: number) => Api.deleteUser(id),
});
const permissionMutation = useMutation({
mutationFn: ({ data, id }: { data: UserPermissionsUpdate; id: number }) => Api.updateUserPermissions(data, id),
});
useEffect(() => {
if ((page ?? 0) >= usersQuery.pages) {
navigate({ to: ROUTES.USERS, search: { page: usersQuery.pages - 1 } });
}
}, [page]); //eslint-disable-line react-hooks/exhaustive-deps
return (
<HeaderLayout>
<Snackbar open={isFetching} message={t('Updating')} />
<Grid container spacing={2} sx={{ marginTop: 0 }}>
{usersQuery.data.map((user) => (
<Grid item xs={12} key={user.id}>
<Card>
<CardContent>
{user.id !== Api.authenticatedUser?.id ? (
<MUILink component={Link} to="/profile/$id" params={{ id: user.id }}>
{user.username}
</MUILink>
) : (
<Typography>{user.username}</Typography>
)}
<Typography>{user.email}</Typography>
</CardContent>
{user.id !== Api.authenticatedUser?.id && (
<>
<CardActions>
<Button size="small" color="error" onClick={() => setDeleteOpen(true)}>
{t('Delete')}
</Button>
<Button
size="small"
onClick={() => {
permissionMutation.mutate(
{ data: { isAdmin: !user.isAdmin }, id: user.id },
{
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ['users'],
});
},
onError: setError,
}
);
}}
>
{t(user.isAdmin ? 'Demote Admin' : 'Make Admin')}
</Button>
<Dialog open={deleteOpen} onClose={() => setDeleteOpen(false)}>
<DialogTitle>{t('Confirm user delete title')}</DialogTitle>
<DialogContent>
<DialogContentText>{t('Confirm user delete body', { name: user.username })}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteOpen(false)} autoFocus variant="contained">
{t('No')}
</Button>
<Button
variant="outlined"
color="error"
onClick={() => {
deleteMutation.mutate(user.id, {
onSuccess: async (data) => {
await queryClient.invalidateQueries({
queryKey: ['users'],
});
if ((page ?? 0) >= data.pages)
navigate({ to: ROUTES.PROFILE, search: { page: data.pages - 1 } });
},
onError: setError,
});
setDeleteOpen(false);
}}
>
{t('Yes')}
</Button>
</DialogActions>
</Dialog>
</CardActions>
<Snackbar
open={deleteMutation.isError || permissionMutation.isError}
autoHideDuration={2000}
onClose={() => {
deleteMutation.reset();
permissionMutation.reset();
}}
TransitionProps={{
onExited: () => setError(undefined),
}}
>
<Alert severity="error" variant="filled" sx={{ width: '100%' }}>
{error && <ErrorComponent error={_error} context="delete|PermissionsUser" color="white" />}
</Alert>
</Snackbar>
<Snackbar open={deleteMutation.isPending} message={t('Deleting')} />
<Snackbar open={permissionMutation.isPending} message={t('Updating')} />
</>
)}
</Card>
</Grid>
))}
<Grid item xs={12}>
<Divider variant="middle" />
</Grid>
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
<Pagination
page={(page ?? 0) + 1}
count={usersQuery.pages}
siblingCount={oneSibling ? 1 : 0}
color="primary"
renderItem={(item) => (
<PaginationItem
{...item}
component={Link}
to="/"
search={{ page: (item.page ?? 0) > 0 ? (item.page ?? 1) - 1 : undefined }}
//eslint-disable-next-line @typescript-eslint/no-explicit-any
onClick={(e) => item.onClick(e as any)}
/>
)}
/>
</Grid>
</Grid>
</HeaderLayout>
);
};
export const Route = createFileRoute(`${ROUTES.USERS}/`)({
loaderDeps: ({ search: { page } }) => ({ page }),
loader: ({ context: { queryClient, Api }, deps: { page } }) =>
queryClient.ensureQueryData(usersQueryOptions(Api, page)),
validateSearch: (search: Record<string, unknown>): { page?: number } => {
return {
page: search?.page !== undefined ? Number(search?.page ?? 0) : undefined,
};
},
beforeLoad: ({ context: { Api } }) => {
if ((!Api.hasAuth && !Api.currentSession[0]) || !Api.authenticatedUser?.isAdmin)
throw redirect({ to: ROUTES.INDEX });
},
component: Users,
});

View File

@ -2,4 +2,5 @@ export enum ROUTES {
INDEX = '/',
PROFILE = '/profile',
CONFIRM = '/confirm',
USERS = '/users',
}

View File

@ -17,6 +17,10 @@ export interface UserUpdate {
password?: string;
}
export interface UserPermissionsUpdate {
isAdmin?: boolean;
}
export interface UserCreate {
username: string;
email: string;
@ -32,3 +36,13 @@ export interface UserImageUpdate {
image?: File;
predefined?: string;
}
export interface UserList {
pages: number;
data: User[];
}
export interface UserDelete {
pages: number;
user: User;
}