feat: multi-user auth with role-based access
Deploy HiHala Dashboard / deploy (push) Successful in 6s
Deploy HiHala Dashboard / deploy (push) Successful in 6s
- Server checks PIN against env (super admin) + NocoDB Users table - Session stores name + role (admin/viewer) - Admin: sees Settings page (seasons + users management) - Viewer: sees Dashboard + Comparison only, no Settings - Users CRUD on Settings page: add name + PIN + role, delete - Settings link + nav hidden for non-admin users Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+17
-9
@@ -38,6 +38,8 @@ interface DataSource {
|
||||
function App() {
|
||||
const { t, dir, switchLanguage } = useLanguage();
|
||||
const [authenticated, setAuthenticated] = useState<boolean | null>(null);
|
||||
const [userRole, setUserRole] = useState<string>('viewer');
|
||||
const [userName, setUserName] = useState<string>('');
|
||||
const [data, setData] = useState<MuseumRecord[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||
@@ -114,6 +116,8 @@ function App() {
|
||||
.then(d => {
|
||||
setAuthenticated(d.authenticated);
|
||||
if (d.authenticated) {
|
||||
setUserRole(d.role || 'viewer');
|
||||
setUserName(d.name || '');
|
||||
loadData();
|
||||
loadSeasons();
|
||||
}
|
||||
@@ -122,8 +126,10 @@ function App() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleLogin = () => {
|
||||
const handleLogin = (name: string, role: string) => {
|
||||
setAuthenticated(true);
|
||||
setUserName(name);
|
||||
setUserRole(role);
|
||||
loadData();
|
||||
loadSeasons();
|
||||
};
|
||||
@@ -281,9 +287,9 @@ function App() {
|
||||
<main>
|
||||
<Suspense fallback={<LoadingSkeleton />}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard data={data} seasons={seasons} showDataLabels={showDataLabels} setShowDataLabels={setShowDataLabels} includeVAT={includeVAT} setIncludeVAT={setIncludeVAT} />} />
|
||||
<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} />} />
|
||||
<Route path="/settings" element={<Settings onSeasonsChange={loadSeasons} />} />
|
||||
{userRole === 'admin' && <Route path="/settings" element={<Settings onSeasonsChange={loadSeasons} />} />}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</main>
|
||||
@@ -307,12 +313,14 @@ function App() {
|
||||
</svg>
|
||||
<span>{t('nav.compare')}</span>
|
||||
</NavLink>
|
||||
<NavLink to="/settings" className="mobile-nav-item">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
<span>{t('nav.settings')}</span>
|
||||
</NavLink>
|
||||
{userRole === 'admin' && (
|
||||
<NavLink to="/settings" className="mobile-nav-item">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
<span>{t('nav.settings')}</span>
|
||||
</NavLink>
|
||||
)}
|
||||
<button
|
||||
className="mobile-nav-item"
|
||||
onClick={switchLanguage}
|
||||
|
||||
Reference in New Issue
Block a user