feat: 认证相关
This commit is contained in:
@@ -63,6 +63,11 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
return ctx.session.user;
|
return ctx.session.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('info')
|
||||||
|
async info(ctx: Context) {
|
||||||
|
return ctx.session?.user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoginRequestBody {
|
interface LoginRequestBody {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
import KoaRouter from '@koa/router';
|
import KoaRouter from '@koa/router';
|
||||||
import { getRouteMetadata, getControllerPrefix, type RouteMetadata } from '../decorators/route.ts';
|
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) {
|
private wrapControllerMethod(instance: any, methodName: string) {
|
||||||
return async (ctx: Koa.Context, next: Koa.Next) => {
|
return async (ctx: Koa.Context, next: Koa.Next) => {
|
||||||
try {
|
// 调用控制器方法
|
||||||
// 调用控制器方法
|
const method = instance[methodName];
|
||||||
const method = instance[methodName];
|
if (typeof method !== 'function') {
|
||||||
if (typeof method !== 'function') {
|
ctx.throw(401, 'Not Found')
|
||||||
throw new Error(`控制器方法 ${methodName} 不存在或不是函数`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定this并调用方法
|
|
||||||
const result = await method.call(instance, ctx, next);
|
|
||||||
|
|
||||||
// 如果控制器返回了数据,则包装成统一响应格式
|
|
||||||
ctx.body = createAutoSuccessResponse(result);
|
|
||||||
} catch (error) {
|
|
||||||
// 错误由全局异常处理中间件处理
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绑定this并调用方法
|
||||||
|
const result = await method.call(instance, ctx, next) ?? null;
|
||||||
|
|
||||||
|
ctx.body = createSuccessResponse(result);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
apps/server/middlewares/authorization.ts
Normal file
23
apps/server/middlewares/authorization.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,7 +122,7 @@ export class Exception implements Middleware {
|
|||||||
*/
|
*/
|
||||||
export function createSuccessResponse<T>(
|
export function createSuccessResponse<T>(
|
||||||
data: T,
|
data: T,
|
||||||
message = '操作成功',
|
message = 'success',
|
||||||
): ApiResponse<T> {
|
): ApiResponse<T> {
|
||||||
return {
|
return {
|
||||||
code: 0,
|
code: 0,
|
||||||
@@ -132,24 +132,6 @@ export function createSuccessResponse<T>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建成功响应的辅助函数(自动设置消息)
|
|
||||||
*/
|
|
||||||
export function createAutoSuccessResponse<T>(data: T): ApiResponse<T> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建失败响应的辅助函数
|
* 创建失败响应的辅助函数
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Session } from './session.ts';
|
|||||||
import { CORS } from './cors.ts';
|
import { CORS } from './cors.ts';
|
||||||
import { HttpLogger } from './logger.ts';
|
import { HttpLogger } from './logger.ts';
|
||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
|
import { Authorization } from './Authorization.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化中间件
|
* 初始化中间件
|
||||||
@@ -20,10 +21,12 @@ export function initMiddlewares(app: Koa) {
|
|||||||
// Session 中间件需要在请求体解析之前注册
|
// Session 中间件需要在请求体解析之前注册
|
||||||
new Session().apply(app);
|
new Session().apply(app);
|
||||||
|
|
||||||
|
new CORS().apply(app);
|
||||||
|
|
||||||
|
new Authorization().apply(app);
|
||||||
|
|
||||||
// 请求体解析中间件
|
// 请求体解析中间件
|
||||||
new BodyParser().apply(app);
|
new BodyParser().apply(app);
|
||||||
|
|
||||||
new CORS().apply(app);
|
|
||||||
|
|
||||||
new Router().apply(app);
|
new Router().apply(app);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -15,7 +15,8 @@
|
|||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router": "^7.8.0"
|
"react-router": "^7.8.0",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@arco-plugins/unplugin-react": "2.0.0-beta.5",
|
"@arco-plugins/unplugin-react": "2.0.0-beta.5",
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from '@pages/App';
|
import App from '@pages/App';
|
||||||
import { BrowserRouter } from 'react-router';
|
import { BrowserRouter } from 'react-router';
|
||||||
|
import { useGlobalStore } from './stores/global';
|
||||||
|
|
||||||
const rootEl = document.getElementById('root');
|
const rootEl = document.getElementById('root');
|
||||||
|
|
||||||
if (rootEl) {
|
if (rootEl) {
|
||||||
const root = ReactDOM.createRoot(rootEl);
|
const root = ReactDOM.createRoot(rootEl);
|
||||||
|
useGlobalStore.getState().refreshUser();
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { Avatar, Layout, Menu } from '@arco-design/web-react';
|
import { Avatar, Dropdown, Layout, Menu } from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
IconApps,
|
IconApps,
|
||||||
|
IconExport,
|
||||||
IconMenuFold,
|
IconMenuFold,
|
||||||
IconMenuUnfold,
|
IconMenuUnfold,
|
||||||
IconRobot,
|
IconRobot,
|
||||||
IconUser,
|
|
||||||
} from '@arco-design/web-react/icon';
|
} from '@arco-design/web-react/icon';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Logo from '@assets/images/logo.svg?react';
|
import Logo from '@assets/images/logo.svg?react';
|
||||||
import { Link, Outlet } from 'react-router';
|
import { Link, Outlet } from 'react-router';
|
||||||
|
import { useGlobalStore } from '../../stores/global';
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className="h-screen w-full">
|
<Layout className="h-screen w-full">
|
||||||
@@ -53,12 +55,32 @@ function Home() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Header className="h-14 border-b-gray-100 border-b-[1px]">
|
<Layout.Header className="h-14 border-b-gray-100 border-b-[1px]">
|
||||||
<div className="flex items-center justify-end px-4 h-full">
|
<div className="flex items-center justify-end px-4 h-full">
|
||||||
<Avatar>
|
<Dropdown
|
||||||
<IconUser />
|
trigger={'click'}
|
||||||
</Avatar>
|
droplist={
|
||||||
|
<Menu className="px-3">
|
||||||
|
<Menu.Item key="1">
|
||||||
|
<IconExport />
|
||||||
|
<span className="ml-2">退出登录</span>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="p-2 rounded-xl cursor-pointer flex items-center hover:bg-[rgba(0,0,0,0.03)]">
|
||||||
|
<Avatar size={28}>
|
||||||
|
<img
|
||||||
|
alt="avatar"
|
||||||
|
src={globalStore.user?.avatar_url.replace('https', 'http')}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
<span className="ml-2 font-semibold text-gray-500">
|
||||||
|
{globalStore.user?.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
<Layout.Content>
|
<Layout.Content className="overflow-y-auto bg-gray-100">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ function ProjectPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 bg-gray-100 min-h-screen">
|
<div className="p-6">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title heading={2} className="!m-0 !text-gray-900">
|
<Typography.Title heading={2} className="!m-0 !text-gray-900">
|
||||||
|
|||||||
@@ -8,6 +8,24 @@ class Net {
|
|||||||
timeout: 20000,
|
timeout: 20000,
|
||||||
withCredentials: true,
|
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<T>(config: AxiosRequestConfig): Promise<T> {
|
async request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||||
|
|||||||
26
apps/web/src/stores/global.tsx
Normal file
26
apps/web/src/stores/global.tsx
Normal file
@@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGlobalStore = create<GlobalStore>((set) => ({
|
||||||
|
user: null,
|
||||||
|
async refreshUser() {
|
||||||
|
const { data } = await net.request<APIResponse<User>>({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/auth/info',
|
||||||
|
});
|
||||||
|
set({ user: data });
|
||||||
|
},
|
||||||
|
}));
|
||||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -90,6 +90,9 @@ importers:
|
|||||||
react-router:
|
react-router:
|
||||||
specifier: ^7.8.0
|
specifier: ^7.8.0
|
||||||
version: 7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
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:
|
devDependencies:
|
||||||
'@arco-plugins/unplugin-react':
|
'@arco-plugins/unplugin-react':
|
||||||
specifier: 2.0.0-beta.5
|
specifier: 2.0.0-beta.5
|
||||||
@@ -1915,6 +1918,24 @@ packages:
|
|||||||
zod@4.1.5:
|
zod@4.1.5:
|
||||||
resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==}
|
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:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
@@ -3664,3 +3685,8 @@ snapshots:
|
|||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|
||||||
zod@4.1.5: {}
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user