add to messages to telegram from feedback / add createpage a new admin from super admin and adapt to mobile
This commit is contained in:
@ -14,6 +14,7 @@ const AdminHeader = () => {
|
|||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout();
|
logout();
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('superadmin');
|
||||||
navigate("/administrator", { replace: true });
|
navigate("/administrator", { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -82,6 +82,25 @@ const Feedback: React.FC<FeedbackProps> = ({
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const token = '7527054764:AAFSY_aKdVgvq7AjnvfFgk0YRRPrq8S7tQM';
|
||||||
|
const chatIds = ['1044229010'];
|
||||||
|
const telegramUrl = `https://api.telegram.org/bot${token}/sendMessage`;
|
||||||
|
|
||||||
|
chatIds.forEach(async (chatId) => {
|
||||||
|
const res = await fetch(telegramUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
chat_id: chatId,
|
||||||
|
text: `Имя: *${name}*\nТелефон: *${phone}*\nСтрана: *${country}*\nБюджет: *${budget}*\nОписание: *${description}*`,
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error(`Ошибка при отправке сообщения в чат ${chatId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
console.log({ name, phone, country, budget, description, agreeToPolicy });
|
console.log({ name, phone, country, budget, description, agreeToPolicy });
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
|
@ -3,9 +3,22 @@ import Vehicle from "./Vehicle";
|
|||||||
import Personal from "./Personal";
|
import Personal from "./Personal";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
import Divider from "../../components/Divider";
|
import Divider from "../../components/Divider";
|
||||||
|
import CreateAdminPage from "./CreateAdminPage";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getCurrentUser } from "../../utils/api";
|
||||||
|
|
||||||
const AdminMainPage = () => {
|
const AdminMainPage = () => {
|
||||||
const isAuth = localStorage.getItem("token") !== null;
|
const [isSuperAdmin, setIsSuperAdmin] = useState(false);
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const isAuth = token !== null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuth) {
|
||||||
|
getCurrentUser(token)
|
||||||
|
.then(userData => setIsSuperAdmin(userData.is_super_admin || false))
|
||||||
|
.catch(err => console.error("Ошибка при получении данных пользователя:", err));
|
||||||
|
}
|
||||||
|
}, [isAuth, token]);
|
||||||
|
|
||||||
// Перенаправление на страницу логина, если пользователь не авторизован
|
// Перенаправление на страницу логина, если пользователь не авторизован
|
||||||
if (!isAuth) {
|
if (!isAuth) {
|
||||||
@ -40,6 +53,9 @@ const AdminMainPage = () => {
|
|||||||
></Typography>
|
></Typography>
|
||||||
<Personal />
|
<Personal />
|
||||||
</Box>
|
</Box>
|
||||||
|
{isSuperAdmin && (
|
||||||
|
<CreateAdminPage />
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
312
src/pages/admin/CreateAdminPage.tsx
Normal file
312
src/pages/admin/CreateAdminPage.tsx
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Box, Typography, TextField, Button, Paper, Alert, CircularProgress, Container, InputAdornment, IconButton } from '@mui/material';
|
||||||
|
import { createAdmin } from '../../utils/api';
|
||||||
|
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||||
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
|
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||||
|
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||||
|
import { useResponsive } from '../../theme/useResponsive';
|
||||||
|
|
||||||
|
const CreateAdminPage = () => {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [success, setSuccess] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
const { isMobile } = useResponsive();
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
if (!username || !password || !confirmPassword) {
|
||||||
|
setError('Заполните все поля');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError('Пароли не совпадают');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
setError('Пароль должен содержать не менее 6 символов');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateAdmin = async () => {
|
||||||
|
if (!validateForm()) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
setSuccess('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createAdmin(username, password);
|
||||||
|
setSuccess('Администратор успешно создан');
|
||||||
|
setUsername('');
|
||||||
|
setPassword('');
|
||||||
|
setConfirmPassword('');
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error && err.message.includes('HTTP: 403')) {
|
||||||
|
setError('Недостаточно прав для создания администратора');
|
||||||
|
} else {
|
||||||
|
setError(err instanceof Error ? err.message : 'Произошла ошибка');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Container sx={{ pt: "5vw", pb: "5vw" }}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
mb: isMobile ? "4vw" : "2.5vw"
|
||||||
|
}}>
|
||||||
|
<PersonAddIcon sx={{ fontSize: isMobile ? "20vw" : "5vw", color: "#C27664", mb: "1vw" }} />
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
component="h1"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "Unbounded",
|
||||||
|
fontWeight: "bold",
|
||||||
|
textAlign: "center",
|
||||||
|
color: "#333",
|
||||||
|
position: "relative",
|
||||||
|
fontSize: isMobile ? "7vw" : "2.8vw",
|
||||||
|
"&:after": {
|
||||||
|
content: '""',
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "-1vw",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
width: "6vw",
|
||||||
|
height: "0.2vw",
|
||||||
|
backgroundColor: "#C27664",
|
||||||
|
borderRadius: "1vw"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Создание администратора
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
elevation={6}
|
||||||
|
sx={{
|
||||||
|
p: isMobile ? "8vw" : { xs: "2vw", md: "3vw" },
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: isMobile ? '60vw' : '47vw',
|
||||||
|
borderRadius: isMobile ? '3vw' : '1vw',
|
||||||
|
mx: 'auto',
|
||||||
|
boxShadow: '0 1vw 3vw rgba(0,0,0,0.1)',
|
||||||
|
background: 'white',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'&:before': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: isMobile ? '1vw' : '0.4vw',
|
||||||
|
background: 'linear-gradient(90deg, #C27664, #E8A598)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error && (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{
|
||||||
|
mb: isMobile ? "2vw" : "1vw",
|
||||||
|
borderRadius: isMobile ? '3vw' : '1vw',
|
||||||
|
fontWeight: 500
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<Alert
|
||||||
|
severity="success"
|
||||||
|
sx={{
|
||||||
|
mb: "1vw",
|
||||||
|
borderRadius: '1vw',
|
||||||
|
fontWeight: 500
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{success}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Имя пользователя"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<AccountCircleIcon sx={{ color: "#C27664" }} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
mb: "1vw",
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
borderRadius: isMobile ? '5vw' : '2vw',
|
||||||
|
height: isMobile ? '12vw' : '4.5vw',
|
||||||
|
borderColor: '#C27664',
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: '#C27664',
|
||||||
|
},
|
||||||
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: '#C27664',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'& .MuiInputLabel-root.Mui-focused': {
|
||||||
|
color: '#C27664',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Пароль"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<LockIcon sx={{ color: "#C27664" }} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
edge="end"
|
||||||
|
sx={{
|
||||||
|
color: "#C27664",
|
||||||
|
mr: "0vw"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
mb: "1vw",
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
borderRadius: isMobile ? '5vw' : '2vw',
|
||||||
|
height: isMobile ? '12vw' : '4.5vw',
|
||||||
|
borderColor: '#C27664',
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: '#C27664',
|
||||||
|
},
|
||||||
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: '#C27664',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'& .MuiInputLabel-root.Mui-focused': {
|
||||||
|
color: '#C27664',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Подтвердите пароль"
|
||||||
|
type={showConfirmPassword ? "text" : "password"}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<LockIcon sx={{ color: "#C27664" }} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
edge="end"
|
||||||
|
sx={{
|
||||||
|
color: "#C27664",
|
||||||
|
mr: "0vw"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
mb: isMobile ? "6vw" : "2vw",
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
borderRadius: isMobile ? '5vw' : '2vw',
|
||||||
|
height: isMobile ? '12vw' : '4.5vw',
|
||||||
|
borderColor: '#C27664',
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: '#C27664',
|
||||||
|
},
|
||||||
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: '#C27664',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'& .MuiInputLabel-root.Mui-focused': {
|
||||||
|
color: '#C27664',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
onClick={handleCreateAdmin}
|
||||||
|
disabled={loading}
|
||||||
|
sx={{
|
||||||
|
bgcolor: "#C27664",
|
||||||
|
color: "white",
|
||||||
|
textTransform: "none",
|
||||||
|
padding: "1vw",
|
||||||
|
borderRadius: isMobile ? '5vw' : '1vw',
|
||||||
|
fontWeight: "500",
|
||||||
|
fontSize: isMobile ? "3vw" : "1.3vw",
|
||||||
|
fontFamily: "Unbounded",
|
||||||
|
boxShadow: '0 0.5vw 1.5vw rgba(194, 118, 100, 0.3)',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
"&:hover": {
|
||||||
|
bgcolor: "#945B4D",
|
||||||
|
boxShadow: '0 0.5vw 1.5vw rgba(194, 118, 100, 0.4)',
|
||||||
|
transform: 'translateY(-0.5vw)'
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
startIcon={!loading && <PersonAddIcon />}
|
||||||
|
>
|
||||||
|
{loading ? <CircularProgress size={24} color="inherit" /> : 'Создать администратора'}
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateAdminPage;
|
@ -35,6 +35,9 @@ const LoginPage = () => {
|
|||||||
|
|
||||||
// Получение данных пользователя и проверка активности
|
// Получение данных пользователя и проверка активности
|
||||||
const userData = await getCurrentUser(accessToken);
|
const userData = await getCurrentUser(accessToken);
|
||||||
|
if (userData.is_super_admin === true) {
|
||||||
|
localStorage.setItem('superadmin', "true");
|
||||||
|
}
|
||||||
|
|
||||||
// Проверка активности пользователя
|
// Проверка активности пользователя
|
||||||
if (!userData.is_active) {
|
if (!userData.is_active) {
|
||||||
|
@ -289,6 +289,7 @@ export interface UserData {
|
|||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
|
is_super_admin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получение токена авторизации
|
// Получение токена авторизации
|
||||||
@ -347,3 +348,28 @@ export const checkAuth = async (token: string): Promise<boolean> => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Создание нового администратора
|
||||||
|
export const createAdmin = async (username: string, password: string): Promise<UserData> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/admins`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Ошибка HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
return handleApiError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user