mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 15:33:18 +08:00
feat(auth): add local login, audit, API keys, and system admin UI
Gate netx Web/API/WebCRT with JWT and per-user API tokens, bootstrap an admin with forced password change, and expose users/audit/API-key management under a System section. MCP can reuse data/auth/mcp_token without extra env for local labs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
14f14d34bd
commit
6d4cd741ef
35 changed files with 2699 additions and 21 deletions
108
web/src/auth/AuthContext.tsx
Normal file
108
web/src/auth/AuthContext.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { apiGet, apiPost, clearAuthToken, getAuthToken, setAuthToken } from "../services/api";
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
is_active: boolean;
|
||||
must_change_password?: boolean;
|
||||
created_by?: string;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
};
|
||||
|
||||
type AuthState = {
|
||||
ready: boolean;
|
||||
token: string | null;
|
||||
user: AuthUser | null;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
refreshMe: () => Promise<void>;
|
||||
isAdmin: boolean;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthState | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [token, setToken] = useState<string | null>(() => getAuthToken());
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
|
||||
const refreshMe = useCallback(async () => {
|
||||
const tok = getAuthToken();
|
||||
if (!tok) {
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await apiGet<{ user: AuthUser }>("/v1/auth/me");
|
||||
setToken(tok);
|
||||
setUser(data.user);
|
||||
} catch {
|
||||
clearAuthToken();
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
await refreshMe();
|
||||
setReady(true);
|
||||
})();
|
||||
}, [refreshMe]);
|
||||
|
||||
const login = useCallback(async (username: string, password: string) => {
|
||||
const data = await apiPost<{ access_token: string; user: AuthUser }>("/v1/auth/login", {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
setAuthToken(data.access_token);
|
||||
setToken(data.access_token);
|
||||
setUser(data.user);
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
if (getAuthToken()) {
|
||||
await apiPost("/v1/auth/logout", {});
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
clearAuthToken();
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthState>(
|
||||
() => ({
|
||||
ready,
|
||||
token,
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
refreshMe,
|
||||
isAdmin: user?.role === "admin",
|
||||
}),
|
||||
[ready, token, user, login, logout, refreshMe],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthState {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuth outside AuthProvider");
|
||||
return ctx;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue