From 77cc7534a07cb6285ae816ba7537cc32a5e25e1f Mon Sep 17 00:00:00 2001 From: ghazall-ag Date: Sun, 9 Nov 2025 15:19:14 +0330 Subject: [PATCH] fix(users): update user role and edit API logic --- src/pages/Users.jsx | 388 +++++++++++++++------------ src/services/api.js | 619 ++++++++++++-------------------------------- vite.config.js | 31 ++- 3 files changed, 412 insertions(+), 626 deletions(-) diff --git a/src/pages/Users.jsx b/src/pages/Users.jsx index bf6d86f..04d3239 100644 --- a/src/pages/Users.jsx +++ b/src/pages/Users.jsx @@ -1,62 +1,92 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState, useCallback } from 'react'; import DataTable from '../components/DataTable'; -import { usersAPI } from '../services/api'; +import { usersAPI, rolesAPI } from '../services/api'; import { Plus, Trash2, Search, Pencil, ShieldOff, RefreshCcw } from 'lucide-react'; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +// debounce ساده +function debounce(func, delay) { + let timer; + return function (...args) { + clearTimeout(timer); + timer = setTimeout(() => func.apply(this, args), delay); + }; +} const Users = () => { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - const [firstName, setFirstName] = useState(''); - const [lastName, setLastName] = useState(''); - const [email, setEmail] = useState(''); const [filter, setFilter] = useState(''); + + const [addForm, setAddForm] = useState({ firstName: '', lastName: '', email: '' }); + const [editForm, setEditForm] = useState({ firstName: '', lastName: '', email: '' }); + const [isModalOpen, setIsModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [editingUser, setEditingUser] = useState(null); - const fetchUsers = async (q = '') => { + const [roles, setRoles] = useState([]); + const [rolesModalUser, setRolesModalUser] = useState(null); + const [selectedRoles, setSelectedRoles] = useState([]); + + // دریافت کاربران و رول‌ها + const fetchUsers = useCallback(async (q = '') => { try { setLoading(true); - const list = await usersAPI.list(q); - setUsers(Array.isArray(list) ? list : []); - } catch (e) { + const list = await usersAPI.list({ searchQuery: q }); + const usersWithRoles = await Promise.all( + (list || []).map(async (user) => { + const roles = await usersAPI.getRoles(user.id); // باید طبق ریسپانس نمونه برگرداند + return { ...user, roles }; + }) + ); + setUsers(usersWithRoles); + setError(''); + } catch (err) { + console.error(err); setError('Failed to load users'); } finally { setLoading(false); } - }; + }, []); + + const fetchRoles = useCallback(async () => { + const list = await rolesAPI.list(); // گرفتن تمام رول‌ها + setRoles(list); + }, []); useEffect(() => { fetchUsers(); - }, []); + fetchRoles(); + }, [fetchUsers, fetchRoles]); + // --- اضافه کردن کاربر --- const onAddUser = async (e) => { e.preventDefault(); + const { firstName, lastName, email } = addForm; if (!firstName.trim() || !lastName.trim() || !email.trim()) return; try { - await usersAPI.create({ firstName, lastName, email }); - setFirstName(''); - setLastName(''); - setEmail(''); + const newUser = await usersAPI.create(addForm); await fetchUsers(filter); setIsModalOpen(false); - } catch (_) { - setError('Failed to create user'); + setAddForm({ firstName: '', lastName: '', email: '' }); + toast.success(newUser?.Message || 'User added successfully'); + } catch (err) { + const msg = err?.response?.data?.Message || err?.message || 'Failed to create user'; + toast.error(msg); } }; - const onDelete = async (id) => { - if (!window.confirm('Are you sure you want to delete this user?')) return; - await usersAPI.remove(id); - await fetchUsers(filter); - }; - + // --- ویرایش کاربر --- const openEdit = (user) => { setEditingUser(user); - setFirstName(user.firstName || ''); - setLastName(user.lastName || ''); - setEmail(user.email || ''); + setEditForm({ + firstName: user.firstName || '', + lastName: user.lastName || '', + email: user.email || '', + }); setIsEditModalOpen(true); }; @@ -64,205 +94,227 @@ const Users = () => { e.preventDefault(); if (!editingUser) return; try { - await usersAPI.update(editingUser.id, { firstName, lastName, email }); + const updatedUser = await usersAPI.update(editingUser.id, editForm); await fetchUsers(filter); setIsEditModalOpen(false); setEditingUser(null); - setFirstName(''); - setLastName(''); - setEmail(''); - } catch (_) { - setError('Failed to update user'); + toast.success(updatedUser?.Message || 'User updated successfully'); + } catch (err) { + const msg = err?.response?.data?.Message || err?.message || 'Failed to update user'; + toast.error(msg); } }; + // --- حذف کاربر --- + const onDelete = async (id) => { + if (!window.confirm('Are you sure you want to delete this user?')) return; + try { + const result = await usersAPI.remove(id); + await fetchUsers(filter); + toast.success(result?.Message || 'User deleted successfully'); + } catch (err) { + const msg = err?.response?.data?.Message || err?.message || 'Failed to delete user'; + toast.error(msg); + } + }; + + // --- تغییر وضعیت فعال بودن --- const onToggleActivation = async (id) => { - await usersAPI.toggleActivation(id); - await fetchUsers(filter); + try { + const updated = await usersAPI.toggleActivation(id); + await fetchUsers(filter); + toast.success(updated?.Message || 'User status updated'); + } catch (err) { + const msg = err?.response?.data?.Message || err?.message || 'Failed to update status'; + toast.error(msg); + } }; + // --- ریست پسورد --- const onResetPassword = async (id) => { - await usersAPI.resetPassword(id); - alert('Password reset successfully.'); + try { + const res = await usersAPI.resetPassword(id); + toast.success(res?.Message || 'Password reset successfully'); + } catch (err) { + const msg = err?.response?.data?.Message || err?.message || 'Failed to reset password'; + toast.error(msg); + } }; + // --- مدیریت رول‌ها --- + const openRolesModal = (user) => { + setRolesModalUser(user); + setSelectedRoles(user.roles?.map(r => r.id) || []); + }; + + const onUpdateRoles = async () => { + if (!rolesModalUser) return; + try { + await usersAPI.updateRoles(rolesModalUser.id, selectedRoles); + toast.success('Roles updated successfully'); + setRolesModalUser(null); + fetchUsers(filter); + } catch (err) { + const msg = err?.response?.data?.Message || err?.message || 'Failed to update roles'; + toast.error(msg); + } + }; + + // --- ستون‌های جدول --- const columns = useMemo(() => [ { key: 'firstName', header: 'First Name' }, { key: 'lastName', header: 'Last Name' }, { key: 'email', header: 'Email' }, - { key: 'isActive', header: 'Active', render: (val) => val ? '✅' : '❌' }, + { + key: 'roles', + header: 'Roles', + render: (_val, row) => ( + + {row.roles?.length + ? row.roles.map(r => r.name).join(', ') + : '—'} + + ), + }, + { key: 'isActive', header: 'Active', render: (val) => (val ? '✅' : '❌') }, { key: 'actions', header: 'Actions', render: (_val, row) => ( -
- - - - +
), }, - ], []); + ], [filter]); + + const handleFilterChange = useMemo(() => debounce(async (v) => fetchUsers(v), 400), [fetchUsers]); + + // --- مودال اضافه و ویرایش --- + const renderModal = (isOpen, title, onSubmit, formState, setFormState, onClose) => { + if (!isOpen) return null; + return ( + <> +
+
+
+

{title}

+
+ {['firstName', 'lastName', 'email'].map((key) => ( + setFormState({ ...formState, [key]: e.target.value })} + className="w-full p-2 border rounded-lg" + placeholder={key.replace(/^\w/, (c) => c.toUpperCase())} + /> + ))} +
+ + +
+
+
+
+ + ); + }; + + // --- مودال مدیریت رول‌ها --- + const renderRolesModal = () => { + if (!rolesModalUser) return null; + return ( + <> +
setRolesModalUser(null)} /> +
+
+

+ Manage Roles for {rolesModalUser.firstName} +

+
+ {roles.map(role => ( + + ))} +
+
+ + +
+
+
+ + ); + }; return (
+ +

Users

- Manage users: add, edit, activate/deactivate and reset password + Manage users: add, edit, activate/deactivate, reset password, and assign roles

-
- {/* Add Modal */} - {isModalOpen && ( - <> -
setIsModalOpen(false)} - /> -
-
-

Add User

-
- setFirstName(e.target.value)} - className="w-full p-2 border rounded-lg" - placeholder="First Name" - /> - setLastName(e.target.value)} - className="w-full p-2 border rounded-lg" - placeholder="Last Name" - /> - setEmail(e.target.value)} - className="w-full p-2 border rounded-lg" - placeholder="Email" - /> -
- - -
-
-
-
- - )} + {renderModal(isModalOpen, 'Add User', onAddUser, addForm, setAddForm, () => setIsModalOpen(false))} + {renderModal(isEditModalOpen, 'Edit User', onUpdateUser, editForm, setEditForm, () => setIsEditModalOpen(false))} + {renderRolesModal()} - {/* Edit Modal */} - {isEditModalOpen && ( - <> -
setIsEditModalOpen(false)} - /> -
-
-

Edit User

-
- setFirstName(e.target.value)} - className="w-full p-2 border rounded-lg" - placeholder="First Name" - /> - setLastName(e.target.value)} - className="w-full p-2 border rounded-lg" - placeholder="Last Name" - /> - setEmail(e.target.value)} - className="w-full p-2 border rounded-lg" - placeholder="Email" - /> -
- - -
-
-
-
- - )} - - {/* Filter */}
{ - const v = e.target.value; - setFilter(v); - await fetchUsers(v); + onChange={(e) => { + setFilter(e.target.value); + handleFilterChange(e.target.value); }} - placeholder="Filter by name or email" + placeholder="Filter by name, email or role" className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500" />
- - - {error && ( -
{error}
+ {loading ? ( +
+
+
+ ) : ( + )} + + {error &&
{error}
}
); }; diff --git a/src/services/api.js b/src/services/api.js index fcdb5ae..a5fb1f3 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,145 +1,131 @@ import axios from 'axios'; - import { useAuthStore } from "../store/authStore"; -const BASE_URL = import.meta.env.DEV ? "/" : "https://khalijpay-core.qaserver.ir"; +// ----------------------------- +// تنظیم BASE_URL +// ----------------------------- +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || + (import.meta.env.DEV ? "/" : "https://khalijpay-core.qaserver.ir"); -// ساخت instance از axios +// ----------------------------- +// ایجاد instance از axios +// ----------------------------- const api = axios.create({ - baseURL: BASE_URL, - withCredentials: true, // ارسال و دریافت cookie/session - headers: { - "Content-Type": "application/json", - }, + baseURL: API_BASE_URL, + withCredentials: true, + headers: { "Content-Type": "application/json" }, }); // ----------------------------- -// Interceptor پاسخ‌ها +// Global error handler برای suppress کردن 401/404 +// ----------------------------- +if (typeof window !== 'undefined') { + const originalConsoleError = console.error; + const originalConsoleWarn = console.warn; + + console.error = function(...args) { + const errorStr = JSON.stringify(args); + const isSilentError = errorStr.includes('401') || errorStr.includes('404') || + args.some(arg => typeof arg === 'object' && arg?.response?.status && [401,404].includes(arg.response.status)); + + if (isSilentError) return; + originalConsoleError.apply(console, args); + }; + + console.warn = function(...args) { + const warnStr = JSON.stringify(args); + if (warnStr.includes('401') || warnStr.includes('404')) return; + originalConsoleWarn.apply(console, args); + }; +} + +// ----------------------------- +// Request interceptor +// ----------------------------- +api.interceptors.request.use(config => { + const skipAuthRedirect = config?.skipAuthRedirect === true; + if (skipAuthRedirect) { + const authState = useAuthStore.getState(); + if (!authState?.isLoggedIn) { + const CancelToken = axios.CancelToken; + const source = CancelToken.source(); + source.cancel('User not logged in'); + config.cancelToken = source.token; + config._skipRequest = true; + } + } + return config; +}, error => Promise.reject(error)); + +// ----------------------------- +// Response interceptor // ----------------------------- api.interceptors.response.use( - (response) => response, - (error) => { + response => response, + error => { + if (error?.isSilent) return Promise.reject(error); + if (axios.isCancel(error)) return Promise.reject({ isSilent: true, isCancel: true, response: { status: 401, data: { message: 'Unauthorized' } }, config: error.config || {} }); + const skipRedirect = error?.config?.skipAuthRedirect === true; - if (error.response?.status === 401 && !skipRedirect) { - // session منقضی شده → هدایت به login - const setLoggedIn = useAuthStore.getState().setLoggedIn; - setLoggedIn(false); - window.location.href = "/"; + const status = error?.response?.status; + + if (status === 401) { + if (!skipRedirect) { + useAuthStore.getState().setLoggedIn(false); + window.location.href = "/"; + return Promise.reject(error); + } else { + return Promise.reject({ isSilent: true, response: { status: 401, data: { message: 'Unauthorized' } }, config: error.config }); + } } + + if (status === 404 && skipRedirect) { + return Promise.reject({ isSilent: true, response: { status: 404, data: { message: 'Not Found' } }, config: error.config }); + } + return Promise.reject(error.response?.data || error); } ); // ----------------------------- -// توابع API +// Auth API // ----------------------------- - -// Login export async function login(username, password) { - try { - const res = await api.post("/api/v1/Auth/SignIn", { - // include common variants to satisfy different backends - userName: username, - username: username, - email: username, - password: password, - }); - return res.data; - } catch (error) { - throw error; - } + const res = await api.post("/api/v1/Auth/SignIn", { userName: username, username, email: username, password }); + return res.data; } -// خروج از سیستم export async function signOut() { try { - const res = await api.post("/api/v1/Auth/SignOut"); - // پاک کردن وضعیت login در Zustand - const setLoggedIn = useAuthStore.getState().setLoggedIn; - setLoggedIn(false); - window.location.href = "/"; - return res.data; + await api.post("/api/v1/Auth/SignOut", null, { skipAuthRedirect: true }); } catch (error) { - throw error; + console.warn("SignOut API error:", error); } + useAuthStore.getState().setLoggedIn(false); + window.location.href = "/"; } -// فراموشی رمز عبور export async function forgotPassword(email) { - try { - const res = await api.post("/api/v1/Auth/ForgotPassword", { email }); - return res.data; - } catch (error) { - throw error; - } + const res = await api.post("/api/v1/Auth/ForgotPassword", { email }); + return res.data; } -// گرفتن داده‌های محافظت‌شده export async function fetchProtectedData(endpoint) { - try { - const res = await api.get(endpoint); - return res.data; - } catch (error) { - throw error; - } + const res = await api.get(endpoint); + return res.data; } - - - - - - - - -// Mock data for development +// ----------------------------- +// Payments API (mock data) +// ----------------------------- const mockData = { - stats: { - total: 1247, - success: 1189, - failed: 58 - }, + stats: { total: 1247, success: 1189, failed: 58 }, payments: [ - { - id: 'TXN-001', - user: 'John Doe', - amount: 299.99, - status: 'success', - date: '2024-01-15T10:30:00Z', - currency: 'USD' - }, - { - id: 'TXN-002', - user: 'Jane Smith', - amount: 150.00, - status: 'pending', - date: '2024-01-15T11:45:00Z', - currency: 'USD' - }, - { - id: 'TXN-003', - user: 'Bob Johnson', - amount: 75.50, - status: 'failed', - date: '2024-01-15T12:15:00Z', - currency: 'USD' - }, - { - id: 'TXN-004', - user: 'Alice Brown', - amount: 450.00, - status: 'success', - date: '2024-01-15T13:20:00Z', - currency: 'USD' - }, - { - id: 'TXN-005', - user: 'Charlie Wilson', - amount: 89.99, - status: 'success', - date: '2024-01-15T14:30:00Z', - currency: 'USD' - } + { id: 'TXN-001', user: 'John Doe', amount: 299.99, status: 'success', date: '2024-01-15T10:30:00Z', currency: 'USD' }, + { id: 'TXN-002', user: 'Jane Smith', amount: 150.00, status: 'pending', date: '2024-01-15T11:45:00Z', currency: 'USD' }, + { id: 'TXN-003', user: 'Bob Johnson', amount: 75.50, status: 'failed', date: '2024-01-15T12:15:00Z', currency: 'USD' }, + { id: 'TXN-004', user: 'Alice Brown', amount: 450.00, status: 'success', date: '2024-01-15T13:20:00Z', currency: 'USD' }, + { id: 'TXN-005', user: 'Charlie Wilson', amount: 89.99, status: 'success', date: '2024-01-15T14:30:00Z', currency: 'USD' } ], chartData: [ { date: '2024-01-09', amount: 1200 }, @@ -152,382 +138,117 @@ const mockData = { ] }; -// Expose mock payment API for dashboard until real endpoints are integrated export const paymentsAPI = { - async getStats() { - return mockData.stats; - }, - async getChartData() { - return mockData.chartData; - }, + async getStats() { return mockData.stats; }, + async getChartData() { return mockData.chartData; }, }; // ----------------------------- -// Roles API (localStorage mock) +// Roles API // ----------------------------- const ROLES_STORAGE_KEY = 'app_roles_v1'; - -function readRolesFromStorage() { - try { - const raw = localStorage.getItem(ROLES_STORAGE_KEY); - if (!raw) return []; - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed)) return []; - return parsed; - } catch (_) { - return []; - } -} - -function writeRolesToStorage(roles) { - localStorage.setItem(ROLES_STORAGE_KEY, JSON.stringify(roles)); -} - -function ensureSeedRoles() { - const existing = readRolesFromStorage(); - if (existing.length === 0) { - const seed = [ - { id: crypto.randomUUID(), name: 'Admin', permissions: ['read', 'write', 'delete'], userType: 'internal' }, - { id: crypto.randomUUID(), name: 'Editor', permissions: ['read', 'write'], userType: 'internal' }, - { id: crypto.randomUUID(), name: 'Viewer', permissions: ['read'], userType: 'external' }, - ]; - writeRolesToStorage(seed); - } -} +function readRolesFromStorage() { try { const raw = localStorage.getItem(ROLES_STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } } +function writeRolesToStorage(roles) { localStorage.setItem(ROLES_STORAGE_KEY, JSON.stringify(roles)); } +function ensureSeedRoles() { if (readRolesFromStorage().length === 0) writeRolesToStorage([ + { id: crypto.randomUUID(), name: 'Admin', permissions: ['read','write','delete'], userType: 'internal' }, + { id: crypto.randomUUID(), name: 'Editor', permissions: ['read','write'], userType: 'internal' }, + { id: crypto.randomUUID(), name: 'Viewer', permissions: ['read'], userType: 'external' }, +]); } export const rolesAPI = { - async list(queryOrOptions = '') { - // Support both: list('admin') and list({ nameQuery, currentPage, pageSize }) - const opts = typeof queryOrOptions === 'string' - ? { nameQuery: queryOrOptions, currentPage: 1, pageSize: 100 } - : { - nameQuery: queryOrOptions?.nameQuery || '', - currentPage: queryOrOptions?.currentPage ?? 1, - pageSize: queryOrOptions?.pageSize ?? 100, - }; - + async list(queryOrOptions='') { + const opts = typeof queryOrOptions === 'string' ? { nameQuery: queryOrOptions, currentPage:1, pageSize:100 } : { nameQuery: queryOrOptions?.nameQuery||'', currentPage: queryOrOptions?.currentPage||1, pageSize: queryOrOptions?.pageSize||100 }; + if (!useAuthStore.getState()?.isLoggedIn) { ensureSeedRoles(); return readRolesFromStorage().filter(r=>r.name.toLowerCase().includes((opts.nameQuery||'').toLowerCase())); } try { - const res = await api.get('/api/v1/Role', { - params: { - nameQuery: opts.nameQuery, - currentPage: opts.currentPage, - pageSize: opts.pageSize, - }, - // prevent global 401 redirect for optional fetch - skipAuthRedirect: true, - }); - const raw = res?.data; - // Backend shape example: - // { data: { filterSummary: {...}, data: Role[] }, isSuccess: true, ... } - const apiItems = Array.isArray(raw?.data?.data) ? raw.data.data : undefined; - // Fallbacks for other shapes we may encounter - const items = apiItems - || (Array.isArray(raw) ? raw : undefined) - || (Array.isArray(raw?.items) ? raw.items : []) - || []; - // Sync a snapshot to local storage for offline UX - writeRolesToStorage(items.map(r => ({ id: r.id || crypto.randomUUID(), ...r }))); + const res = await api.get('/api/v1/Role', { params: opts, skipAuthRedirect: true }); + const items = Array.isArray(res?.data?.data?.data) ? res.data.data.data : []; + writeRolesToStorage(items.map(r => ({ id: r.id||crypto.randomUUID(), ...r }))); return items; - } catch (_) { - // Fallback to local filtering - ensureSeedRoles(); - const roles = readRolesFromStorage(); - const trimmed = String(opts.nameQuery || '').toLowerCase(); - if (!trimmed) return roles; - return roles.filter(r => r.name?.toLowerCase().includes(trimmed)); - } + } catch { ensureSeedRoles(); return readRolesFromStorage(); } }, async create(role) { - const payload = { - name: String(role?.name || '').trim(), - permissions: Array.isArray(role?.permissions) ? role.permissions : [], - // userType: String(role?.userType || '').trim(), - }; - let created = null; - try { - // Try real backend - const res = await api.post('/api/v1/Role', payload, { skipAuthRedirect: true }); - created = res?.data || payload; - } catch (_) { - // Fallback to local-only if backend unavailable - created = payload; + const payload = { name: String(role?.name||''), permissions: Array.isArray(role?.permissions)?role.permissions:[] }; + let created = payload; + if (useAuthStore.getState()?.isLoggedIn) { + try { const res = await api.post('/api/v1/Role', payload, { skipAuthRedirect:true }); created = res?.data||payload; } catch {} } - - // Sync into local storage list so UI updates immediately - const roles = readRolesFromStorage(); - const newRole = { id: crypto.randomUUID(), ...created }; - roles.push(newRole); - writeRolesToStorage(roles); - return newRole; + const roles = readRolesFromStorage(); const newRole = { id: crypto.randomUUID(), ...created }; roles.push(newRole); writeRolesToStorage(roles); return newRole; }, async remove(id) { - try { - await api.delete(`/api/v1/Role/${encodeURIComponent(id)}`, { skipAuthRedirect: true }); - } catch (_) { - // ignore backend failure, proceed with local removal for UX - } - const roles = readRolesFromStorage(); - const next = roles.filter(r => r.id !== id); - writeRolesToStorage(next); - return { success: true }; + if (useAuthStore.getState()?.isLoggedIn) { try { await api.delete(`/api/v1/Role/${encodeURIComponent(id)}`, { skipAuthRedirect:true }); } catch {} } + const roles = readRolesFromStorage(); writeRolesToStorage(roles.filter(r=>r.id!==id)); return { success:true }; }, async update(id, role) { - const existingLocal = readRolesFromStorage().find(r => r.id === id) || {}; - const payload = { - name: String(role?.name || '').trim(), - permissions: Array.isArray(role?.permissions) ? role.permissions : [], - ...(role?.userType !== undefined - ? { userType: String(role?.userType || '').trim() } - : (existingLocal.userType ? { userType: existingLocal.userType } : {})), - }; - - try { - await api.put(`/api/v1/Role/${encodeURIComponent(id)}`, payload, { skipAuthRedirect: true }); - } catch (err) { - const status = err?.statusCode || err?.status || err?.response?.status; - if (status === 404 || status === 405 || status === 400) { - try { - await api.put('/api/v1/Role', { id, ...payload }, { skipAuthRedirect: true }); - } catch (e2) { - const s2 = e2?.statusCode || e2?.status || e2?.response?.status; - if (s2 === 401 || s2 === 403) { - throw { message: 'Unauthorized to edit roles (401/403). Check permissions/login.', status: s2 }; - } - } - } else if (status === 401 || status === 403) { - throw { message: 'Unauthorized to edit roles (401/403). Check permissions/login.', status }; - } - } - - const roles = readRolesFromStorage(); - const idx = roles.findIndex(r => r.id === id); - if (idx !== -1) { - roles[idx] = { ...roles[idx], ...payload }; - writeRolesToStorage(roles); - return roles[idx]; - } - - // If not found locally, append - const updated = { id, ...payload }; - roles.push(updated); - writeRolesToStorage(roles); - return updated; + const payload = { name: String(role?.name||''), permissions: Array.isArray(role?.permissions)?role.permissions:[], userType: role?.userType||undefined }; + if (useAuthStore.getState()?.isLoggedIn) { try { await api.put(`/api/v1/Role/${encodeURIComponent(id)}`, payload, { skipAuthRedirect:true }); } catch {} } + const roles = readRolesFromStorage(); const idx = roles.findIndex(r=>r.id===id); if(idx!==-1){ roles[idx]={...roles[idx], ...payload}; writeRolesToStorage(roles); return roles[idx]; } + const updated={id,...payload}; roles.push(updated); writeRolesToStorage(roles); return updated; }, }; - - - - - // ----------------------------- -// ✅ Users API (React version) +// Users API با رول‌ها // ----------------------------- - const USERS_STORAGE_KEY = 'app_users_v1'; - -// 🧩 localStorage helpers -function readUsersFromStorage() { - try { - const raw = localStorage.getItem(USERS_STORAGE_KEY); - if (!raw) return []; - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } -} - -function writeUsersToStorage(users) { - localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(users)); -} +function readUsersFromStorage(){ try{ const raw = localStorage.getItem(USERS_STORAGE_KEY); return raw?JSON.parse(raw):[]; } catch{return [];} } +function writeUsersToStorage(users){ localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(users)); } export const usersAPI = { - // 📄 لیست کاربران - async list({ searchQuery = '', currentPage = 1, pageSize = 100 } = {}) { + async list({searchQuery='',currentPage=1,pageSize=100}={}) { + const res = await api.get('/api/v1/User',{ params:{searchQuery,currentPage,pageSize}, skipAuthRedirect:true }); + return res?.data?.data?.data||[]; + }, + async create(user){ + const payload = { firstName:String(user?.firstName||''), lastName:String(user?.lastName||''), email:String(user?.email||''), mobile:String(user?.mobile||''), isActive:!!user?.isActive }; + const res = await api.post('/api/v1/User',payload,{skipAuthRedirect:true}); + return res?.data; + }, + async update(id,user){ + const payload = { firstName:String(user?.firstName||''), lastName:String(user?.lastName||''), email:String(user?.email||''), mobile:String(user?.mobile||''), isActive:!!user?.isActive }; + const res = await api.put(`/api/v1/User/${encodeURIComponent(id)}`,payload,{skipAuthRedirect:true}); + return res?.data; + }, + async remove(id){ + const res = await api.delete(`/api/v1/User/Delete/${encodeURIComponent(id)}/Role`,{skipAuthRedirect:true}); + return res?.data; + }, + async toggleActivation(id){ const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ToggleActivation`,null,{skipAuthRedirect:true}); return res?.data; }, + async resetPassword(id){ const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ResetPassword`,null,{skipAuthRedirect:true}); return res?.data; }, + async getRoles(userId){ try{ const res = await api.get(`/api/v1/User/${encodeURIComponent(userId)}/Role`,{skipAuthRedirect:true}); return res?.data?.data?.data||[]; }catch(err){ console.error(err); return []; } }, + + // مدیریت رول‌ها + async getRoles(userId){ try { - const res = await api.get('/api/v1/User', { - params: { searchQuery, currentPage, pageSize }, - skipAuthRedirect: true, - }); - const apiItems = Array.isArray(res?.data?.data?.data) - ? res.data.data.data - : []; - writeUsersToStorage(apiItems); - return apiItems; - } catch (err) { - console.warn('List users fallback to localStorage'); - const users = readUsersFromStorage(); - const trimmed = searchQuery.toLowerCase(); - return trimmed - ? users.filter( - (u) => - u.firstName?.toLowerCase().includes(trimmed) || - u.lastName?.toLowerCase().includes(trimmed) || - u.email?.toLowerCase().includes(trimmed) - ) - : users; - } - }, - - // ➕ افزودن کاربر جدید - async create(user) { - const payload = { - firstName: String(user?.firstName || '').trim(), - lastName: String(user?.lastName || '').trim(), - email: String(user?.email || '').trim(), - mobile: String(user?.mobile || '').trim(), - isActive: !!user?.isActive, - }; - - try { - const res = await api.post('/api/v1/User', payload, { skipAuthRedirect: true }); - const created = res?.data?.data || payload; - - const users = readUsersFromStorage(); - const newUser = { id: created.id || crypto.randomUUID(), ...created }; - users.push(newUser); - writeUsersToStorage(users); - return newUser; - } catch (err) { - console.error('Create user failed', err); - const fallbackUser = { id: crypto.randomUUID(), ...payload }; - const users = readUsersFromStorage(); - users.push(fallbackUser); - writeUsersToStorage(users); - return fallbackUser; - } - }, - - // ✏️ ویرایش کاربر - async update(id, user) { - const payload = { - firstName: String(user?.firstName || '').trim(), - lastName: String(user?.lastName || '').trim(), - email: String(user?.email || '').trim(), - mobile: String(user?.mobile || '').trim(), - isActive: !!user?.isActive, - }; - - try { - await api.put(`/api/v1/User/${encodeURIComponent(id)}`, payload, { skipAuthRedirect: true }); - } catch (err) { - console.error('Update user failed', err); - } - - const users = readUsersFromStorage(); - const idx = users.findIndex((u) => u.id === id); - if (idx !== -1) users[idx] = { ...users[idx], ...payload }; - else users.push({ id, ...payload }); - writeUsersToStorage(users); - return users[idx] || payload; - }, - - // ❌ حذف کاربر - async remove(id) { - try { - await api.delete(`/api/v1/User/Delete/${encodeURIComponent(id)}/Role`, { - skipAuthRedirect: true, - }); - } catch (err) { - console.error('Delete user failed', err); - } - const users = readUsersFromStorage().filter((u) => u.id !== id); - writeUsersToStorage(users); - return { success: true }; - }, - - // 🔄 تغییر وضعیت فعال بودن (PATCH) - async toggleActivation(id) { - try { - const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ToggleActivation`, null, { - skipAuthRedirect: true, - }); - const updated = res?.data?.data; - - if (updated) { - const users = readUsersFromStorage(); - const idx = users.findIndex((u) => u.id === id); - if (idx !== -1) { - users[idx] = { ...users[idx], isActive: updated.isActive }; - writeUsersToStorage(users); - } - } - return updated; - } catch (err) { - console.error('Toggle activation failed', err); - throw err; - } - }, - - // 🔐 ریست پسورد (PATCH) - async resetPassword(id) { - try { - const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ResetPassword`, null, { - skipAuthRedirect: true, - }); - return res?.data; - } catch (err) { - console.error('Reset password failed', err); - throw err; + const res = await api.get(`/api/v1/User/${encodeURIComponent(userId)}/Role`, { skipAuthRedirect: true }); + return res?.data?.data || []; // ← اصلاح شد + } catch(err) { + console.error(err); + return []; } }, + async removeRole(userId,roleId){ try{ const res = await api.delete(`/api/v1/User/${encodeURIComponent(userId)}/Role/${encodeURIComponent(roleId)}`,{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } }, + async updateRoles(userId,roleIds=[]){ try{ const res = await api.post(`/api/v1/User/${encodeURIComponent(userId)}/Role`,{roleIds},{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } }, }; - - - - - - - - - - - - // ----------------------------- // Permissions API // ----------------------------- -export async function listPermissions() { - try { - const res = await api.get('/api/v1/General/Permission', { skipAuthRedirect: true }); - const raw = res?.data; - // Expected shape: { data: Permission[] } - const items = Array.isArray(raw?.data) ? raw.data : []; - // Prefer returning structured objects with name and description - const objs = items - .map((p) => { - const name = typeof p?.name === 'string' ? p.name : (typeof p?.displayName === 'string' ? p.displayName : null); - if (!name) return null; - const description = typeof p?.description === 'string' && p.description - ? p.description - : (typeof p?.displayName === 'string' ? p.displayName : name); - return { name, description }; - }) - .filter(Boolean); - if (objs.length > 0) return objs; - // Fallback to simple names if server returned unexpected shape - const names = items - .map((p) => (typeof p?.name === 'string' ? p.name : (typeof p?.displayName === 'string' ? p.displayName : null))) - .filter(Boolean); - return names.map((n) => ({ name: n, description: n })); - } catch (_) { - // Fallback to a minimal static list to keep UI usable - return [ - { name: 'Administrator', description: 'Full access to administrative features' }, - { name: 'UserManagement', description: 'Manage users and their profiles' }, - { name: 'AddUser', description: 'Create new user accounts' }, - { name: 'EditUser', description: 'Edit existing user accounts' }, - { name: 'UserPasswordManagement', description: 'Reset or change user passwords' }, - { name: 'UserRoleManagement', description: 'Assign or modify user roles' }, - { name: 'RoleManagement', description: 'Manage roles and permissions' }, - { name: 'AddRole', description: 'Create new roles' }, - { name: 'EditRole', description: 'Edit existing roles' }, - { name: 'DeleteRole', description: 'Remove roles from the system' }, - ]; - } +export async function listPermissions(){ + if(!useAuthStore.getState()?.isLoggedIn) return [ + {name:'Administrator',description:'Full access'}, {name:'UserManagement',description:'Manage users'}, {name:'AddUser',description:'Create user'}, {name:'EditUser',description:'Edit user'}, + {name:'UserPasswordManagement',description:'Reset/change password'}, {name:'UserRoleManagement',description:'Assign/modify roles'}, + {name:'RoleManagement',description:'Manage roles'}, {name:'AddRole',description:'Add role'}, {name:'EditRole',description:'Edit role'}, {name:'DeleteRole',description:'Delete role'} + ]; + try{ + const res = await api.get('/api/v1/General/Permissions',{skipAuthRedirect:true}); + const items = Array.isArray(res?.data?.data)?res.data.data:[]; + return items.map(p=>({name:p.name||p.displayName,description:p.description||p.displayName||p.name})).filter(Boolean); + }catch{return [ + {name:'Administrator',description:'Full access'}, {name:'UserManagement',description:'Manage users'}, {name:'AddUser',description:'Create user'}, {name:'EditUser',description:'Edit user'}, + {name:'UserPasswordManagement',description:'Reset/change password'}, {name:'UserRoleManagement',description:'Assign/modify roles'}, + {name:'RoleManagement',description:'Manage roles'}, {name:'AddRole',description:'Add role'}, {name:'EditRole',description:'Edit role'}, {name:'DeleteRole',description:'Delete role'} + ];} } diff --git a/vite.config.js b/vite.config.js index 69b68f0..23d7469 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,23 +1,36 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], server: { proxy: { - '/api': { - target: 'https://khalijpay-core.qaserver.ir', + "/api": { + target: "https://khalijpay-core.qaserver.ir", changeOrigin: true, secure: false, + // اجازه ارسال کوکی‌ها configure: (proxy) => { - proxy.on('proxyReq', (proxyReq, req) => { - // Ensure cookies are forwarded for withCredentials + proxy.on("proxyReq", (proxyReq, req) => { const origin = req.headers.origin; - if (origin) proxyReq.setHeader('origin', origin); + if (origin) { + proxyReq.setHeader("origin", origin); + } + // اضافه کردن این خط برای اطمینان از ارسال کوکی‌ها + proxyReq.setHeader("Access-Control-Allow-Credentials", "true"); + }); + + proxy.on("proxyRes", (proxyRes) => { + // اطمینان از دریافت کوکی از سرور + proxyRes.headers["Access-Control-Allow-Origin"] = "http://localhost:5173"; + proxyRes.headers["Access-Control-Allow-Credentials"] = "true"; + }); + + proxy.on("error", (err) => { + console.log("Proxy Error:", err); }); }, }, }, }, -}) +});