Доработана работа с пользователями - загрузка аватара. Добавлены ограничения по правам на админской странице.
This commit is contained in:
parent
1d09dbadf3
commit
e254ef0dc0
@ -13,6 +13,8 @@ import { Footer } from './components/Footer';
|
||||
import { AuthGuard } from './components/AuthGuard';
|
||||
import { ImportArticlesPage } from "./pages/ImportArticlesPage";
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';
|
||||
|
||||
function App() {
|
||||
const { setUser, setLoading } = useAuthStore();
|
||||
|
||||
@ -20,7 +22,7 @@ function App() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setLoading(true);
|
||||
axios.get('http://localhost:5000/api/auth/me', {
|
||||
axios.get(`${API_URL}/api/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
.then(response => {
|
||||
|
@ -15,29 +15,29 @@ export function ArticleCard({ article, featured = false }: ArticleCardProps) {
|
||||
<article className={`overflow-hidden rounded-lg shadow-lg transition-transform hover:scale-[1.02] ${
|
||||
featured ? 'col-span-2 row-span-2' : ''
|
||||
}`}>
|
||||
<div className="relative">
|
||||
<div className="relative pt-7">
|
||||
<img
|
||||
src={article.coverImage}
|
||||
alt={article.title}
|
||||
className={`w-full object-cover ${featured ? 'h-96' : 'h-64'}`}
|
||||
/>
|
||||
<div className="absolute top-4 left-4 flex gap-2">
|
||||
<span className="bg-white/90 px-3 py-1 rounded-full text-sm font-medium">
|
||||
<div className="absolute top-1 left-0 flex gap-2 z-10">
|
||||
<span className="bg-gray-100 shadow px-3 py-1 rounded-full text-sm font-medium">
|
||||
{categoryName}
|
||||
</span>
|
||||
<span className="bg-white/90 px-3 py-1 rounded-full text-sm font-medium flex items-center">
|
||||
<span className="bg-gray-100 shadow px-3 py-1 rounded-full text-sm font-medium flex items-center">
|
||||
<MapPin size={14} className="mr-1" />
|
||||
{CityTitles[article.cityId]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<img
|
||||
src={article.author.avatarUrl}
|
||||
alt={article.author.displayName}
|
||||
className="w-10 h-10 rounded-full"
|
||||
className="w-12 h-12 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">{article.author.displayName}</p>
|
||||
|
@ -16,7 +16,7 @@ export function FeaturedSection() {
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Загрузка статей с backend
|
||||
// Загрузка статей
|
||||
useEffect(() => {
|
||||
const fetchArticles = async () => {
|
||||
try {
|
||||
|
@ -4,7 +4,20 @@ import TextAlign from '@tiptap/extension-text-align';
|
||||
import Blockquote from '@tiptap/extension-blockquote';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { Bold, Italic, List, ListOrdered, Quote, Redo, Undo, AlignLeft, AlignCenter, Plus, Minus, Text } from 'lucide-react';
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
Redo,
|
||||
Undo,
|
||||
AlignLeft,
|
||||
AlignCenter,
|
||||
Plus,
|
||||
Minus,
|
||||
SquareUser
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
||||
@ -334,7 +347,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
||||
onClick={handleClick}
|
||||
className="p-1 rounded hover:bg-gray-200"
|
||||
>
|
||||
<Text size={18} />
|
||||
<SquareUser size={18} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
@ -1,19 +1,25 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import axios from "axios";
|
||||
import { TipTapEditor } from '../components/TipTapEditor';
|
||||
import { Header } from '../components/Header';
|
||||
import { GalleryManager } from '../components/GalleryManager';
|
||||
import { CoverImageUpload } from '../components/ImageUpload/CoverImageUpload.tsx';
|
||||
import { CoverImageUpload } from '../components/ImageUpload/CoverImageUpload';
|
||||
import { ImageUploader } from '../components/ImageUpload/ImageUploader';
|
||||
import MinutesWord from '../components/MinutesWord';
|
||||
import { GalleryImage, CategoryTitles, CityTitles, CategoryIds, CityIds, Article } from '../types';
|
||||
import { GalleryImage, CategoryTitles, CityTitles, CategoryIds, CityIds, Article, Author } from '../types';
|
||||
import { Pencil, Trash2, ChevronLeft, ChevronRight, ImagePlus, X, ToggleLeft, ToggleRight} from 'lucide-react';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
|
||||
const allCategoryIds: number[] = CategoryIds;
|
||||
const allCityIds: number[] = CityIds;
|
||||
|
||||
// Обложка по умоланию для новых статей
|
||||
const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp';
|
||||
|
||||
export function AdminPage() {
|
||||
const { user } = useAuthStore();
|
||||
const isAdmin = user?.permissions.isAdmin || false;
|
||||
|
||||
const [articleId, setArticleId] = useState('');
|
||||
const [title, setTitle] = useState('');
|
||||
const [excerpt, setExcerpt] = useState('');
|
||||
@ -34,6 +40,28 @@ export function AdminPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [content, setContent] = useState('');
|
||||
const [showDraftOnly, setShowDraftOnly] = useState(false);
|
||||
const [authors, setAuthors] = useState<Author[]>([]);
|
||||
const [authorId, setAuthorId] = useState<string>('');
|
||||
|
||||
// Загрузка списка авторов
|
||||
useEffect(() => {
|
||||
const fetchAuthors = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/users/' , {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAuthors(response.data);
|
||||
} catch (err) {
|
||||
console.error('Ошибка загрузки авторов:', err);
|
||||
setError('Не удалось загрузить список авторов.');
|
||||
}
|
||||
};
|
||||
|
||||
fetchAuthors();
|
||||
}, []);
|
||||
|
||||
// Загрузка статей
|
||||
useEffect(() => {
|
||||
@ -62,6 +90,35 @@ export function AdminPage() {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// Фильтр для категорий и городов, основанный на разрешениях пользователя
|
||||
const availableCategoryIds = useMemo(() => {
|
||||
if (!user) return [];
|
||||
if (isAdmin) return allCategoryIds;
|
||||
|
||||
return allCategoryIds.filter(categoryId =>
|
||||
user.permissions.categories[categoryId] &&
|
||||
(user.permissions.categories[categoryId].create || user.permissions.categories[categoryId].edit)
|
||||
);
|
||||
}, [user, isAdmin]);
|
||||
|
||||
const availableCityIds = useMemo(() => {
|
||||
if (!user) return [];
|
||||
if (isAdmin) return allCityIds;
|
||||
|
||||
return user.permissions.cities;
|
||||
}, [user, isAdmin]);
|
||||
|
||||
// Установка категории и города по умолчанию на основе доступных параметров
|
||||
useEffect(() => {
|
||||
if (availableCategoryIds.length > 0 && !availableCategoryIds.includes(categoryId)) {
|
||||
setCategoryId(availableCategoryIds[0]);
|
||||
}
|
||||
|
||||
if (availableCityIds.length > 0 && !availableCityIds.includes(cityId)) {
|
||||
setCityId(availableCityIds[0]);
|
||||
}
|
||||
}, [availableCategoryIds, availableCityIds, categoryId, cityId]);
|
||||
|
||||
// Редактирование статьи - загрузка данных
|
||||
const handleEdit = (id: string) => {
|
||||
const article = articles.find(a => a.id === id);
|
||||
@ -73,8 +130,9 @@ export function AdminPage() {
|
||||
setCityId(article.cityId);
|
||||
setCoverImage(article.coverImage);
|
||||
setReadTime(article.readTime);
|
||||
setAuthorId(article.author.id);
|
||||
setGallery(article.gallery || []);
|
||||
setContent(article.content); // Устанавливаем содержимое редактора
|
||||
setContent(article.content);
|
||||
setEditingId(id);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
@ -129,6 +187,22 @@ export function AdminPage() {
|
||||
// Создание новой статьи, сохранение существующей
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
//const selectedAuthor = authors.find(author => author.id === authorId) || authors[0];
|
||||
|
||||
let selectedAuthor;
|
||||
|
||||
if (editingId) {
|
||||
if (isAdmin) {
|
||||
selectedAuthor = authors.find(author => author.id === authorId) || authors[0];
|
||||
} else {
|
||||
// Пользователь без административных прав не может менять автора
|
||||
const originalArticle = articles.find(a => a.id === editingId);
|
||||
selectedAuthor = originalArticle?.author || authors[0];
|
||||
}
|
||||
} else {
|
||||
// Для новой статьи всегда использовать текущего пользователя
|
||||
selectedAuthor = user;
|
||||
}
|
||||
|
||||
const articleData = {
|
||||
title,
|
||||
@ -141,6 +215,7 @@ export function AdminPage() {
|
||||
content: content || '',
|
||||
importId: 0,
|
||||
isActive: false,
|
||||
author: selectedAuthor
|
||||
};
|
||||
|
||||
if (editingId) {
|
||||
@ -182,16 +257,28 @@ export function AdminPage() {
|
||||
setArticleId('');
|
||||
setTitle('');
|
||||
setExcerpt('');
|
||||
if (availableCategoryIds.length > 0) {
|
||||
setCategoryId(availableCategoryIds[0]);
|
||||
}
|
||||
if (availableCityIds.length > 0) {
|
||||
setCityId(availableCityIds[0]);
|
||||
}
|
||||
/*
|
||||
setCategoryId(1);
|
||||
setCityId(1);
|
||||
*/
|
||||
setCoverImage(DEFAULT_COVER_IMAGE);
|
||||
setReadTime(5);
|
||||
setAuthorId(authors[0].id || '');
|
||||
setGallery([]);
|
||||
setContent(''); // Очищаем содержимое редактора
|
||||
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
// Проверка прав пользователя
|
||||
const hasNoPermissions = availableCategoryIds.length === 0 || availableCityIds.length === 0;
|
||||
|
||||
const handleGalleryImageUpload = (imageUrl: string) => {
|
||||
const newImage: GalleryImage = {
|
||||
id: Date.now().toString(),
|
||||
@ -206,6 +293,17 @@ export function AdminPage() {
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{hasNoPermissions ? (
|
||||
<div className="bg-white rounded-lg shadow-sm p-6 mb-8 text-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
Недостаточно прав
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-4">
|
||||
У вас нет прав на создание и редактирование статей в любой категории и городе.
|
||||
Свяжитесь с администраторм чтобы получить права.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-lg shadow-sm p-6 mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">
|
||||
{editingId ? 'Редактировать статью' : 'Создать новую статью'}
|
||||
@ -246,7 +344,7 @@ export function AdminPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label htmlFor="category" className="block text-sm font-medium text-gray-700">
|
||||
<span className="italic font-bold">Категория</span>
|
||||
@ -257,7 +355,7 @@ export function AdminPage() {
|
||||
onChange={(e) => setCategoryId(Number(e.target.value))}
|
||||
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
|
||||
>
|
||||
{CategoryIds.map((cat) => (
|
||||
{availableCategoryIds.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{CategoryTitles[cat]}
|
||||
</option>
|
||||
@ -275,7 +373,7 @@ export function AdminPage() {
|
||||
onChange={(e) => setCityId(Number(e.target.value))}
|
||||
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
|
||||
>
|
||||
{CityIds.map((c) => (
|
||||
{availableCityIds.map((c) => (
|
||||
<option key={c} value={c}>
|
||||
{CityTitles[c]}
|
||||
</option>
|
||||
@ -296,6 +394,27 @@ export function AdminPage() {
|
||||
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{editingId && isAdmin && (
|
||||
<div>
|
||||
<label htmlFor="author" className="block text-sm font-medium text-gray-700">
|
||||
<span className="italic font-bold">Автор</span>
|
||||
</label>
|
||||
<select
|
||||
id="author"
|
||||
value={authorId}
|
||||
onChange={(e) => setAuthorId(e.target.value)}
|
||||
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
|
||||
>
|
||||
{authors.map((author) => (
|
||||
<option key={author.id} value={author.id}>
|
||||
{author.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
<CoverImageUpload
|
||||
@ -317,14 +436,16 @@ export function AdminPage() {
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
<span className="italic font-bold">Фото галерея</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowGalleryUploader(true)}
|
||||
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
<ImagePlus size={16} className="mr-2" />
|
||||
Загрузить фото
|
||||
</button>
|
||||
{editingId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowGalleryUploader(true)}
|
||||
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
<ImagePlus size={16} className="mr-2" />
|
||||
Загрузить фото
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<GalleryManager
|
||||
images={gallery}
|
||||
@ -340,10 +461,17 @@ export function AdminPage() {
|
||||
setEditingId(null);
|
||||
setTitle('');
|
||||
setExcerpt('');
|
||||
setCategoryId(1);
|
||||
setCityId(1);
|
||||
if (availableCategoryIds.length > 0) {
|
||||
setCategoryId(availableCategoryIds[0]);
|
||||
}
|
||||
if (availableCityIds.length > 0) {
|
||||
setCityId(availableCityIds[0]);
|
||||
}
|
||||
//setCategoryId(1);
|
||||
//setCityId(1);
|
||||
setCoverImage(DEFAULT_COVER_IMAGE);
|
||||
setReadTime(5);
|
||||
setAuthorId(authors[0].id || '');
|
||||
setGallery([]);
|
||||
setContent('');
|
||||
}}
|
||||
@ -356,11 +484,12 @@ export function AdminPage() {
|
||||
type="submit"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
{editingId ? 'Изменить' : 'Созранить черновик'}
|
||||
{editingId ? 'Изменить' : 'Сохранить черновик'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Модальная загрузка изображений галереи */}
|
||||
{showGalleryUploader && (
|
||||
@ -438,7 +567,7 @@ export function AdminPage() {
|
||||
{articles.map((article) => (
|
||||
<li
|
||||
key={article.id}
|
||||
className={`px-6 py-4 ${!article.isActive ? 'bg-gray-50' : ''}`}
|
||||
className={`px-6 py-4 ${!article.isActive ? 'bg-gray-200' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
@ -448,6 +577,15 @@ export function AdminPage() {
|
||||
<p className="text-sm text-gray-500">
|
||||
· {CategoryTitles[article.categoryId]} · {CityTitles[article.cityId]} · {article.readTime} <MinutesWord minutes={article.readTime}/> чтения
|
||||
</p>
|
||||
<div className="flex items-center text-xs text-gray-500 mt-1">
|
||||
<img
|
||||
src={article.author.avatarUrl}
|
||||
alt={article.author.displayName}
|
||||
className="h-4 w-4 rounded-full mr-1"
|
||||
/>
|
||||
<span className="italic font-bold"> {article.author.displayName}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="flex items-center gap-4 ml-4">
|
||||
<button
|
||||
|
@ -4,12 +4,13 @@ import { AuthGuard } from '../components/AuthGuard';
|
||||
import { UserFormData } from '../types/auth';
|
||||
import { useUserManagement } from '../hooks/useUserManagement';
|
||||
import { ImagePlus, X, UserPlus, Pencil } from 'lucide-react';
|
||||
import { uploadImage } from '../services/imageService';
|
||||
import { imageResolutions } from '../config/imageResolutions';
|
||||
import {CategoryIds, CategoryTitles, CityIds} from "../types";
|
||||
import { CategoryIds, CategoryTitles, CityIds, CityTitles } from "../types";
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
const initialFormData: UserFormData = {
|
||||
id: '',
|
||||
email: '',
|
||||
password: '',
|
||||
displayName: '',
|
||||
@ -27,7 +28,7 @@ export function UserManagementPage() {
|
||||
handleCityChange,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser
|
||||
// deleteUser
|
||||
} = useUserManagement();
|
||||
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
@ -35,7 +36,7 @@ export function UserManagementPage() {
|
||||
const [formData, setFormData] = useState<UserFormData>(initialFormData);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>, userId: string) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
@ -43,11 +44,23 @@ export function UserManagementPage() {
|
||||
const resolution = imageResolutions.find(r => r.id === 'thumbnail');
|
||||
if (!resolution) throw new Error('Invalid resolution');
|
||||
|
||||
const uploadedImage = await uploadImage(file, resolution);
|
||||
setFormData(prev => ({ ...prev, avatarUrl: uploadedImage.url }));
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('resolutionId', resolution.id);
|
||||
formData.append('folder', 'users/' + userId);
|
||||
|
||||
// Отправка запроса на сервер
|
||||
const response = await axios.post('/api/images/upload-url', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`, // Передача токена аутентификации
|
||||
},
|
||||
});
|
||||
|
||||
setFormData(prev => ({ ...prev, avatarUrl: response.data?.fileUrl }));
|
||||
} catch (error) {
|
||||
setFormError('Failed to upload avatar. Please try again.');
|
||||
console.error('Upload error:', error);
|
||||
setFormError('Ошибка загрузки аватара. Повторите попытку.');
|
||||
console.error('Ошибка загрузки:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -129,6 +142,7 @@ export function UserManagementPage() {
|
||||
onClick={() => {
|
||||
setSelectedUser(user);
|
||||
setFormData({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
password: '',
|
||||
displayName: user.displayName,
|
||||
@ -160,9 +174,9 @@ export function UserManagementPage() {
|
||||
Права по категориям
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{CategoryIds.map((category) => (
|
||||
<div key={category} className="border rounded-lg p-4">
|
||||
<h4 className="font-medium mb-2">{CategoryTitles[category]}</h4>
|
||||
{CategoryIds.map((categoryId) => (
|
||||
<div key={categoryId} className="border rounded-lg p-4">
|
||||
<h4 className="font-medium mb-2">{CategoryTitles[categoryId]}</h4>
|
||||
<div className="space-y-2">
|
||||
{(['create', 'edit', 'delete'] as const).map((action) => (
|
||||
<label
|
||||
@ -172,11 +186,11 @@ export function UserManagementPage() {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
selectedUser.permissions.categories[CategoryTitles[category]]?.[action] ?? false
|
||||
selectedUser.permissions.categories[categoryId]?.[action] ?? false
|
||||
}
|
||||
onChange={(e) =>
|
||||
handlePermissionChange(
|
||||
category,
|
||||
categoryId,
|
||||
action,
|
||||
e.target.checked
|
||||
)
|
||||
@ -213,7 +227,7 @@ export function UserManagementPage() {
|
||||
}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{city}</span>
|
||||
<span className="text-sm text-gray-700">{CityTitles[city]}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@ -231,7 +245,7 @@ export function UserManagementPage() {
|
||||
<div className="bg-white rounded-lg max-w-md w-full p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
{showCreateModal ? 'Create New User' : 'Edit User'}
|
||||
{showCreateModal ? 'Создание нового' : 'Редактирование'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
@ -255,22 +269,22 @@ export function UserManagementPage() {
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Avatar
|
||||
Аватар
|
||||
</label>
|
||||
<div className="mt-1 flex items-center space-x-4">
|
||||
<img
|
||||
src={formData.avatarUrl || '/images/avatar.jpg'}
|
||||
alt="User avatar"
|
||||
alt="Аватар пользователя"
|
||||
className="h-12 w-12 rounded-full object-cover"
|
||||
/>
|
||||
<label className="cursor-pointer inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||
<ImagePlus size={16} className="mr-2" />
|
||||
Change Avatar
|
||||
Сменить аватар
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarUpload}
|
||||
onChange={(e) => handleAvatarUpload(e, formData.id)} // Передаем userId
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@ -278,7 +292,7 @@ export function UserManagementPage() {
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Display Name
|
||||
Имя
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -291,7 +305,7 @@ export function UserManagementPage() {
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Email
|
||||
E-mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@ -304,7 +318,7 @@ export function UserManagementPage() {
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Password {showEditModal && '(leave blank to keep current)'}
|
||||
Пароль {showEditModal && '(оставить пустым чтобы использовать текущий)'}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@ -326,13 +340,13 @@ export function UserManagementPage() {
|
||||
}}
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{showCreateModal ? 'Create User' : 'Save Changes'}
|
||||
{showCreateModal ? 'Создать' : 'Сохранить'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -11,12 +11,12 @@ export const galleryService = {
|
||||
size: number;
|
||||
format: string
|
||||
}) => {
|
||||
const { data } = await axios.post(`/gallery/article/${articleId}`, imageData);
|
||||
const { data } = await axios.post(`/api/gallery/article/${articleId}`, imageData);
|
||||
return data;
|
||||
},
|
||||
|
||||
updateImage: async (id: string, updates: Partial<GalleryImage>) => {
|
||||
const { data } = await axios.put(`/gallery/${id}`, updates);
|
||||
const { data } = await axios.put(`/api/gallery/${id}`, updates);
|
||||
return data;
|
||||
},
|
||||
|
||||
@ -25,11 +25,11 @@ export const galleryService = {
|
||||
},
|
||||
|
||||
reorderImages: async (articleId: string, imageIds: string[]) => {
|
||||
await axios.post(`/gallery/article/${articleId}/reorder`, { imageIds });
|
||||
await axios.post(`/api/gallery/article/${articleId}/reorder`, { imageIds });
|
||||
},
|
||||
|
||||
getArticleGallery: async (articleId: string) => {
|
||||
const { data } = await axios.get(`/gallery/article/${articleId}`);
|
||||
const { data } = await axios.get(`/api/gallery/article/${articleId}`);
|
||||
return data as GalleryImage[];
|
||||
}
|
||||
};
|
@ -7,8 +7,8 @@ export const userService = {
|
||||
const response = await axios.get('/users');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
throw new Error('Failed to fetch users');
|
||||
console.error('Ошибка получения списка пользователей:', error);
|
||||
throw new Error('Ошибка получения списка пользователей');
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -6,6 +6,8 @@ export interface UserPermissions {
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export type PermissionAction = 'edit' | 'create' | 'delete';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
@ -15,6 +17,7 @@ export interface User {
|
||||
}
|
||||
|
||||
export interface UserFormData {
|
||||
id: string
|
||||
email: string;
|
||||
password: string;
|
||||
displayName: string;
|
||||
|
@ -1,31 +1,53 @@
|
||||
import { Category, City } from '../types';
|
||||
import { User } from '../types/auth';
|
||||
import { PermissionAction, User } from '../types/auth';
|
||||
|
||||
export const checkPermission = (
|
||||
user: User,
|
||||
category: Category,
|
||||
action: 'create' | 'edit' | 'delete'
|
||||
categoryId: string,
|
||||
action: PermissionAction
|
||||
): boolean => {
|
||||
// Если пользователь админ — ему разрешено всё
|
||||
if (user.permissions.isAdmin) return true;
|
||||
return !!user.permissions.categories[category.name]?.[action];
|
||||
|
||||
// Проверяем, есть ли такая категория в permissions
|
||||
const categoryPermissions = user.permissions.categories[categoryId];
|
||||
|
||||
// Если категория отсутствует или в ней нет действия — запрет
|
||||
return categoryPermissions ? categoryPermissions[action] : false;
|
||||
};
|
||||
|
||||
export const checkCityAccess = (user: User, city: City): boolean => {
|
||||
export const checkCityAccess = (user: User, cityId: number): boolean => {
|
||||
if (user.permissions.isAdmin) return true;
|
||||
return user.permissions.cities.includes(city);
|
||||
return user.permissions.cities.includes(cityId);
|
||||
};
|
||||
|
||||
export const getDefaultPermissions = () => ({
|
||||
categories: {
|
||||
Film: { create: false, edit: false, delete: false },
|
||||
Theater: { create: false, edit: false, delete: false },
|
||||
Music: { create: false, edit: false, delete: false },
|
||||
Sports: { create: false, edit: false, delete: false },
|
||||
Art: { create: false, edit: false, delete: false },
|
||||
Legends: { create: false, edit: false, delete: false },
|
||||
Anniversaries: { create: false, edit: false, delete: false },
|
||||
Memory: { create: false, edit: false, delete: false }
|
||||
1: { create: false, edit: false, delete: false },
|
||||
2: { create: false, edit: false, delete: false },
|
||||
3: { create: false, edit: false, delete: false },
|
||||
4: { create: false, edit: false, delete: false },
|
||||
5: { create: false, edit: false, delete: false },
|
||||
6: { create: false, edit: false, delete: false },
|
||||
7: { create: false, edit: false, delete: false },
|
||||
8: { create: false, edit: false, delete: false }
|
||||
},
|
||||
cities: [],
|
||||
isAdmin: false
|
||||
});
|
||||
});
|
||||
|
||||
export const getUserAvailableCategories = (user: User): number[] => {
|
||||
if (!user) return [];
|
||||
if (user.permissions.isAdmin) return [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
|
||||
return Object.entries(user.permissions.categories)
|
||||
.filter(([, permissions]) => permissions.create || permissions.edit) // Убрали `_`
|
||||
.map(([categoryId]) => Number(categoryId))
|
||||
.filter((categoryId) => !isNaN(categoryId));
|
||||
};
|
||||
|
||||
export const getUserAvailableCities = (user: User): number[] => {
|
||||
if (!user) return [];
|
||||
if (user.permissions.isAdmin) return [1, 2];
|
||||
|
||||
return user.permissions.cities;
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user