diff --git a/backend/main.py b/backend/main.py index cc4c200..9503259 100644 --- a/backend/main.py +++ b/backend/main.py @@ -305,6 +305,25 @@ async def reset_password( "reset_by": current_user.email, } + +@app.get("/api/auth/users") +async def list_users( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_admin), +): + """Admin-only: list all users (id, email, name, role, is_active).""" + users = db.query(User).order_by(User.id.asc()).all() + return [ + { + "id": u.id, + "email": u.email, + "name": u.name, + "role": u.role, + "is_active": u.is_active, + } + for u in users + ] + # ============ Excel File Storage ============ def get_excel_path(section: str) -> str: """Get path to the stored Excel file for a section""" diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 42f99c2..a974994 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react" -import { Trash2, AlertTriangle, Shield, Download, Upload, RefreshCw, Database, Info } from "lucide-react" +import { Trash2, AlertTriangle, Shield, Download, Upload, RefreshCw, Database, Info, KeyRound, User as UserIcon } from "lucide-react" import api from "../api" import toast from "react-hot-toast" @@ -41,11 +41,68 @@ export default function Settings() { const [restoring, setRestoring] = useState(null) const [restoreConfirm, setRestoreConfirm] = useState("") + // User management (admin only) + const [users, setUsers] = useState([]) + const [loadingUsers, setLoadingUsers] = useState(false) + const [resetTarget, setResetTarget] = useState(null) + const [newPassword, setNewPassword] = useState("") + const [resetting, setResetting] = useState(false) + const [resetError, setResetError] = useState(null) + useEffect(() => { fetchVersion() fetchBackups() + fetchUsers() }, []) + const fetchUsers = async () => { + setLoadingUsers(true) + try { + const { data } = await api.get("/api/auth/users") + setUsers(data || []) + } catch (e) { + // 403 if not admin — leave list empty + setUsers([]) + } finally { + setLoadingUsers(false) + } + } + + const openResetDialog = (u) => { + setResetTarget(u) + setNewPassword("") + setResetError(null) + } + + const closeResetDialog = () => { + if (resetting) return + setResetTarget(null) + setNewPassword("") + setResetError(null) + } + + const handleResetPassword = async () => { + if (!resetTarget || !newPassword) return + if (newPassword.length < 6) { + setResetError("密碼至少需要 6 個字元") + return + } + setResetting(true) + setResetError(null) + try { + await api.post("/api/auth/reset-password", { + email: resetTarget.email, + new_password: newPassword, + }) + toast.success(`已重設 ${resetTarget.email} 嘅密碼`) + closeResetDialog() + } catch (e) { + setResetError(e.response?.data?.detail || e.message) + } finally { + setResetting(false) + } + } + const fetchVersion = async () => { try { const { data } = await api.get("/api/version") @@ -273,6 +330,122 @@ export default function Settings() { )} + {/* User Management — admin only */} +
+
+
+ +

使用者管理 User Management

+
+

+ 重設使用者密碼。Admin 限。 +

+
+ +
+ {loadingUsers && ( +
載入中…
+ )} + {!loadingUsers && users.length === 0 && ( +
+ 沒有使用者(或你唔係 admin) +
+ )} + {!loadingUsers && users.map((u) => ( +
+
+
+ +
+
+
+ {u.name || u.email} +
+
+ {u.email} + + {u.role} + + {!u.is_active && ( + + inactive + + )} +
+
+
+ +
+ ))} +
+
+ + {/* Reset Password Modal */} + {resetTarget && ( +
+
e.stopPropagation()} + > +
+

重設密碼

+

+ 為 {resetTarget.email} 設定新密碼 +

+
+
+ + {resetError && ( +
+ ❌ {resetError} +
+ )} +
+
+ + +
+
+
+ )} + {/* Danger Zone */}