From a1b120076b8d099d7ba211358df6e6fa24fddbea Mon Sep 17 00:00:00 2001 From: Mia Wu Date: Tue, 2 Jun 2026 11:31:11 -0400 Subject: [PATCH] login system --- .gitignore | 1 + dashboard/package-lock.json | 58 +++++++++ dashboard/package.json | 1 + dashboard/src/App.jsx | 123 ++++++++++++++++-- dashboard/src/hooks/useAuth.js | 74 +++++++++++ dashboard/src/hooks/useConfig.js | 4 +- dashboard/src/hooks/useStats.js | 3 +- dashboard/src/hooks/useTests.js | 3 +- dashboard/src/lib/api.js | 30 ++++- dashboard/src/main.jsx | 5 +- server/.dockerignore | 3 +- server/.env | 5 - server/.env.template | 11 ++ server/app.py | 213 ++++++++++++++++++++++++++++++- server/dashboard.db | Bin 544768 -> 0 bytes server/db_py.py | 70 ++++++++++ server/requirements.txt | 2 + 17 files changed, 579 insertions(+), 27 deletions(-) create mode 100644 dashboard/src/hooks/useAuth.js delete mode 100644 server/.env create mode 100644 server/.env.template delete mode 100644 server/dashboard.db diff --git a/.gitignore b/.gitignore index fc8a770..e1cb9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ server/__pycache__/ server/dashboard.db* server/*.pyc *.pyc +server/.env # Test output test/ \ No newline at end of file diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 4b33272..939c6b0 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -13,6 +13,7 @@ "lucide-react": "^1.16.0", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-router-dom": "^7.16.0", "tailwindcss": "^4.3.0" }, "devDependencies": { @@ -1340,6 +1341,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2437,6 +2451,44 @@ "react": "^19.2.6" } }, + "node_modules/react-router": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.16.0.tgz", + "integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.16.0.tgz", + "integrity": "sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==", + "license": "MIT", + "dependencies": { + "react-router": "7.16.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/rolldown": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", @@ -2486,6 +2538,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index bd24a44..2ebb0d0 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -15,6 +15,7 @@ "lucide-react": "^1.16.0", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-router-dom": "^7.16.0", "tailwindcss": "^4.3.0" }, "devDependencies": { diff --git a/dashboard/src/App.jsx b/dashboard/src/App.jsx index fa49f9f..beb0e9f 100644 --- a/dashboard/src/App.jsx +++ b/dashboard/src/App.jsx @@ -1,8 +1,10 @@ -import { useState, useMemo } from 'react' +import { useMemo, useState } from 'react' import { Settings } from 'lucide-react' +import { useQueryClient } from '@tanstack/react-query' import { useStats } from './hooks/useStats' import { useTests } from './hooks/useTests' import { useConfig } from './hooks/useConfig' +import { useAuth } from './hooks/useAuth' import cgw453Image from './assets/CGW453.PNG' import StatCard from './components/StatCard' import CompletionBar from './components/CompletionBar' @@ -24,12 +26,19 @@ function hasCoePairs(value) { } export default function App() { + const queryClient = useQueryClient() + const { user, isReady, isAuthenticated, isAdmin, login, logout } = useAuth() + const [showConfig, setShowConfig] = useState(false) const [filters, setFilters] = useState({}) + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [loginError, setLoginError] = useState('') + const [isLoggingIn, setIsLoggingIn] = useState(false) - const { data: stats, isLoading: statsLoading } = useStats() - const { data: allTests = [], isLoading: testsLoading } = useTests() - const { data: config, isLoading: configLoading } = useConfig() + const { data: stats, isLoading: statsLoading } = useStats(isAuthenticated) + const { data: allTests = [], isLoading: testsLoading } = useTests({}, isAuthenticated) + const { data: config, isLoading: configLoading } = useConfig(isAuthenticated && isAdmin) // Apply filters client-side const filteredTests = useMemo(() => { @@ -52,22 +61,114 @@ export default function App() { }) }, [allTests, filters]) - const noConfig = !configLoading && !config?.target_dir && !config?.results_dir - const configuredButEmpty = !configLoading && config?.target_dir && config?.results_dir && + const noConfig = isAdmin && !configLoading && !config?.target_dir && !config?.results_dir + const configuredButEmpty = isAdmin && !configLoading && config?.target_dir && config?.results_dir && !statsLoading && stats?.overall.total === 0 + async function handleLogin(e) { + e.preventDefault() + setLoginError('') + setIsLoggingIn(true) + try { + await login(username.trim(), password) + setPassword('') + } catch (err) { + setLoginError(err?.message ?? 'Login failed') + } finally { + setIsLoggingIn(false) + } + } + + function handleLogout() { + logout() + queryClient.clear() + setShowConfig(false) + setFilters({}) + } + + if (!isReady) { + return ( +
+

Checking session...

+
+ ) + } + + if (!isAuthenticated) { + return ( +
+
+
+

Sign in

+
+ + {loginError && ( +
+ {loginError} +
+ )} + + + + + + +
+
+ ) + } + return (
{/* Top bar */}

CGW453 Test Dashboard

+ + {user?.username} ({user?.role}) + + {isAdmin && ( + + )}
@@ -155,7 +256,7 @@ export default function App() {
- {showConfig && setShowConfig(false)} />} + {isAdmin && showConfig && setShowConfig(false)} />} ) } diff --git a/dashboard/src/hooks/useAuth.js b/dashboard/src/hooks/useAuth.js new file mode 100644 index 0000000..44d02c7 --- /dev/null +++ b/dashboard/src/hooks/useAuth.js @@ -0,0 +1,74 @@ +import { createContext, createElement, useContext, useEffect, useMemo, useState } from 'react' +import { getMe, getStoredToken, login as loginApi, setStoredToken } from '../lib/api' + +const AuthContext = createContext(null) + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null) + const [isReady, setIsReady] = useState(false) + + useEffect(() => { + async function initializeAuth() { + const token = getStoredToken() + if (!token) { + setIsReady(true) + return + } + + try { + const data = await getMe() + setUser(data?.user ?? null) + } catch { + setStoredToken(null) + setUser(null) + } finally { + setIsReady(true) + } + } + + initializeAuth() + }, []) + + useEffect(() => { + function handleUnauthorized() { + setUser(null) + } + + window.addEventListener('auth:unauthorized', handleUnauthorized) + return () => window.removeEventListener('auth:unauthorized', handleUnauthorized) + }, []) + + async function login(username, password) { + const data = await loginApi({ username, password }) + setStoredToken(data?.token) + setUser(data?.user ?? null) + return data?.user ?? null + } + + function logout() { + setStoredToken(null) + setUser(null) + } + + const value = useMemo( + () => ({ + user, + isReady, + isAuthenticated: Boolean(user), + isAdmin: user?.role === 'admin', + login, + logout, + }), + [user, isReady] + ) + + return createElement(AuthContext.Provider, { value }, children) +} + +export function useAuth() { + const context = useContext(AuthContext) + if (!context) { + throw new Error('useAuth must be used inside AuthProvider') + } + return context +} diff --git a/dashboard/src/hooks/useConfig.js b/dashboard/src/hooks/useConfig.js index edf6549..6a945a1 100644 --- a/dashboard/src/hooks/useConfig.js +++ b/dashboard/src/hooks/useConfig.js @@ -1,8 +1,8 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { getConfig, saveConfig } from '../lib/api' -export function useConfig() { - return useQuery({ queryKey: ['config'], queryFn: getConfig }) +export function useConfig(enabled = true) { + return useQuery({ queryKey: ['config'], queryFn: getConfig, enabled }) } export function useSaveConfig() { diff --git a/dashboard/src/hooks/useStats.js b/dashboard/src/hooks/useStats.js index 5837378..cfb2b7a 100644 --- a/dashboard/src/hooks/useStats.js +++ b/dashboard/src/hooks/useStats.js @@ -1,10 +1,11 @@ import { useQuery } from '@tanstack/react-query' import { getStats } from '../lib/api' -export function useStats() { +export function useStats(enabled = true) { const statsQuery = useQuery({ queryKey: ['stats'], queryFn: getStats, + enabled, }) return { diff --git a/dashboard/src/hooks/useTests.js b/dashboard/src/hooks/useTests.js index 9adfb4c..528b248 100644 --- a/dashboard/src/hooks/useTests.js +++ b/dashboard/src/hooks/useTests.js @@ -1,10 +1,11 @@ import { useQuery } from '@tanstack/react-query' import { getTests } from '../lib/api' -export function useTests(filters = {}) { +export function useTests(filters = {}, enabled = true) { return useQuery({ queryKey: ['tests', filters], queryFn: () => getTests(filters), keepPreviousData: true, + enabled, }) } diff --git a/dashboard/src/lib/api.js b/dashboard/src/lib/api.js index e69b048..6f53148 100644 --- a/dashboard/src/lib/api.js +++ b/dashboard/src/lib/api.js @@ -1,10 +1,33 @@ const BASE = '/api' +const TOKEN_KEY = 'dashboard_jwt' + +export function getStoredToken() { + return localStorage.getItem(TOKEN_KEY) +} + +export function setStoredToken(token) { + if (!token) { + localStorage.removeItem(TOKEN_KEY) + return + } + localStorage.setItem(TOKEN_KEY, token) +} + export async function apiFetch(path, options = {}) { + const token = getStoredToken() + const authHeader = token ? { Authorization: `Bearer ${token}` } : {} + const { headers: customHeaders = {}, ...restOptions } = options const res = await fetch(`${BASE}${path}`, { - headers: { 'Content-Type': 'application/json', ...options.headers }, - ...options, + ...restOptions, + headers: { 'Content-Type': 'application/json', ...authHeader, ...customHeaders }, }) + + if (res.status === 401) { + setStoredToken(null) + window.dispatchEvent(new Event('auth:unauthorized')) + } + if (!res.ok) { const text = await res.text().catch(() => res.statusText) throw new Error(text || res.statusText) @@ -12,6 +35,9 @@ export async function apiFetch(path, options = {}) { return res.json() } +export const login = (body) => apiFetch('/auth/login', { method: 'POST', body: JSON.stringify(body) }) +export const getMe = () => apiFetch('/auth/me') + export const getStats = () => apiFetch('/stats') export const getTests = (params = {}) => { const qs = new URLSearchParams( diff --git a/dashboard/src/main.jsx b/dashboard/src/main.jsx index 1e93110..1e2e46c 100644 --- a/dashboard/src/main.jsx +++ b/dashboard/src/main.jsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import './index.css' import App from './App.jsx' +import { AuthProvider } from './hooks/useAuth' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 10_000, retry: 1 } }, @@ -11,7 +12,9 @@ const queryClient = new QueryClient({ createRoot(document.getElementById('root')).render( - + + + , ) diff --git a/server/.dockerignore b/server/.dockerignore index 418543d..60e602f 100644 --- a/server/.dockerignore +++ b/server/.dockerignore @@ -1,4 +1,5 @@ .venv __pycache__ *.pyc -dashboard.db* \ No newline at end of file +dashboard.db* +.env \ No newline at end of file diff --git a/server/.env b/server/.env deleted file mode 100644 index dd00d95..0000000 --- a/server/.env +++ /dev/null @@ -1,5 +0,0 @@ -# Flask backend port (default: 3001) -PORT=3001 - -# Note: Target and results directories are configured via the dashboard Settings UI -# and stored in SQLite (dashboard.db), not in environment variables. diff --git a/server/.env.template b/server/.env.template new file mode 100644 index 0000000..8dff798 --- /dev/null +++ b/server/.env.template @@ -0,0 +1,11 @@ +# Flask backend port (default: 3001) +PORT=3001 + +JWT_SECRET= +JWT_EXPIRES_HOURS=8 +DEFAULT_ADMIN_USERNAME=wnc +DEFAULT_ADMIN_PASSWORD=@wnc111111 +JWT_ALGORITHM=HS256 + +DEFAULT_VIEWER_USERNAME=viewer +DEFAULT_VIEWER_PASSWORD=viewer3040 \ No newline at end of file diff --git a/server/app.py b/server/app.py index e257163..8a4bff6 100644 --- a/server/app.py +++ b/server/app.py @@ -1,13 +1,41 @@ import os +from datetime import datetime, timedelta, timezone +from functools import wraps from pathlib import Path -from flask import Flask, jsonify, request, send_from_directory +import jwt +from dotenv import load_dotenv +from flask import Flask, g, jsonify, request, send_from_directory from flask_cors import CORS +from werkzeug.security import check_password_hash, generate_password_hash -from db_py import count_tests, del_config, get_all_tests, get_config, set_config +from db_py import ( + count_tests, + count_users, + create_user, + del_config, + get_all_tests, + get_all_users, + get_config, + get_user_by_id, + get_user_by_username, + set_config, + clear_users, +) from scanner import full_scan, is_scan_in_progress, resolve_runtime_path, scan_results_only +BASE_DIR = Path(__file__).resolve().parent +load_dotenv(BASE_DIR / ".env") + PORT = int(os.getenv("PORT", "3001")) +JWT_SECRET = os.getenv("JWT_SECRET") +JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256") +JWT_EXPIRES_HOURS = int(os.getenv("JWT_EXPIRES_HOURS", "8")) +DEFAULT_ADMIN_USERNAME = os.getenv("DEFAULT_ADMIN_USERNAME") +DEFAULT_ADMIN_PASSWORD = os.getenv("DEFAULT_ADMIN_PASSWORD") +DEFAULT_VIEWER_USERNAME = os.getenv("DEFAULT_VIEWER_USERNAME") +DEFAULT_VIEWER_PASSWORD = os.getenv("DEFAULT_VIEWER_PASSWORD") + ALLOWED_KEYS = { "target_dir", "results_dir", @@ -21,13 +49,110 @@ ALLOWED_KEYS = { "smb_domain", } -BASE_DIR = Path(__file__).resolve().parent DIST_DIR = BASE_DIR.parent / "dashboard" / "dist" app = Flask(__name__, static_folder=str(DIST_DIR), static_url_path="") CORS(app) +def _make_token(user): + now = datetime.now(timezone.utc) + payload = { + "sub": str(user["id"]), + "username": user["username"], + "role": user["role"], + "iat": int(now.timestamp()), + "exp": int((now + timedelta(hours=JWT_EXPIRES_HOURS)).timestamp()), + } + return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) + + +def _decode_token(token): + try: + return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + except jwt.InvalidTokenError: + return None + + +def _extract_bearer_token(): + auth_header = request.headers.get("Authorization", "") + if not auth_header.lower().startswith("bearer "): + return None + return auth_header[7:].strip() or None + + +def require_auth(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + token = _extract_bearer_token() + if not token: + return jsonify({"error": "Authentication required"}), 401 + + payload = _decode_token(token) + if payload is None: + return jsonify({"error": "Invalid or expired token"}), 401 + + try: + user_id = int(payload.get("sub")) + except (TypeError, ValueError): + return jsonify({"error": "Invalid token subject"}), 401 + + user = get_user_by_id(user_id) + if not user or not user.get("is_active"): + return jsonify({"error": "User is not authorized"}), 401 + + g.current_user = { + "id": user["id"], + "username": user["username"], + "role": user["role"], + } + return fn(*args, **kwargs) + + return wrapper + + +def require_role(required_role): + def decorator(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + current_user = getattr(g, "current_user", None) + if not current_user: + return jsonify({"error": "Authentication required"}), 401 + if current_user.get("role") != required_role: + return jsonify({"error": "Forbidden"}), 403 + return fn(*args, **kwargs) + + return wrapper + + return decorator + + +def _create_default_user_if_missing(username, password, role): + if not username or not password: + print(f"[server] Skipping default {role} seed: username/password not configured") + return + + existing = get_user_by_username(username) + if existing: + return + + create_user( + username, + generate_password_hash(password), + role=role, + is_active=1, + ) + print(f"[server] Created default {role} user: {username}") + + +def _ensure_default_users(): + if count_users() == 0: + print("[server] No users found. Seeding default accounts...") + + _create_default_user_if_missing(DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_PASSWORD, "admin") + _create_default_user_if_missing(DEFAULT_VIEWER_USERNAME, DEFAULT_VIEWER_PASSWORD, "viewer") + + def _apply_smb_env_from_config(): mapping = { "SMB_USERNAME": get_config("smb_username"), @@ -43,6 +168,7 @@ def _apply_smb_env_from_config(): @app.get("/api/tests") +@require_auth def get_tests_route(): completed = request.args.get("completed") interference = request.args.get("interference") @@ -90,6 +216,7 @@ def get_tests_route(): @app.get("/api/stats") +@require_auth def get_stats_route(): tests = get_all_tests() types = ["COE", "P2P", "P3P"] @@ -197,11 +324,84 @@ def get_stats_route(): @app.get("/api/scan-status") +@require_auth def get_scan_status_route(): return jsonify({"scanning": is_scan_in_progress()}) +@app.post("/api/auth/login") +def auth_login_route(): + body = request.get_json(silent=True) + if not isinstance(body, dict): + return jsonify({"error": "Request body must be a JSON object"}), 400 + + username = (body.get("username") or "").strip() + password = body.get("password") or "" + + if not username or not password: + return jsonify({"error": "Username and password are required"}), 400 + + user = get_user_by_username(username) + if not user or not user.get("is_active"): + return jsonify({"error": "Invalid username or password"}), 401 + + if not check_password_hash(user["password_hash"], password): + return jsonify({"error": "Invalid username or password"}), 401 + + token = _make_token(user) + return jsonify( + { + "token": token, + "user": { + "id": user["id"], + "username": user["username"], + "role": user["role"], + }, + } + ) + + +@app.get("/api/auth/me") +@require_auth +def auth_me_route(): + return jsonify({"user": g.current_user}) + + +@app.get("/api/users") +@require_auth +@require_role("admin") +def list_users_route(): + return jsonify(get_all_users()) + + +@app.post("/api/users") +@require_auth +@require_role("admin") +def create_user_route(): + body = request.get_json(silent=True) + if not isinstance(body, dict): + return jsonify({"error": "Request body must be a JSON object"}), 400 + + username = (body.get("username") or "").strip() + password = body.get("password") or "" + role = (body.get("role") or "viewer").strip().lower() + + if not username or not password: + return jsonify({"error": "Username and password are required"}), 400 + + if role not in {"admin", "viewer"}: + return jsonify({"error": "Role must be admin or viewer"}), 400 + + if get_user_by_username(username): + return jsonify({"error": "User already exists"}), 409 + + user_id = create_user(username, generate_password_hash(password), role=role, is_active=1) + return jsonify({"id": user_id, "username": username, "role": role, "is_active": 1}), 201 + + @app.get("/api/config") +@require_auth +@require_role("admin") def get_config_route(): config = {} for key in ALLOWED_KEYS: @@ -210,6 +410,8 @@ def get_config_route(): @app.post("/api/config") +@require_auth +@require_role("admin") def set_config_route(): updates = request.get_json(silent=True) if not isinstance(updates, dict): @@ -249,6 +451,8 @@ def set_config_route(): @app.post("/api/config/rescan") +@require_auth +@require_role("admin") def rescan_route(): _apply_smb_env_from_config() target_dir = resolve_runtime_path(get_config("target_dir")) @@ -267,6 +471,8 @@ def rescan_route(): @app.post("/api/config/rescan-results") +@require_auth +@require_role("admin") def rescan_results_route(): _apply_smb_env_from_config() results_dir = resolve_runtime_path(get_config("results_dir")) @@ -295,6 +501,7 @@ def static_or_spa(path=""): def bootstrap(): + _ensure_default_users() _apply_smb_env_from_config() target_dir = resolve_runtime_path(get_config("target_dir")) results_dir = resolve_runtime_path(get_config("results_dir")) diff --git a/server/dashboard.db b/server/dashboard.db deleted file mode 100644 index bff3e1c45fc5e3ee8ecf9c479faae7be7d1cf87d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544768 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.0.0,<4.0.0 Flask-Cors>=4.0.1,<5.0.0 smbprotocol>=1.13.0,<2.0.0 +PyJWT>=2.9.0,<3.0.0 +python-dotenv>=1.0.1,<2.0.0