Projekt

Obecné

Profil

« Předchozí | Další » 

Revize b523c74d

Přidáno uživatelem Michal Schwob před asi 2 roky(ů)

Creation of administration page and user registration
re #9627

Zobrazit rozdíly:

backend/docker-compose.yml
19 19
  app: # Spring boot application
20 20
    build: .
21 21
    container_name: app-backend  # name of the container
22
    image: schwobik/backend-app:1.2
22
    image: schwobik/backend-app:1.5
23 23
    ports:
24 24
      - "8080:8080"                 # expose port 8080 out of the docker container do the local machine
25 25
    depends_on:
......
31 31
#     Since our Dockerfile for web-server is located in react-app folder, our build context is ./react-app
32 32
    build: ../frontend
33 33
    container_name: frontend
34
    image: schwobik/frontend-app:1.2
34
    image: schwobik/frontend-app:1.5
35 35
    ports:
36 36
      - "80:80"
37 37

  
frontend/src/App.css
36 36
    transform: rotate(360deg);
37 37
  }
38 38
}
39

  
40
.clicked-user {
41
  background: #A28253;
42
  color: white;
43
}
frontend/src/App.tsx
16 16
import EditHome from "./features/Home/EditHome"
17 17
import Notification from './features/Notification/Notification'
18 18
import { Fragment } from 'react'
19
import Administration from "./features/Administration/Administration"
20
import Register from "./features/Auth/Register"
19 21

  
20 22
const App = () => {
21 23
    return (
......
33 35
                                element={<RoutedCatalogItemDetail />}
34 36
                            />
35 37
                            <Route path="/login" element={<Login />} />
38
                            <Route path="/register" element={<Register />} />
36 39
                            <Route path="/logout" element={<Logout />} />
37 40
                            <Route path="/map" element={<TrackingTool />} />
41
                            <Route path="/administration" element={<Administration />}/>
38 42
                            <Route path="*" element={<NotFound />} />
39 43
                        </Routes>
40 44
                    </Box>
frontend/src/features/Administration/Administration.tsx
1
import {
2
    Button,
3
    Container, Grid,
4
    Link,
5
    List,
6
    ListItem,
7
    ListItemIcon,
8
    ListItemText,
9
    NativeSelect,
10
    Paper,
11
    Select, SelectChangeEvent
12
} from '@mui/material'
13
import {Fragment, ReactNode, useEffect, useState} from 'react'
14
import { useDispatch, useSelector } from 'react-redux'
15
import { RootState } from '../redux/store'
16
import axiosInstance from "../../api/api"
17
import 'react-quill/dist/quill.snow.css'
18
import ContentLoading from "../Reusables/ContentLoading"
19
import {UserDto} from "../../swagger/data-contracts"
20
import UserDetail from "./UserDetail"
21
import EditIcon from "@mui/icons-material/Edit"
22
import {Link as RouterLink} from "react-router-dom"
23
import AddIcon from '@mui/icons-material/Add';
24

  
25

  
26

  
27
const apiError =
28
    'Error while fetching data from the server, please try again later.'
29

  
30
const Administration = () => {
31
    const [users, setUsers] = useState<UserDto[] | undefined>(undefined)
32
    const [selectedUser, setSelectedUser] = useState<UserDto | undefined>(undefined)
33
    const [selectedUserID, setSelectedUserID] = useState<number | undefined>(undefined)
34
    const [isContentLoading, setIsContentLoading] = useState(true)
35
    const [err, setErr] = useState<string | undefined>(undefined)
36

  
37
    const isAdmin = useSelector(
38
        (state: RootState) => state.user.roles.includes("ROLE_ADMIN")
39
    )
40

  
41
    const handleClick = (userID: number) => {
42
        console.log("clicked " + userID)
43
        if (users) {
44
            setSelectedUser(users.at(userID))
45
            setSelectedUserID(userID)
46
        }
47
    }
48

  
49
    // Function to fetch the item from the api
50
    const fetchItem = async () => {
51
        try {
52
            const { data, status } = await axiosInstance.get(
53
                `/users`
54
            )
55
            if (status !== 200) {
56
                setErr(apiError)
57
                return
58
            }
59

  
60
            setUsers(data)
61
            setIsContentLoading(false)
62
        } catch (err: any) {
63
            setErr(apiError)
64
        }
65
    }
66

  
67
    // Fetch the item from the api after mounting the component
68
    useEffect(() => {
69
        fetchItem()
70
    }, [])
71

  
72
    const childClosed = () => {
73
        setSelectedUser(undefined)
74
        setSelectedUserID(undefined)
75
        fetchItem()
76
    }
77

  
78
    return (
79
        <Fragment>
80

  
81
            {isContentLoading && !err ? <ContentLoading /> : null}
82
            {!isContentLoading && users && isAdmin ? (
83
                <Grid container justifyContent="space-around">
84
                    <Grid item xs={3} md={2} sx={{ px: 2 }}>
85
                        <Paper style={{ minHeight: '80vh', display:'flex', justifyContent: 'space-between', flexDirection:'column'}} variant="outlined">
86
                            <List>
87
                                {users.map((user, id) => (
88
                                    <ListItem
89
                                        key={id}
90
                                        className={
91
                                            selectedUserID === id
92
                                                ? 'clicked-user'
93
                                                : ''
94
                                        }
95
                                        onClick={() => handleClick(id)}
96
                                    >
97
                                        <ListItemText primary={user.name} />
98
                                    </ListItem>
99
                                ))}
100
                            </List>
101
                            <Button startIcon={<AddIcon />}
102
                                    variant="contained"
103
                                    component={RouterLink}
104
                                    to="/register"
105
                                    color="primary"
106
                                    sx={{ m: 2 }} >
107
                                Add user
108
                            </Button>
109
                        </Paper>
110
                    </Grid>
111
                    <Grid item md={10} xs={9}>
112
                        {selectedUser ?
113
                            <UserDetail user={selectedUser} onClose={childClosed} />
114
                        : null }
115
                    </Grid>
116
                </Grid>
117
            ) : null}
118
        </Fragment>
119
    )
120
}
121

  
122
export default Administration
frontend/src/features/Administration/UserDetail.tsx
1
import {
2
    Button,
3
    Container, Divider, Grid,
4
    Checkbox,
5
    Link,
6
    List,
7
    ListItem,
8
    ListItemIcon,
9
    ListItemText,
10
    NativeSelect,
11
    Paper,
12
    Select, SelectChangeEvent, Typography, FormGroup, FormControlLabel
13
} from '@mui/material'
14
import React, {ChangeEvent, Fragment, ReactNode, useEffect, useState} from 'react'
15
import {useDispatch, useSelector} from 'react-redux'
16
import {RootState} from '../redux/store'
17
import axiosInstance from "../../api/api"
18
import 'react-quill/dist/quill.snow.css'
19
import ContentLoading from "../Reusables/ContentLoading"
20
import {Link as RouterLink, useNavigate} from "react-router-dom"
21
import EditIcon from '@mui/icons-material/Edit'
22
import {formatHtmlStringToReactDom} from "../../utils/formatting/HtmlUtils"
23
import {PermissionDto, UserDto} from "../../swagger/data-contracts"
24
import {Label} from "@mui/icons-material"
25
import ClearIcon from "@mui/icons-material/Clear"
26

  
27
export interface UserDetailProps {
28
    user: UserDto,
29
    onClose: () => void
30
}
31

  
32
const UserDetail = (props: UserDetailProps) => {
33
    const [user] = useState(props.user)
34
    const [canRead, setCanRead] = useState(user.permissions?.canRead)
35
    const [canWrite, setCanWrite] = useState(user.permissions?.canWrite)
36
    const [canDelete, setCanDelete] = useState(user.permissions?.canDelete)
37

  
38

  
39
    // Maps user property to corresponding table row
40
    const mapToRow = (rowName: string, item: string) => (
41
        <Fragment>
42
            <Grid sx={{my: 2}} container justifyContent="space-around">
43
                <Grid item xs={4} sx={{px: 1}}>
44
                    <Typography fontWeight={500}>{rowName}</Typography>
45
                </Grid>
46
                <Grid item xs={8} sx={{ml: 'auto'}}>
47
                    <Typography key={item}>{item}</Typography>
48
                </Grid>
49
            </Grid>
50
        </Fragment>
51
    )
52

  
53
    const rows = [
54
        {
55
            rowName: 'Name:',
56
            item: [props.user?.name],
57
        },
58
        {
59
            rowName: 'E-mail:',
60
            item: user?.email,
61
        }
62
    ]
63
    const rights = {
64
        canWrite: 'Add and edit locations',
65
        canDelete: 'Delete locations',
66
        canRead: 'Access to tracking tool',
67
    }
68

  
69

  
70
    const deleteUser = async () => {
71
        const { data, status } = await axiosInstance.delete(
72
            `/users/${user.email}`
73
        )
74
        if (status !== 200) {
75
            // TODO dodělat zpracování erroru
76
            return
77
        }
78
        props.onClose();
79
    }
80

  
81
    return (
82
        <Fragment>
83
            <Paper style={{minHeight: '80vh'}} variant="outlined">
84
                <Grid container justifyContent="space-around">
85
                    <Grid item xs={6} md={6} sx={{p: 2}}>
86
                        <Typography fontWeight={800}>Information</Typography>
87
                        {rows.map((row) => (
88

  
89
                            <Fragment>
90
                                {mapToRow(
91
                                    row.rowName as string,
92
                                    row.item as string
93
                                )}
94
                            </Fragment>
95
                        ))}
96
                    </Grid>
97
                    <Grid item xs={6} md={6} sx={{p: 2}}>
98
                        <b>Rights</b>
99
                            <FormGroup>
100
                                <FormControlLabel
101
                                    control={
102
                                        <Checkbox
103
                                            checked={canWrite}
104
                                            onChange={e => setCanWrite(e.target.checked)}
105
                                        />}
106
                                    label={rights.canWrite}/>
107
                                <FormControlLabel
108
                                    control={
109
                                        <Checkbox
110
                                            checked={canRead}
111
                                            onChange={e => setCanRead(e.target.checked)}
112
                                        />}
113
                                    label={rights.canRead}/>
114
                                <FormControlLabel
115
                                    control={
116
                                        <Checkbox
117
                                            checked={canDelete}
118
                                            onChange={e => setCanDelete(e.target.checked)}
119
                                        />}
120
                                    label={rights.canDelete}/>
121
                            </FormGroup>
122
                    </Grid>
123
                </Grid>
124
                <Button startIcon={<ClearIcon />}
125
                        variant="contained"
126
                        color="primary"
127
                        onClick={deleteUser}
128
                        sx={{ m: 2 }} >
129
                    Delete
130
                </Button>
131
            </Paper>
132
        </Fragment>
133
    )
134
}
135

  
136
export default UserDetail
frontend/src/features/Auth/Register.tsx
1
import { Fragment, useEffect } from 'react'
2
import { useSelector } from 'react-redux'
3
import { useNavigate } from 'react-router-dom'
4
import { RootState } from '../redux/store'
5
import LoginDialog from './LoginDialog'
6
import NotAuthorized from "../NotAuthorized/NotAuthorized"
7
import RegisterDialog from "./RegisterDialog"
1 8

  
2
export default {}
9

  
10
const Register = () => {
11
    const isAdmin = useSelector(
12
        (state: RootState) => state.user.roles.includes("ROLE_ADMIN")
13
    )
14

  
15
    return (
16
        <Fragment>
17
            {isAdmin ? (<RegisterDialog />) : <NotAuthorized />}
18
        </Fragment>
19
    )
20
}
21

  
22
export default Register
frontend/src/features/Auth/RegisterDialog.tsx
1
import { useFormik } from "formik"
1
import { Fragment, FunctionComponent, useEffect, useState } from 'react'
2
import Dialog, { DialogProps } from '@mui/material/Dialog'
3
import {
4
    Button,
5
    DialogContent,
6
    Link,
7
    TextField,
8
    Typography,
9
} from '@mui/material'
10
import { useFormik } from 'formik'
11
import * as yup from 'yup'
12
import { useDispatch, useSelector } from 'react-redux'
13
import { Link as RouterLink, useNavigate } from 'react-router-dom'
14
import {logIn, register} from './userThunks'
15
import { RootState } from '../redux/store'
16
import { resetLoggingIn } from './userSlice'
17
import { Box } from '@mui/system'
2 18

  
3
const register = () => {
19
export interface RegisterDialogProps {
20
    maxWidth?: DialogProps['maxWidth']
21
}
22

  
23
const RegisterDialog: FunctionComponent<RegisterDialogProps> = ({
24
                                                              maxWidth,
25
                                                          }) => {
26
    const [open, setOpen] = useState(true)
27

  
28
    const dispatch = useDispatch()
29
    const navigate = useNavigate()
30
    dispatch(resetLoggingIn())
31
    const validationSchema = yup.object().shape({
32
        username: yup.string().required('Username is required'),
33
        password: yup.string().required('Password is required'),
34
        confirmationPassword: yup.string().oneOf([yup.ref('password'), null], 'Passwords must match'),
35
        email: yup.string().required('Email is required'),
36

  
37
    })
4 38

  
5
    return <></>
39
    const formik = useFormik({
40
        initialValues: {
41
            username: '',
42
            password: '',
43
            confirmationPassword: '',
44
            email: '',
45
        },
46
        validationSchema,
47
        onSubmit: () => {
48
            dispatch(
49
                register({
50
                    username: formik.values.username,
51
                    passwords: {
52
                        password: formik.values.password,
53
                        confirmationPassword: formik.values.confirmationPassword,
54
                    },
55
                    permissions: {
56
                        canRead: false,
57
                        canWrite: false,
58
                        canDelete: false,
59
                    },
60
                    email: formik.values.email,
61
                })
62
            )
63
        },
64
    })
65

  
66
    const onCancel = () => {
67
        formik.resetForm()
68
        navigate('/Administration')
69
    }
70

  
71
    return (
72
        <Fragment>
73
            <Dialog
74
                open={open}
75
                fullWidth={true}
76
                onClose={onCancel}
77
                maxWidth="md"
78
            >
79
                <Typography sx={{ ml: 2, mt: 2 }} variant="h5" fontWeight="600">
80
                    Register new user
81
                </Typography>
82
                <DialogContent>
83
                    <form onSubmit={formik.handleSubmit}>
84
                        <TextField
85
                            fullWidth
86
                            label="Name"
87
                            name="username"
88
                            sx={{ mb: 2 }}
89
                            value={formik.values.username}
90
                            onChange={formik.handleChange}
91
                            error={
92
                                Boolean(formik.errors.username) &&
93
                                formik.touched.username
94
                            }
95
                            helperText={
96
                                formik.errors.username &&
97
                                formik.touched.username &&
98
                                formik.errors.username
99
                            }
100
                        />
101
                        <TextField
102
                            fullWidth
103
                            label="E-mail"
104
                            name="email"
105
                            type="email"
106
                            sx={{ mb: 2 }}
107
                            value={formik.values.email}
108
                            onChange={formik.handleChange}
109
                            error={
110
                                Boolean(formik.errors.email) &&
111
                                formik.touched.email
112
                            }
113
                            helperText={
114
                                formik.errors.email &&
115
                                formik.touched.email &&
116
                                formik.errors.email
117
                            }
118
                        />
119
                        <TextField
120
                            fullWidth
121
                            label="Password"
122
                            name="password"
123
                            type="password"
124
                            sx={{ mb: 2 }}
125
                            value={formik.values.password}
126
                            onChange={formik.handleChange}
127
                            error={
128
                                Boolean(formik.errors.password) &&
129
                                formik.touched.password
130
                            }
131
                            helperText={
132
                                formik.errors.password &&
133
                                formik.touched.password &&
134
                                formik.errors.password
135
                            }
136
                        />
137
                        <TextField
138
                            fullWidth
139
                            label="Confirm Password"
140
                            name="confirmationPassword"
141
                            type="password"
142
                            sx={{ mb: 2 }}
143
                            value={formik.values.confirmationPassword}
144
                            onChange={formik.handleChange}
145
                            error={
146
                                Boolean(formik.errors.confirmationPassword) &&
147
                                formik.touched.confirmationPassword
148
                            }
149
                            helperText={
150
                                formik.errors.confirmationPassword &&
151
                                formik.touched.confirmationPassword &&
152
                                formik.errors.confirmationPassword
153
                            }
154
                        />
155
                        <Fragment>
156
                            <Button
157
                                sx={{mb: 2}}
158
                                type="submit"
159
                                variant="contained"
160
                                fullWidth
161
                            >
162
                                Register
163
                            </Button>
164
                        </Fragment>
165
                    </form>
166

  
167
                    <Box sx={{mb: 1}} />
168
                </DialogContent>
169
            </Dialog>
170
        </Fragment>
171
    )
6 172
}
173

  
174
export default RegisterDialog
frontend/src/features/Auth/userThunks.ts
6 6
const loginError =
7 7
    'Server error occurred while logging in. Please contact help service to resolve this issue or try again later.'
8 8

  
9
const registerError =
10
    'Server error occurred while registering. Please contact help service to resolve this issue or try again later.'
11

  
9 12
// This is not present in the swagger since spring generates
10 13
export interface UserLogin {
11 14
    username: string,
12 15
    password: string
13 16
}
14 17

  
18
export interface UserRegister {
19
    username: string,
20
    passwords: {
21
        password: string,
22
        confirmationPassword: string,
23
    },
24
    permissions: {
25
        canRead: boolean,
26
        canWrite: boolean,
27
        canDelete: boolean,
28
    },
29
    email: string,
30
}
31

  
15 32
export const logIn = createAsyncThunk(
16 33
    'user/login',
17 34
    async (userDto: UserLogin, { getState }) => {
......
50 67
        }
51 68
    }
52 69
)
70

  
71
export const register = createAsyncThunk(
72
    'user/register',
73
    async (userDto: UserRegister) => {
74
        try {
75
            console.log(userDto.passwords.password)
76
            console.log(userDto.passwords.confirmationPassword)
77
            const { data, status } = await axiosInstance.post('/users', userDto)
78
            if (status !== 200) {
79
                // TODO read API err
80
                return Promise.reject(loginError)
81
            }
82
            return status
83
        } catch (err: any) {
84
            return Promise.reject(loginError)
85
        }
86
    }
87
)
frontend/src/features/Home/EditHome.tsx
23 23
    const navigate = useNavigate()
24 24

  
25 25
    const updateContent = (updated: string) => {
26
        console.log("updateContent: " + updated)
27 26
        if (content) {
28 27
            content["content"] = updated
29 28
        }
......
84 83
    {!isContentLoading && content ? (
85 84
        <div>
86 85
            <h1>Home</h1>
87
        <form onSubmit={handleSubmit}>
88
    <input type="submit" value="Submit" />
86
        <form>
87
            <Button type="button" onClick={handleSubmit}>Submit</Button>
89 88
    <ReactQuill
90 89
        theme='snow'
91 90
        value={content["content"]}
frontend/src/features/Home/Home.tsx
1
import {Button, Paper} from '@mui/material'
1
import {Button, Container, Paper} from '@mui/material'
2 2
import {Fragment, useEffect, useState} from 'react'
3 3
import { useDispatch, useSelector } from 'react-redux'
4 4
import { logout } from '../Auth/userSlice'
......
28 28
        navigate(path)
29 29
    }
30 30

  
31
    const roles = useSelector(
32
        (state: RootState) => state.user.roles
31
    const isAdmin = useSelector(
32
        (state: RootState) => state.user.roles.includes("ROLE_ADMIN")
33 33
    )
34

  
35
    const isAdmin = roles.includes("ROLE_ADMIN")
36

  
37 34
    // Fetch the item from the api after mounting the component
38 35
    useEffect(() => {
39 36
        // Function to fetch the item from the api
40
        console.log("useEffect called")
41 37
        const fetchItem = async () => {
42 38
            try {
43 39
                const { data, status } = await axiosInstance.get(
......
55 51
            }
56 52
        }
57 53
        fetchItem()
58
    }, [roles])
54
    }, [])
59 55

  
60 56
    return (
61 57
        <Fragment>
62
            <Paper style={{ minHeight: '100vh' }} variant="outlined">
58
            <Paper style={{ minHeight: '80vh', display:'flex', justifyContent: 'space-between', flexDirection:'column'}} variant="outlined">
63 59
                {isContentLoading && !err ? <ContentLoading /> : null}
64 60
                {!isContentLoading && content ? (
65
                    <div>
61
                    <Container>
66 62
                        <h1>Home</h1>
67 63
                        {formatHtmlStringToReactDom(content["content"] as string)}
68
                    </div>
64
                    </Container>
69 65
                ) : null}
70
                {isAdmin ? (
71
                        <Button
72
                            startIcon={<EditIcon />}
73
                            variant="contained"
74
                            component={RouterLink}
75
                            to="/editHome"
76
                            color="primary"
77
                            sx={{ mb: 2 }}
78
                        >
79
                            Edit
80
                        </Button>
81 66

  
67
                {isAdmin ? (
68
                    <Container sx={{ mb: 3 }} >
69
                            <Button
70
                                startIcon={<EditIcon />}
71
                                variant="contained"
72
                                component={RouterLink}
73
                                to="/editHome"
74
                                color="primary"
75
                                sx={{ mb: 2 }}
76
                            >
77
                                Edit
78
                            </Button>
79
                    </Container>
82 80
                ) : null}
83 81
            </Paper>
84 82
        </Fragment>
frontend/src/features/Navigation/navigationMenuItems.ts
8 8
import { SvgIconTypeMap } from '@mui/material'
9 9
import DataSaverOffIcon from '@mui/icons-material/DataSaverOff'
10 10
import LogoutIcon from '@mui/icons-material/Logout'
11
import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount'
11 12

  
12 13
export interface NavigationMenuItem {
13 14
    name: string
......
79 80
        icon: DataSaverOffIcon,
80 81
        position: 4,
81 82
    },
83
    {
84
        name: 'Administration',
85
        path: '/administration',
86
        accessibleTo: loggedInAccess,
87
        icon: SupervisorAccountIcon,
88
        position: 6,
89
    },
82 90
    {
83 91
        name: 'Logout',
84 92
        path: '/logout',
frontend/src/features/NotAuthorized/NotAuthorized.tsx
1
import { Button, Stack } from "@mui/material"
2
import { Link as RouterLink } from 'react-router-dom'
3

  
4
const NotAuthorized = () => {
5

  
6
    return (
7
        <Stack alignItems={"center"} sx={{my: 4}}>
8
            <h1>Not Authorized 😔</h1>
9
            <Button variant="outlined" component={RouterLink} to="/" >Return Home</Button>
10
        </Stack>
11
    )
12
}
13

  
14
export default NotAuthorized
frontend/src/features/Theme/ThemeWrapper.tsx
16 16
        secondary: {
17 17
            main: '#E3DBCF',
18 18
        },
19
        selected: {
20
            main: '#C0C0C0'
21
        }
19 22
    }
20 23

  
21 24
    const darkThemePalette = {

Také k dispozici: Unified diff