feat: per-user museum and channel access control
- PATCH /api/users/:id route to update user permissions - Auth session stores and returns allowedMuseums/allowedChannels - User type gains AllowedMuseums/AllowedChannels (JSON string fields) - parseAllowed() with fail-closed semantics (empty string → null → no data) - Dashboard/Comparison apply permission base filter before user filters - Filter dropdowns (museums, channels, years, districts) derived from permission-filtered data — restricted users only see their allowed options - Settings UserRow component with inline checkbox pickers for access config - Access badges in users table showing current restriction summary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+15
-6
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback, ReactNode, lazy, Suspense } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo, ReactNode, lazy, Suspense } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
|
||||
|
||||
const Dashboard = lazy(() => import('./components/Dashboard'));
|
||||
@@ -6,8 +6,9 @@ const Comparison = lazy(() => import('./components/Comparison'));
|
||||
const Settings = lazy(() => import('./components/Settings'));
|
||||
import Login from './components/Login';
|
||||
import LoadingSkeleton from './components/shared/LoadingSkeleton';
|
||||
import { fetchData, getCacheStatus, refreshData } from './services/dataService';
|
||||
import { fetchData, getCacheStatus, refreshData, getUniqueMuseums, getUniqueChannels } from './services/dataService';
|
||||
import { fetchSeasons } from './services/seasonsService';
|
||||
import { parseAllowed } from './services/usersService';
|
||||
import { useLanguage } from './contexts/LanguageContext';
|
||||
import type { MuseumRecord, Season, CacheStatus, DataErrorType } from './types';
|
||||
import { DataError } from './types';
|
||||
@@ -40,7 +41,11 @@ function App() {
|
||||
const [authenticated, setAuthenticated] = useState<boolean | null>(null);
|
||||
const [userRole, setUserRole] = useState<string>('viewer');
|
||||
const [userName, setUserName] = useState<string>('');
|
||||
const [allowedMuseums, setAllowedMuseums] = useState<string[] | null>([]);
|
||||
const [allowedChannels, setAllowedChannels] = useState<string[] | null>([]);
|
||||
const [data, setData] = useState<MuseumRecord[]>([]);
|
||||
const allMuseumsList = useMemo(() => getUniqueMuseums(data), [data]);
|
||||
const allChannelsList = useMemo(() => getUniqueChannels(data), [data]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||
const [error, setError] = useState<{ message: string; type: DataErrorType } | null>(null);
|
||||
@@ -118,6 +123,8 @@ function App() {
|
||||
if (d.authenticated) {
|
||||
setUserRole(d.role || 'viewer');
|
||||
setUserName(d.name || '');
|
||||
setAllowedMuseums(parseAllowed(d.allowedMuseums));
|
||||
setAllowedChannels(parseAllowed(d.allowedChannels));
|
||||
loadData();
|
||||
loadSeasons();
|
||||
}
|
||||
@@ -126,10 +133,12 @@ function App() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleLogin = (name: string, role: string) => {
|
||||
const handleLogin = (name: string, role: string, rawMuseums: string, rawChannels: string) => {
|
||||
setAuthenticated(true);
|
||||
setUserName(name);
|
||||
setUserRole(role);
|
||||
setAllowedMuseums(parseAllowed(rawMuseums));
|
||||
setAllowedChannels(parseAllowed(rawChannels));
|
||||
loadData();
|
||||
loadSeasons();
|
||||
};
|
||||
@@ -287,9 +296,9 @@ function App() {
|
||||
<main>
|
||||
<Suspense fallback={<LoadingSkeleton />}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard data={data} seasons={seasons} userRole={userRole} showDataLabels={showDataLabels} setShowDataLabels={setShowDataLabels} includeVAT={includeVAT} setIncludeVAT={setIncludeVAT} />} />
|
||||
<Route path="/comparison" element={<Comparison data={data} seasons={seasons} showDataLabels={showDataLabels} setShowDataLabels={setShowDataLabels} includeVAT={includeVAT} setIncludeVAT={setIncludeVAT} />} />
|
||||
{userRole === 'admin' && <Route path="/settings" element={<Settings onSeasonsChange={loadSeasons} />} />}
|
||||
<Route path="/" element={<Dashboard data={data} seasons={seasons} userRole={userRole} showDataLabels={showDataLabels} setShowDataLabels={setShowDataLabels} includeVAT={includeVAT} setIncludeVAT={setIncludeVAT} allowedMuseums={allowedMuseums} allowedChannels={allowedChannels} />} />
|
||||
<Route path="/comparison" element={<Comparison data={data} seasons={seasons} showDataLabels={showDataLabels} setShowDataLabels={setShowDataLabels} includeVAT={includeVAT} setIncludeVAT={setIncludeVAT} allowedMuseums={allowedMuseums} allowedChannels={allowedChannels} />} />
|
||||
{userRole === 'admin' && <Route path="/settings" element={<Settings onSeasonsChange={loadSeasons} allMuseums={allMuseumsList} allChannels={allChannelsList} />} />}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user