diff --git a/apps/api/package.json b/apps/api/package.json index 2337a99..2fadf80 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,7 +9,7 @@ "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "dev": "nest start --watch", - "start": "nest start", + "start": "nest start --watch", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", diff --git a/apps/api/src/modules/auth/guards/owner-or-admin.guard.ts b/apps/api/src/modules/auth/guards/owner-or-admin.guard.ts new file mode 100644 index 0000000..a28cd1b --- /dev/null +++ b/apps/api/src/modules/auth/guards/owner-or-admin.guard.ts @@ -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; + } +} diff --git a/apps/api/src/modules/user/dto/create-user.dto.ts b/apps/api/src/modules/user/dto/create-user.dto.ts index 25c6ff2..fac695a 100644 --- a/apps/api/src/modules/user/dto/create-user.dto.ts +++ b/apps/api/src/modules/user/dto/create-user.dto.ts @@ -6,7 +6,6 @@ import { MinLength, MaxLength, Matches, - IsIn, } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; @@ -95,15 +94,4 @@ export class CreateUserDto { @IsString() @MaxLength(100) unionId?: string; - - @ApiPropertyOptional({ - description: '用户角色', - example: 'user', - enum: ['user', 'admin', 'super_admin'], - default: 'user', - }) - @IsOptional() - @IsString() - @IsIn(['user', 'admin', 'super_admin']) - role?: string; } diff --git a/apps/api/src/modules/user/user.controller.ts b/apps/api/src/modules/user/user.controller.ts index 3d81863..a245187 100644 --- a/apps/api/src/modules/user/user.controller.ts +++ b/apps/api/src/modules/user/user.controller.ts @@ -6,7 +6,6 @@ import { Patch, Param, Delete, - Query, HttpCode, HttpStatus, UseGuards, @@ -22,11 +21,11 @@ import { UserService } from './user.service'; import { User } from './user.entity'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; -import { QueryUserDto } from './dto/query-user.dto'; import { ChangePasswordDto } from './dto/change-password.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; import { Roles } from '../auth/decorators/roles.decorator'; +import { OwnerOrAdminGuard } from '../auth/guards/owner-or-admin.guard'; @ApiTags('user') @Controller('user') @@ -60,8 +59,8 @@ export class UserController { * 查询所有用户 */ @Get() - // @UseGuards(JwtAuthGuard, RolesGuard) - // @Roles('admin', 'super_admin') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin', 'super_admin') @ApiBearerAuth() @ApiOperation({ summary: '查询所有用户', @@ -78,35 +77,38 @@ export class UserController { return this.userService.findAll(); } - /** - * 根据 username 或 email 查询单个用户 - */ - @Get('find') - @ApiOperation({ - summary: '查询单个用户', - description: '根据 username 或 email 查询用户', - }) - @ApiResponse({ - status: 200, - description: '查询成功', - type: User, - }) - @ApiResponse({ - status: 400, - description: '请求参数错误,必须提供 username 或 email', - }) - @ApiResponse({ status: 404, description: '用户不存在' }) - findOne(@Query() queryDto: QueryUserDto): Promise { - return this.userService.findOne(queryDto); - } + // /** + // * 根据 username 或 email 查询单个用户 + // */ + // @Get('find') + // @ApiOperation({ + // summary: '查询单个用户', + // description: '根据 username 或 email 查询用户', + // }) + // @ApiResponse({ + // status: 200, + // description: '查询成功', + // type: User, + // }) + // @ApiResponse({ + // status: 400, + // description: '请求参数错误,必须提供 username 或 email', + // }) + // @ApiResponse({ status: 404, description: '用户不存在' }) + // findOne(@Query() queryDto: QueryUserDto): Promise { + // return this.userService.findOne(queryDto); + // } /** * 根据 ID 查询单个用户 + * 需要管理员权限或者是用户本人 */ @Get(':id') + @UseGuards(JwtAuthGuard, OwnerOrAdminGuard) + @ApiBearerAuth() @ApiOperation({ summary: '根据ID查询用户', - description: '根据用户ID获取详细信息', + description: '根据用户ID获取详细信息,需要管理员权限或者是用户本人', }) @ApiParam({ name: 'id', description: '用户ID', type: Number }) @ApiResponse({ @@ -114,6 +116,11 @@ export class UserController { description: '查询成功', type: User, }) + @ApiResponse({ status: 401, description: '未授权' }) + @ApiResponse({ + status: 403, + description: '权限不足,只能查询自己的信息或需要管理员权限', + }) @ApiResponse({ status: 404, description: '用户不存在' }) findOneById(@Param('id') id: string): Promise { return this.userService.findOneById(+id); @@ -123,6 +130,8 @@ export class UserController { * 更新用户信息 */ @Patch(':id') + @UseGuards(JwtAuthGuard, OwnerOrAdminGuard) + @ApiBearerAuth() @ApiOperation({ summary: '更新用户信息', description: '更新用户信息,不允许修改 username、openId、unionId', @@ -147,6 +156,8 @@ export class UserController { */ @Patch(':id/password') @HttpCode(HttpStatus.NO_CONTENT) + @UseGuards(JwtAuthGuard, OwnerOrAdminGuard) + @ApiBearerAuth() @ApiOperation({ summary: '修改密码', description: '修改用户密码,需要先验证旧密码', @@ -167,6 +178,8 @@ export class UserController { */ @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) + @UseGuards(JwtAuthGuard, OwnerOrAdminGuard) + @ApiBearerAuth() @ApiOperation({ summary: '删除用户', description: '软删除用户,将状态更新为 deleted', diff --git a/apps/api/src/modules/user/user.service.ts b/apps/api/src/modules/user/user.service.ts index c8ce85e..43c887f 100644 --- a/apps/api/src/modules/user/user.service.ts +++ b/apps/api/src/modules/user/user.service.ts @@ -91,6 +91,7 @@ export class UserService { ); // 创建用户 + // 注意:所有注册用户的 role 固定为 'user',不允许通过注册接口设置其他角色 const user = this.userRepository.create({ username: createUserDto.username, passwordHash, @@ -101,7 +102,7 @@ export class UserService { openId: createUserDto.openId, unionId: createUserDto.unionId, status: 'active', - role: createUserDto.role || 'user', // 默认为普通用户 + role: 'user', // 固定为普通用户,不允许通过注册接口修改 }); return this.userRepository.save(user); diff --git a/apps/api/test/modules/user/user.service.integration.spec.ts b/apps/api/test/modules/user/user.service.integration.spec.ts index 7da5168..3d3a8d2 100644 --- a/apps/api/test/modules/user/user.service.integration.spec.ts +++ b/apps/api/test/modules/user/user.service.integration.spec.ts @@ -229,7 +229,7 @@ describe('UserService (集成测试)', () => { password: 'password123', email: 'findall3@example.com', nickname: '查询测试用户3', - role: 'admin', + // 注意:注册时不允许传入 role,所有注册用户的 role 固定为 'user' }, ]; @@ -288,7 +288,7 @@ describe('UserService (集成测试)', () => { nickname: '完整信息用户', avatarUrl: 'https://example.com/avatar.jpg', phone: '13800138000', - role: 'admin', + // 注意:注册时不允许传入 role,所有注册用户的 role 固定为 'user' }; await service.create(createUserDto); @@ -307,7 +307,7 @@ describe('UserService (集成测试)', () => { expect(foundUser?.nickname).toBe(createUserDto.nickname); expect(foundUser?.avatarUrl).toBe(createUserDto.avatarUrl); 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?.createdAt).toBeDefined(); expect(foundUser?.updatedAt).toBeDefined(); diff --git a/apps/web/src/components/ProtectedRoute.tsx b/apps/web/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..b3a4b9f --- /dev/null +++ b/apps/web/src/components/ProtectedRoute.tsx @@ -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 ; + } + + // 已登录,渲染子组件 + return <>{children}; +} diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx index bc98a11..198d989 100644 --- a/apps/web/src/layouts/MainLayout.tsx +++ b/apps/web/src/layouts/MainLayout.tsx @@ -1,14 +1,17 @@ import { useState, useEffect, useMemo } from 'react'; 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 { BarChartOutlined, FileTextOutlined, EditOutlined, MenuFoldOutlined, MenuUnfoldOutlined, + LogoutOutlined, + UserOutlined, } from '@ant-design/icons'; import type { MenuProps } from 'antd'; +import { authService } from '../services/auth'; import './MainLayout.css'; const { Header, Sider, Content } = Layout; @@ -29,9 +32,16 @@ const MainLayout = ({ isAdmin = false }: MainLayoutProps) => { const [collapsed, setCollapsed] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [isMobile, setIsMobile] = useState(false); + const [user, setUser] = useState(null); const location = useLocation(); const navigate = useNavigate(); + // 获取用户信息 + useEffect(() => { + const currentUser = authService.getUser(); + setUser(currentUser); + }, []); + // 根据路由获取页面标题 const pageInfo = useMemo(() => { 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: , + label: '个人资料', + disabled: true, // 暂时禁用 + }, + { + type: 'divider', + }, + { + key: 'logout', + icon: , + label: '退出登录', + danger: true, + onClick: handleLogout, + }, + ]; + + // 获取角色显示文本 + const getRoleText = (role: string) => { + const roleMap: Record = { + 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 path = location.pathname; @@ -171,23 +226,32 @@ const MainLayout = ({ isAdmin = false }: MainLayoutProps) => {
-
- - U - -
-
用户名
- 普通用户} - /> + +
+ + {user?.nickname?.[0] || user?.username?.[0] || 'U'} + +
+
+ {user?.nickname || user?.username || '用户'} +
+ + {user ? getRoleText(user.role) : '普通用户'} + + } + /> +
-
+
diff --git a/apps/web/src/pages/LoginPage.css b/apps/web/src/pages/LoginPage.css new file mode 100644 index 0000000..4686756 --- /dev/null +++ b/apps/web/src/pages/LoginPage.css @@ -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); +} diff --git a/apps/web/src/pages/LoginPage.tsx b/apps/web/src/pages/LoginPage.tsx new file mode 100644 index 0000000..e6aae40 --- /dev/null +++ b/apps/web/src/pages/LoginPage.tsx @@ -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 ( +
+
+
+

投小记

+

投资决策与复盘工具

+
+ +
+ + } placeholder="用户名或邮箱" /> + + + + } placeholder="密码" /> + + + + + +
+
+
+ ); +} diff --git a/apps/web/src/router/index.tsx b/apps/web/src/router/index.tsx index f0596c8..aedabec 100644 --- a/apps/web/src/router/index.tsx +++ b/apps/web/src/router/index.tsx @@ -1,13 +1,23 @@ import { createBrowserRouter } from 'react-router'; import MainLayout from '../layouts/MainLayout'; +import ProtectedRoute from '../components/ProtectedRoute'; +import LoginPage from '../pages/LoginPage'; import AssetsPage from '../pages/AssetsPage'; import PlansPage from '../pages/PlansPage'; import ReviewPage from '../pages/ReviewPage'; export const router = createBrowserRouter([ + { + path: '/login', + element: , + }, { path: '/', - element: , + element: ( + + + + ), children: [ { index: true, diff --git a/apps/web/src/services/api.ts b/apps/web/src/services/api.ts index 385476f..1a06f70 100644 --- a/apps/web/src/services/api.ts +++ b/apps/web/src/services/api.ts @@ -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'; +// Token 存储键名(与 auth service 保持一致) +const TOKEN_KEY = 'access_token'; + // 创建 axios 实例 const apiClient: AxiosInstance = axios.create({ baseURL: envConfig.apiBaseUrl, @@ -13,11 +17,11 @@ const apiClient: AxiosInstance = axios.create({ // 请求拦截器 apiClient.interceptors.request.use( (config) => { - // 可以在这里添加 token 等认证信息 - // const token = localStorage.getItem('token') - // if (token) { - // config.headers.Authorization = `Bearer ${token}` - // } + // 自动添加 token 到请求头 + const token = localStorage.getItem(TOKEN_KEY); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } if (envConfig.debug) { console.log('请求:', config.method?.toUpperCase(), config.url); @@ -50,8 +54,13 @@ apiClient.interceptors.response.use( if (error.response) { switch (error.response.status) { case 401: - // 未授权,可以跳转到登录页 - // window.location.href = '/login' + // 未授权,清除 token 并跳转到登录页 + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem('user_info'); + // 避免在登录页重复跳转 + if (window.location.pathname !== '/login') { + window.location.href = '/login'; + } break; case 403: console.error('没有权限访问该资源'); diff --git a/apps/web/src/services/auth.ts b/apps/web/src/services/auth.ts new file mode 100644 index 0000000..bc4f1e2 --- /dev/null +++ b/apps/web/src/services/auth.ts @@ -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 { + const response = await api.post>('/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(); diff --git a/apps/web/src/types/common.ts b/apps/web/src/types/common.ts new file mode 100644 index 0000000..af037c1 --- /dev/null +++ b/apps/web/src/types/common.ts @@ -0,0 +1,9 @@ +/** + * API 响应包装 + */ +export interface ApiResponse { + code: number; + message: string; + data: T; + timestamp?: string; +} diff --git a/apps/web/src/types/user.ts b/apps/web/src/types/user.ts new file mode 100644 index 0000000..8c70ddb --- /dev/null +++ b/apps/web/src/types/user.ts @@ -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; +} diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json index a9b5a59..996e7c4 100644 --- a/apps/web/tsconfig.app.json +++ b/apps/web/tsconfig.app.json @@ -1,28 +1,32 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "types": ["vite/client"], - "skipLibCheck": true, + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["./src/*"] + }, + "typeRoots": ["./node_modules/@types", "./src/types"] + }, + "include": ["src"] }