Admin
This commit is contained in:
parent
b1061e67ac
commit
0fbbfdc997
9
exam/dist/assets/index-Cag5GO1b.js
vendored
9
exam/dist/assets/index-Cag5GO1b.js
vendored
File diff suppressed because one or more lines are too long
9
exam/dist/assets/index-SI5snNZz.js
vendored
Normal file
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
4
exam/dist/index.html
vendored
4
exam/dist/index.html
vendored
@ -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">
|
||||
|
||||
13
exam/dist/locales/de/translation.json
vendored
13
exam/dist/locales/de/translation.json
vendored
@ -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"
|
||||
}
|
||||
|
||||
13
exam/dist/locales/en/translation.json
vendored
13
exam/dist/locales/en/translation.json
vendored
@ -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"
|
||||
}
|
||||
|
||||
2
exam/dist/stats.html
vendored
2
exam/dist/stats.html
vendored
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "react",
|
||||
"private": true,
|
||||
"version": "1.1.1",
|
||||
"version": "3.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
|
||||
@ -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>({
|
||||
|
||||
@ -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 }>({
|
||||
|
||||
@ -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>({
|
||||
|
||||
@ -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>({
|
||||
|
||||
@ -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 }>({
|
||||
|
||||
@ -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 });
|
||||
}}
|
||||
|
||||
@ -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>({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 }}>
|
||||
|
||||
10
exam/react/src/queries/usersQuery.ts
Normal file
10
exam/react/src/queries/usersQuery.ts
Normal 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),
|
||||
});
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
203
exam/react/src/routes/users/index.tsx
Normal file
203
exam/react/src/routes/users/index.tsx
Normal 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,
|
||||
});
|
||||
@ -2,4 +2,5 @@ export enum ROUTES {
|
||||
INDEX = '/',
|
||||
PROFILE = '/profile',
|
||||
CONFIRM = '/confirm',
|
||||
USERS = '/users',
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user