Add admin role with user management (create/delete users)

First registered user becomes admin automatically. Admins see a
"Manage Users" button in the dashboard header that opens a new
/admin page for listing, creating, and deleting users. Backend
enforces admin-only access on /admin/* routes. Startup migration
adds the is_admin column to existing SQLite databases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 19:26:10 +01:00
parent c1371e9c72
commit 0803d86e38
9 changed files with 232 additions and 5 deletions
+10 -2
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, Link } from 'react-router-dom';
import AddPurchase from '../components/AddPurchase';
import PurchaseList from '../components/PurchaseList';
import PortfolioChart from '../components/PortfolioChart';
@@ -12,6 +12,8 @@ const styles = {
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' },
logo: { fontSize: '1.4rem', fontWeight: 700, color: '#f7931a' },
logoutBtn: { background: 'none', border: '1px solid #555', color: '#aaa', borderRadius: '8px', padding: '0.4rem 1rem', cursor: 'pointer' },
adminBtn: { background: 'none', border: '1px solid #f7931a', color: '#f7931a', borderRadius: '8px', padding: '0.4rem 1rem', cursor: 'pointer', textDecoration: 'none', fontSize: '1rem' },
headerBtns: { display: 'flex', gap: '0.5rem', alignItems: 'center' },
statsGrid: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', marginBottom: '1.5rem' },
statCard: { background: '#1a1a1a', padding: '1rem', borderRadius: '12px', border: '1px solid #333' },
statLabel: { color: '#888', fontSize: '0.8rem', marginBottom: '0.3rem' },
@@ -68,8 +70,11 @@ export default function Dashboard() {
fetchData();
}, [fetchData]);
const isAdmin = localStorage.getItem('is_admin') === 'true';
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('is_admin');
navigate('/login');
};
@@ -81,7 +86,10 @@ export default function Dashboard() {
<div style={styles.app}>
<div style={styles.header}>
<div style={styles.logo}> BTC Portfolio</div>
<button style={styles.logoutBtn} onClick={handleLogout}>Logout</button>
<div style={styles.headerBtns}>
{isAdmin && <Link to="/admin" style={styles.adminBtn}>Manage Users</Link>}
<button style={styles.logoutBtn} onClick={handleLogout}>Logout</button>
</div>
</div>
{stats && (