feat: 完善登录等接口鉴权

This commit is contained in:
R524809
2026-01-06 10:49:19 +08:00
parent 84ddca26b5
commit 76c22429ad
16 changed files with 572 additions and 93 deletions

View File

@@ -9,7 +9,7 @@
"build": "nest build", "build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"dev": "nest start --watch", "dev": "nest start --watch",
"start": "nest start", "start": "nest start --watch",
"start:dev": "nest start --watch", "start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",

View File

@@ -0,0 +1,67 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { User } from '../../user/user.entity';
/**
* 资源所有者或管理员权限 Guard
*
* 用途:检查用户是否有权限访问资源
* - 管理员admin、super_admin可以访问任何资源
* - 普通用户只能访问自己的资源(通过比较 userId
*
* 使用场景:
* - 查询用户信息:管理员可以查询任何用户,普通用户只能查询自己
* - 更新用户信息:管理员可以更新任何用户,普通用户只能更新自己
* - 删除用户:管理员可以删除任何用户,普通用户只能删除自己
*
* 使用示例:
* @Get(':id')
* @UseGuards(JwtAuthGuard, OwnerOrAdminGuard)
* findOneById(@Param('id') id: string) { ... }
*/
@Injectable()
export class OwnerOrAdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<{
user?: User;
params: { id?: string };
}>();
const user = request.user;
if (!user) {
throw new ForbiddenException('未授权访问');
}
// 获取请求的资源ID从路由参数中
const requestedId = request.params?.id;
if (!requestedId) {
// 如果没有提供资源ID只允许管理员访问
const isAdmin =
user.role === 'admin' || user.role === 'super_admin';
if (!isAdmin) {
throw new ForbiddenException('权限不足,需要管理员权限');
}
return true;
}
const requestedUserId = +requestedId;
// 检查权限:管理员可以访问任何资源,普通用户只能访问自己的资源
const isAdmin = user.role === 'admin' || user.role === 'super_admin';
const isOwner = user.userId === requestedUserId;
if (!isAdmin && !isOwner) {
throw new ForbiddenException(
'权限不足,只能访问自己的资源或需要管理员权限',
);
}
return true;
}
}

View File

@@ -6,7 +6,6 @@ import {
MinLength, MinLength,
MaxLength, MaxLength,
Matches, Matches,
IsIn,
} from 'class-validator'; } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
@@ -95,15 +94,4 @@ export class CreateUserDto {
@IsString() @IsString()
@MaxLength(100) @MaxLength(100)
unionId?: string; unionId?: string;
@ApiPropertyOptional({
description: '用户角色',
example: 'user',
enum: ['user', 'admin', 'super_admin'],
default: 'user',
})
@IsOptional()
@IsString()
@IsIn(['user', 'admin', 'super_admin'])
role?: string;
} }

View File

@@ -6,7 +6,6 @@ import {
Patch, Patch,
Param, Param,
Delete, Delete,
Query,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
UseGuards, UseGuards,
@@ -22,11 +21,11 @@ import { UserService } from './user.service';
import { User } from './user.entity'; import { User } from './user.entity';
import { CreateUserDto } from './dto/create-user.dto'; import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { QueryUserDto } from './dto/query-user.dto';
import { ChangePasswordDto } from './dto/change-password.dto'; import { ChangePasswordDto } from './dto/change-password.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard'; import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator'; import { Roles } from '../auth/decorators/roles.decorator';
import { OwnerOrAdminGuard } from '../auth/guards/owner-or-admin.guard';
@ApiTags('user') @ApiTags('user')
@Controller('user') @Controller('user')
@@ -60,8 +59,8 @@ export class UserController {
* 查询所有用户 * 查询所有用户
*/ */
@Get() @Get()
// @UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('admin', 'super_admin') @Roles('admin', 'super_admin')
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ @ApiOperation({
summary: '查询所有用户', summary: '查询所有用户',
@@ -78,35 +77,38 @@ export class UserController {
return this.userService.findAll(); return this.userService.findAll();
} }
/** // /**
* 根据 username 或 email 查询单个用户 // * 根据 username 或 email 查询单个用户
*/ // */
@Get('find') // @Get('find')
@ApiOperation({ // @ApiOperation({
summary: '查询单个用户', // summary: '查询单个用户',
description: '根据 username 或 email 查询用户', // description: '根据 username 或 email 查询用户',
}) // })
@ApiResponse({ // @ApiResponse({
status: 200, // status: 200,
description: '查询成功', // description: '查询成功',
type: User, // type: User,
}) // })
@ApiResponse({ // @ApiResponse({
status: 400, // status: 400,
description: '请求参数错误,必须提供 username 或 email', // description: '请求参数错误,必须提供 username 或 email',
}) // })
@ApiResponse({ status: 404, description: '用户不存在' }) // @ApiResponse({ status: 404, description: '用户不存在' })
findOne(@Query() queryDto: QueryUserDto): Promise<User> { // findOne(@Query() queryDto: QueryUserDto): Promise<User> {
return this.userService.findOne(queryDto); // return this.userService.findOne(queryDto);
} // }
/** /**
* 根据 ID 查询单个用户 * 根据 ID 查询单个用户
* 需要管理员权限或者是用户本人
*/ */
@Get(':id') @Get(':id')
@UseGuards(JwtAuthGuard, OwnerOrAdminGuard)
@ApiBearerAuth()
@ApiOperation({ @ApiOperation({
summary: '根据ID查询用户', summary: '根据ID查询用户',
description: '根据用户ID获取详细信息', description: '根据用户ID获取详细信息,需要管理员权限或者是用户本人',
}) })
@ApiParam({ name: 'id', description: '用户ID', type: Number }) @ApiParam({ name: 'id', description: '用户ID', type: Number })
@ApiResponse({ @ApiResponse({
@@ -114,6 +116,11 @@ export class UserController {
description: '查询成功', description: '查询成功',
type: User, type: User,
}) })
@ApiResponse({ status: 401, description: '未授权' })
@ApiResponse({
status: 403,
description: '权限不足,只能查询自己的信息或需要管理员权限',
})
@ApiResponse({ status: 404, description: '用户不存在' }) @ApiResponse({ status: 404, description: '用户不存在' })
findOneById(@Param('id') id: string): Promise<User> { findOneById(@Param('id') id: string): Promise<User> {
return this.userService.findOneById(+id); return this.userService.findOneById(+id);
@@ -123,6 +130,8 @@ export class UserController {
* 更新用户信息 * 更新用户信息
*/ */
@Patch(':id') @Patch(':id')
@UseGuards(JwtAuthGuard, OwnerOrAdminGuard)
@ApiBearerAuth()
@ApiOperation({ @ApiOperation({
summary: '更新用户信息', summary: '更新用户信息',
description: '更新用户信息,不允许修改 username、openId、unionId', description: '更新用户信息,不允许修改 username、openId、unionId',
@@ -147,6 +156,8 @@ export class UserController {
*/ */
@Patch(':id/password') @Patch(':id/password')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(JwtAuthGuard, OwnerOrAdminGuard)
@ApiBearerAuth()
@ApiOperation({ @ApiOperation({
summary: '修改密码', summary: '修改密码',
description: '修改用户密码,需要先验证旧密码', description: '修改用户密码,需要先验证旧密码',
@@ -167,6 +178,8 @@ export class UserController {
*/ */
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(JwtAuthGuard, OwnerOrAdminGuard)
@ApiBearerAuth()
@ApiOperation({ @ApiOperation({
summary: '删除用户', summary: '删除用户',
description: '软删除用户,将状态更新为 deleted', description: '软删除用户,将状态更新为 deleted',

View File

@@ -91,6 +91,7 @@ export class UserService {
); );
// 创建用户 // 创建用户
// 注意:所有注册用户的 role 固定为 'user',不允许通过注册接口设置其他角色
const user = this.userRepository.create({ const user = this.userRepository.create({
username: createUserDto.username, username: createUserDto.username,
passwordHash, passwordHash,
@@ -101,7 +102,7 @@ export class UserService {
openId: createUserDto.openId, openId: createUserDto.openId,
unionId: createUserDto.unionId, unionId: createUserDto.unionId,
status: 'active', status: 'active',
role: createUserDto.role || 'user', // 默认为普通用户 role: 'user', // 固定为普通用户,不允许通过注册接口修改
}); });
return this.userRepository.save(user); return this.userRepository.save(user);

View File

@@ -229,7 +229,7 @@ describe('UserService (集成测试)', () => {
password: 'password123', password: 'password123',
email: 'findall3@example.com', email: 'findall3@example.com',
nickname: '查询测试用户3', nickname: '查询测试用户3',
role: 'admin', // 注意:注册时不允许传入 role所有注册用户的 role 固定为 'user'
}, },
]; ];
@@ -288,7 +288,7 @@ describe('UserService (集成测试)', () => {
nickname: '完整信息用户', nickname: '完整信息用户',
avatarUrl: 'https://example.com/avatar.jpg', avatarUrl: 'https://example.com/avatar.jpg',
phone: '13800138000', phone: '13800138000',
role: 'admin', // 注意:注册时不允许传入 role所有注册用户的 role 固定为 'user'
}; };
await service.create(createUserDto); await service.create(createUserDto);
@@ -307,7 +307,7 @@ describe('UserService (集成测试)', () => {
expect(foundUser?.nickname).toBe(createUserDto.nickname); expect(foundUser?.nickname).toBe(createUserDto.nickname);
expect(foundUser?.avatarUrl).toBe(createUserDto.avatarUrl); expect(foundUser?.avatarUrl).toBe(createUserDto.avatarUrl);
expect(foundUser?.phone).toBe(createUserDto.phone); expect(foundUser?.phone).toBe(createUserDto.phone);
expect(foundUser?.role).toBe(createUserDto.role); expect(foundUser?.role).toBe('user'); // 注册用户的 role 固定为 'user'
expect(foundUser?.status).toBe('active'); expect(foundUser?.status).toBe('active');
expect(foundUser?.createdAt).toBeDefined(); expect(foundUser?.createdAt).toBeDefined();
expect(foundUser?.updatedAt).toBeDefined(); expect(foundUser?.updatedAt).toBeDefined();

View File

@@ -0,0 +1,23 @@
import { ReactNode } from 'react';
import { Navigate } from 'react-router';
import { authService } from '../services/auth';
interface ProtectedRouteProps {
children: ReactNode;
}
/**
* 路由守卫组件
* 用于保护需要登录才能访问的页面
*/
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
const isAuthenticated = authService.isAuthenticated();
if (!isAuthenticated) {
// 未登录,重定向到登录页
return <Navigate to="/login" replace />;
}
// 已登录,渲染子组件
return <>{children}</>;
}

View File

@@ -1,14 +1,17 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router'; import { Outlet, useLocation, useNavigate } from 'react-router';
import { Layout, Menu, Avatar, Badge, Drawer } from 'antd'; import { Layout, Menu, Avatar, Badge, Drawer, Dropdown, Button, message } from 'antd';
import { import {
BarChartOutlined, BarChartOutlined,
FileTextOutlined, FileTextOutlined,
EditOutlined, EditOutlined,
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
LogoutOutlined,
UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import { authService } from '../services/auth';
import './MainLayout.css'; import './MainLayout.css';
const { Header, Sider, Content } = Layout; const { Header, Sider, Content } = Layout;
@@ -29,9 +32,16 @@ const MainLayout = ({ isAdmin = false }: MainLayoutProps) => {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const [user, setUser] = useState<UserInfo | null>(null);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
// 获取用户信息
useEffect(() => {
const currentUser = authService.getUser();
setUser(currentUser);
}, []);
// 根据路由获取页面标题 // 根据路由获取页面标题
const pageInfo = useMemo(() => { const pageInfo = useMemo(() => {
return pageTitles[location.pathname] || pageTitles['/']; return pageTitles[location.pathname] || pageTitles['/'];
@@ -78,6 +88,51 @@ const MainLayout = ({ isAdmin = false }: MainLayoutProps) => {
} }
}; };
// 处理登出
const handleLogout = () => {
authService.logout();
message.success('已退出登录');
navigate('/login', { replace: true });
};
// 用户下拉菜单
const userMenuItems: MenuProps['items'] = [
{
key: 'profile',
icon: <UserOutlined />,
label: '个人资料',
disabled: true, // 暂时禁用
},
{
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
danger: true,
onClick: handleLogout,
},
];
// 获取角色显示文本
const getRoleText = (role: string) => {
const roleMap: Record<string, string> = {
user: '普通用户',
admin: '管理员',
super_admin: '超级管理员',
};
return roleMap[role] || '普通用户';
};
// 获取角色状态
const getRoleStatus = (role: string): 'success' | 'warning' | 'error' => {
if (role === 'admin' || role === 'super_admin') {
return 'warning';
}
return 'success';
};
// 获取当前选中的菜单项 // 获取当前选中的菜单项
const selectedKeys = useMemo(() => { const selectedKeys = useMemo(() => {
const path = location.pathname; const path = location.pathname;
@@ -171,23 +226,32 @@ const MainLayout = ({ isAdmin = false }: MainLayoutProps) => {
</div> </div>
</div> </div>
<div className="header-right"> <div className="header-right">
<div className="user-info"> <Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
<div className="user-info" style={{ cursor: 'pointer' }}>
<Avatar <Avatar
style={{ style={{
backgroundColor: '#8b5cf6', backgroundColor: '#8b5cf6',
verticalAlign: 'middle', verticalAlign: 'middle',
}} }}
src={user?.avatarUrl}
> >
U {user?.nickname?.[0] || user?.username?.[0] || 'U'}
</Avatar> </Avatar>
<div className="user-details"> <div className="user-details">
<div className="user-name"></div> <div className="user-name">
{user?.nickname || user?.username || '用户'}
</div>
<Badge <Badge
status="success" status={user ? getRoleStatus(user.role) : 'success'}
text={<span className="user-role"></span>} text={
<span className="user-role">
{user ? getRoleText(user.role) : '普通用户'}
</span>
}
/> />
</div> </div>
</div> </div>
</Dropdown>
</div> </div>
</Header> </Header>

View File

@@ -0,0 +1,58 @@
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.login-box {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-header h1 {
margin: 0 0 8px 0;
font-size: 32px;
font-weight: 600;
color: var(--primary-color, #8b5cf6);
}
.login-header p {
margin: 0;
color: var(--text-secondary, #6b7280);
font-size: 14px;
}
.login-form {
margin-top: 24px;
}
.login-form .ant-input-affix-wrapper,
.login-form .ant-input {
border-radius: 8px;
}
.login-button {
height: 44px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
background: var(--primary-color, #8b5cf6);
border-color: var(--primary-color, #8b5cf6);
}
.login-button:hover {
background: var(--primary-dark, #7c3aed);
border-color: var(--primary-dark, #7c3aed);
}

View File

@@ -0,0 +1,95 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { Button, Form, Input, message } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { authService } from '../services/auth';
import type { LoginRequest } from '@/types/user';
import './LoginPage.css';
export default function LoginPage() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
// 如果已登录,重定向到首页
useEffect(() => {
if (authService.isAuthenticated()) {
navigate('/', { replace: true });
}
}, [navigate]);
const onFinish = async (values: LoginRequest) => {
setLoading(true);
try {
await authService.login(values);
message.success('登录成功');
navigate('/', { replace: true });
} catch (error: any) {
message.error(error.message || '登录失败,请检查用户名和密码');
} finally {
setLoading(false);
}
};
return (
<div className="login-container">
<div className="login-box">
<div className="login-header">
<h1></h1>
<p></p>
</div>
<Form
name="login"
onFinish={onFinish}
autoComplete="off"
size="large"
className="login-form"
>
<Form.Item
name="usernameOrEmail"
rules={[
{
required: true,
message: '请输入用户名或邮箱',
},
{
min: 3,
message: '用户名或邮箱至少3个字符',
},
]}
>
<Input prefix={<UserOutlined />} placeholder="用户名或邮箱" />
</Form.Item>
<Form.Item
name="password"
rules={[
{
required: true,
message: '请输入密码',
},
{
min: 6,
message: '密码至少6个字符',
},
]}
>
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
loading={loading}
block
className="login-button"
>
</Button>
</Form.Item>
</Form>
</div>
</div>
);
}

View File

@@ -1,13 +1,23 @@
import { createBrowserRouter } from 'react-router'; import { createBrowserRouter } from 'react-router';
import MainLayout from '../layouts/MainLayout'; import MainLayout from '../layouts/MainLayout';
import ProtectedRoute from '../components/ProtectedRoute';
import LoginPage from '../pages/LoginPage';
import AssetsPage from '../pages/AssetsPage'; import AssetsPage from '../pages/AssetsPage';
import PlansPage from '../pages/PlansPage'; import PlansPage from '../pages/PlansPage';
import ReviewPage from '../pages/ReviewPage'; import ReviewPage from '../pages/ReviewPage';
export const router = createBrowserRouter([ export const router = createBrowserRouter([
{
path: '/login',
element: <LoginPage />,
},
{ {
path: '/', path: '/',
element: <MainLayout />, element: (
<ProtectedRoute>
<MainLayout />
</ProtectedRoute>
),
children: [ children: [
{ {
index: true, index: true,

View File

@@ -1,6 +1,10 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { envConfig } from '../config/env'; import { envConfig } from '../config/env';
// Token 存储键名(与 auth service 保持一致)
const TOKEN_KEY = 'access_token';
// 创建 axios 实例 // 创建 axios 实例
const apiClient: AxiosInstance = axios.create({ const apiClient: AxiosInstance = axios.create({
baseURL: envConfig.apiBaseUrl, baseURL: envConfig.apiBaseUrl,
@@ -13,11 +17,11 @@ const apiClient: AxiosInstance = axios.create({
// 请求拦截器 // 请求拦截器
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config) => { (config) => {
// 可以在这里添加 token 等认证信息 // 自动添加 token 到请求头
// const token = localStorage.getItem('token') const token = localStorage.getItem(TOKEN_KEY);
// if (token) { if (token) {
// config.headers.Authorization = `Bearer ${token}` config.headers.Authorization = `Bearer ${token}`;
// } }
if (envConfig.debug) { if (envConfig.debug) {
console.log('请求:', config.method?.toUpperCase(), config.url); console.log('请求:', config.method?.toUpperCase(), config.url);
@@ -50,8 +54,13 @@ apiClient.interceptors.response.use(
if (error.response) { if (error.response) {
switch (error.response.status) { switch (error.response.status) {
case 401: case 401:
// 未授权,可以跳转到登录页 // 未授权,清除 token 并跳转到登录页
// window.location.href = '/login' localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem('user_info');
// 避免在登录页重复跳转
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
break; break;
case 403: case 403:
console.error('没有权限访问该资源'); console.error('没有权限访问该资源');

View File

@@ -0,0 +1,104 @@
import { api } from './api';
import type { UserInfo, LoginRequest, LoginResponse } from '@/types/user';
import type { ApiResponse } from '@/types/common';
/**
* Token 存储键名
*/
const TOKEN_KEY = 'access_token';
const USER_KEY = 'user_info';
/**
* 认证服务
*/
class AuthService {
/**
* 登录
*/
async login(credentials: LoginRequest): Promise<LoginResponse> {
const response = await api.post<ApiResponse<LoginResponse>>('/auth/login', credentials);
if (response.code === 0 && response.data) {
this.setToken(response.data.accessToken);
this.setUser(response.data.user);
return response.data;
}
throw new Error(response.message || '登录失败');
}
/**
* 登出
*/
logout(): void {
this.removeToken();
this.removeUser();
}
/**
* 获取当前 token
*/
getToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
/**
* 设置 token
*/
setToken(token: string): void {
localStorage.setItem(TOKEN_KEY, token);
}
/**
* 移除 token
*/
removeToken(): void {
localStorage.removeItem(TOKEN_KEY);
}
/**
* 获取当前用户信息
*/
getUser(): UserInfo | null {
const userStr = localStorage.getItem(USER_KEY);
if (!userStr) return null;
try {
return JSON.parse(userStr);
} catch {
return null;
}
}
/**
* 设置用户信息
*/
setUser(user: UserInfo): void {
localStorage.setItem(USER_KEY, JSON.stringify(user));
}
/**
* 移除用户信息
*/
removeUser(): void {
localStorage.removeItem(USER_KEY);
}
/**
* 检查是否已登录
*/
isAuthenticated(): boolean {
return !!this.getToken();
}
/**
* 检查是否为管理员
*/
isAdmin(): boolean {
const user = this.getUser();
return user?.role === 'admin' || user?.role === 'super_admin';
}
}
// 导出单例
export const authService = new AuthService();

View File

@@ -0,0 +1,9 @@
/**
* API 响应包装
*/
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
timestamp?: string;
}

View File

@@ -0,0 +1,34 @@
/**
* 用户信息接口(排除密码哈希)
*/
export interface UserInfo {
userId: number;
username: string;
email: string;
nickname?: string;
phone?: string;
avatarUrl?: string;
openId?: string;
unionId?: string;
role: string;
status: string;
createdAt: Date;
updatedAt: Date;
lastLoginAt?: Date;
}
/**
* 登录请求参数
*/
export interface LoginRequest {
usernameOrEmail: string;
password: string;
}
/**
* 登录响应
*/
export interface LoginResponse {
accessToken: string;
user: UserInfo;
}

View File

@@ -22,7 +22,11 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true,
"paths": {
"@/*": ["./src/*"]
},
"typeRoots": ["./node_modules/@types", "./src/types"]
}, },
"include": ["src"] "include": ["src"]
} }