feat(m0): 后端骨架 + Web/安卓前端骨架 + 设计文档
- backend: FastAPI 应用工厂、统一错误信封、JWT、短信登录、工作区/成员/邀请、幂等中间件 - alembic 0001: workspace/member/invitation/audit_log/app_user + RLS 策略 + 双角色 GRANT - tests: 认证/工作区/租户隔离/幂等 四组用例(尚未在本机执行) - web: React18+Vite 骨架(登录、工作区、邀请落地页) - android: Compose 骨架(登录、工作区列表) - 2.Design: PRODUCT-PLAN / ROADMAP-M0-M1 / NAMING / DEVELOPMENT
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Navigate, Route, Routes, useNavigate } from "react-router-dom";
|
||||
import { api, authExpired, session } from "./lib/client";
|
||||
import type { User, Workspace } from "./lib/types";
|
||||
import { LoginPage } from "./pages/Login";
|
||||
import { HomePage } from "./pages/Home";
|
||||
import { WorkspacePage } from "./pages/Workspace";
|
||||
import { JoinPage } from "./pages/Join";
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
workspaces: Workspace[];
|
||||
loading: boolean;
|
||||
reload: () => Promise<void>;
|
||||
signOut: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthState | null>(null);
|
||||
|
||||
export function useAuth(): AuthState {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuth 必须在 AuthProvider 内使用");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
if (!session.access) {
|
||||
setUser(null);
|
||||
setWorkspaces([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const me = await api.get<{ user: User; workspaces: Workspace[] }>("/auth/me");
|
||||
setUser(me.user);
|
||||
setWorkspaces(me.workspaces);
|
||||
} catch {
|
||||
setUser(null);
|
||||
setWorkspaces([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void reload();
|
||||
const onExpired = () => {
|
||||
setUser(null);
|
||||
setWorkspaces([]);
|
||||
navigate("/login", { replace: true });
|
||||
};
|
||||
authExpired.addEventListener("expired", onExpired);
|
||||
return () => authExpired.removeEventListener("expired", onExpired);
|
||||
}, [reload, navigate]);
|
||||
|
||||
const signOut = useCallback(() => {
|
||||
session.clear();
|
||||
setUser(null);
|
||||
setWorkspaces([]);
|
||||
navigate("/login", { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
const value = useMemo<AuthState>(
|
||||
() => ({ user, workspaces, loading, reload, signOut }),
|
||||
[user, workspaces, loading, reload, signOut],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/w/:wsId" element={<WorkspacePage />} />
|
||||
<Route path="/join/:wsId/:code" element={<JoinPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user