diff --git a/apps/server/controllers/auth/index.ts b/apps/server/controllers/auth/index.ts index e0e0f75..f1e85a5 100644 --- a/apps/server/controllers/auth/index.ts +++ b/apps/server/controllers/auth/index.ts @@ -63,6 +63,11 @@ export class AuthController { } return ctx.session.user; } + + @Get('info') + async info(ctx: Context) { + return ctx.session?.user; + } } interface LoginRequestBody { diff --git a/apps/server/libs/route-scanner.ts b/apps/server/libs/route-scanner.ts index ced774c..c676abd 100644 --- a/apps/server/libs/route-scanner.ts +++ b/apps/server/libs/route-scanner.ts @@ -1,7 +1,7 @@ import type Koa from 'koa'; import KoaRouter from '@koa/router'; import { getRouteMetadata, getControllerPrefix, type RouteMetadata } from '../decorators/route.ts'; -import { createAutoSuccessResponse } from '../middlewares/exception.ts'; +import { createSuccessResponse } from '../middlewares/exception.ts'; /** * 控制器类型 @@ -102,22 +102,16 @@ export class RouteScanner { */ private wrapControllerMethod(instance: any, methodName: string) { return async (ctx: Koa.Context, next: Koa.Next) => { - try { - // 调用控制器方法 - const method = instance[methodName]; - if (typeof method !== 'function') { - throw new Error(`控制器方法 ${methodName} 不存在或不是函数`); - } - - // 绑定this并调用方法 - const result = await method.call(instance, ctx, next); - - // 如果控制器返回了数据,则包装成统一响应格式 - ctx.body = createAutoSuccessResponse(result); - } catch (error) { - // 错误由全局异常处理中间件处理 - throw error; + // 调用控制器方法 + const method = instance[methodName]; + if (typeof method !== 'function') { + ctx.throw(401, 'Not Found') } + + // 绑定this并调用方法 + const result = await method.call(instance, ctx, next) ?? null; + + ctx.body = createSuccessResponse(result); }; } diff --git a/apps/server/middlewares/authorization.ts b/apps/server/middlewares/authorization.ts new file mode 100644 index 0000000..0c98ec8 --- /dev/null +++ b/apps/server/middlewares/authorization.ts @@ -0,0 +1,23 @@ +import type Koa from 'koa'; +import type { Middleware } from './types.ts'; + +export class Authorization implements Middleware { + private readonly ignoreAuth = [ + '/api/auth/login', + '/api/auth/info', + '/api/auth/url', + ]; + + apply(app: Koa) { + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + console.log('ctx.path', ctx.path) + if (this.ignoreAuth.includes(ctx.path)) { + return next(); + } + if (ctx.session.isNew) { + ctx.throw(401, 'Unauthorized'); + } + await next(); + }); + } +} diff --git a/apps/server/middlewares/exception.ts b/apps/server/middlewares/exception.ts index c9d3b59..fdd0273 100644 --- a/apps/server/middlewares/exception.ts +++ b/apps/server/middlewares/exception.ts @@ -122,7 +122,7 @@ export class Exception implements Middleware { */ export function createSuccessResponse( data: T, - message = '操作成功', + message = 'success', ): ApiResponse { return { code: 0, @@ -132,24 +132,6 @@ export function createSuccessResponse( }; } -/** - * 创建成功响应的辅助函数(自动设置消息) - */ -export function createAutoSuccessResponse(data: T): ApiResponse { - let message = '操作成功'; - - // 根据数据类型自动生成消息 - if (Array.isArray(data)) { - message = `获取列表成功,共${data.length}条记录`; - } else if (data && typeof data === 'object') { - message = '获取数据成功'; - } else if (data === null || data === undefined) { - message = '操作完成'; - } - - return createSuccessResponse(data, message); -} - /** * 创建失败响应的辅助函数 */ diff --git a/apps/server/middlewares/index.ts b/apps/server/middlewares/index.ts index c19ac4a..2be254c 100644 --- a/apps/server/middlewares/index.ts +++ b/apps/server/middlewares/index.ts @@ -5,6 +5,7 @@ import { Session } from './session.ts'; import { CORS } from './cors.ts'; import { HttpLogger } from './logger.ts'; import type Koa from 'koa'; +import { Authorization } from './Authorization.ts'; /** * 初始化中间件 @@ -20,10 +21,12 @@ export function initMiddlewares(app: Koa) { // Session 中间件需要在请求体解析之前注册 new Session().apply(app); + new CORS().apply(app); + + new Authorization().apply(app); + // 请求体解析中间件 new BodyParser().apply(app); - new CORS().apply(app); - new Router().apply(app); } diff --git a/apps/server/prisma/data/dev.db b/apps/server/prisma/data/dev.db index 07d8db1..cead5ad 100644 Binary files a/apps/server/prisma/data/dev.db and b/apps/server/prisma/data/dev.db differ diff --git a/apps/web/package.json b/apps/web/package.json index 2a52a25..83cbb0d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,7 +15,8 @@ "axios": "^1.11.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router": "^7.8.0" + "react-router": "^7.8.0", + "zustand": "^5.0.8" }, "devDependencies": { "@arco-plugins/unplugin-react": "2.0.0-beta.5", diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index 43d7132..b0b138b 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -1,11 +1,14 @@ import ReactDOM from 'react-dom/client'; import App from '@pages/App'; import { BrowserRouter } from 'react-router'; +import { useGlobalStore } from './stores/global'; const rootEl = document.getElementById('root'); if (rootEl) { const root = ReactDOM.createRoot(rootEl); + useGlobalStore.getState().refreshUser(); + root.render( diff --git a/apps/web/src/pages/home/index.tsx b/apps/web/src/pages/home/index.tsx index e660d15..6bcebda 100644 --- a/apps/web/src/pages/home/index.tsx +++ b/apps/web/src/pages/home/index.tsx @@ -1,17 +1,19 @@ -import { Avatar, Layout, Menu } from '@arco-design/web-react'; +import { Avatar, Dropdown, Layout, Menu } from '@arco-design/web-react'; import { IconApps, + IconExport, IconMenuFold, IconMenuUnfold, IconRobot, - IconUser, } from '@arco-design/web-react/icon'; import { useState } from 'react'; import Logo from '@assets/images/logo.svg?react'; import { Link, Outlet } from 'react-router'; +import { useGlobalStore } from '../../stores/global'; function Home() { const [collapsed, setCollapsed] = useState(false); + const globalStore = useGlobalStore(); return ( @@ -53,12 +55,32 @@ function Home() {
- - - + + + + 退出登录 + + + } + > +
+ + avatar + + + {globalStore.user?.username} + +
+
- +
diff --git a/apps/web/src/pages/project/index.tsx b/apps/web/src/pages/project/index.tsx index 72422f4..5507d11 100644 --- a/apps/web/src/pages/project/index.tsx +++ b/apps/web/src/pages/project/index.tsx @@ -61,7 +61,7 @@ function ProjectPage() { }; return ( -
+
diff --git a/apps/web/src/shared/request.ts b/apps/web/src/shared/request.ts index 3ca8d60..f18ac94 100644 --- a/apps/web/src/shared/request.ts +++ b/apps/web/src/shared/request.ts @@ -8,6 +8,24 @@ class Net { timeout: 20000, withCredentials: true, }); + + this.applyInterceptors(this.instance); + } + + private applyInterceptors(instance: Axios) { + instance.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + console.log('error', error) + if (error.status === 401 && error.config.url !== '/api/auth/info') { + window.location.href = '/login'; + return; + } + return Promise.reject(error); + }, + ); } async request(config: AxiosRequestConfig): Promise { diff --git a/apps/web/src/stores/global.tsx b/apps/web/src/stores/global.tsx new file mode 100644 index 0000000..ddd34ce --- /dev/null +++ b/apps/web/src/stores/global.tsx @@ -0,0 +1,26 @@ +import { net, type APIResponse } from '@shared'; +import { create } from 'zustand'; + +interface User { + id: string; + username: string; + email: string; + avatar_url: string; + active: boolean; +} + +interface GlobalStore { + user: User | null; + refreshUser: () => Promise; +} + +export const useGlobalStore = create((set) => ({ + user: null, + async refreshUser() { + const { data } = await net.request>({ + method: 'GET', + url: '/api/auth/info', + }); + set({ user: data }); + }, +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eaf888f..ad1e61a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ importers: react-router: specifier: ^7.8.0 version: 7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@19.1.12)(react@18.3.1) devDependencies: '@arco-plugins/unplugin-react': specifier: 2.0.0-beta.5 @@ -1915,6 +1918,24 @@ packages: zod@4.1.5: resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==} + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -3664,3 +3685,8 @@ snapshots: zod@3.25.76: {} zod@4.1.5: {} + + zustand@5.0.8(@types/react@19.1.12)(react@18.3.1): + optionalDependencies: + '@types/react': 19.1.12 + react: 18.3.1