浏览代码

添加了数据表支持

yourname 6 月之前
父节点
当前提交
f702474ac5

+ 9 - 0
package.json

@@ -15,14 +15,23 @@
     "@hono/vite-dev-server": "^0.19.1",
     "@hono/zod-openapi": "^0.19.7",
     "@hono/zod-validator": "^0.4.3",
+    "@nestjs/typeorm": "^11.0.0",
     "@tanstack/react-query": "^5.77.2",
+    "@types/bcrypt": "^5.0.2",
+    "@types/jsonwebtoken": "^9.0.9",
+    "bcrypt": "^6.0.0",
     "hono": "^4.7.6",
+    "jsonwebtoken": "^9.0.2",
+    "mysql2": "^3.14.1",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
     "react-router-dom": "^7.6.1",
+    "reflect-metadata": "^0.2.2",
+    "typeorm": "^0.3.24",
     "zod": "^3.24.2"
   },
   "devDependencies": {
+    "@types/node": "^22.15.23",
     "@types/react": "^19.1.1",
     "@types/react-dom": "^19.1.2",
     "hono-vite-react-stack-node": "^0.2.1",

+ 2 - 0
src/client/app.tsx

@@ -2,8 +2,10 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'
 import { hc } from 'hono/client'
 import { useQuery } from '@tanstack/react-query'
 import type { AppType } from '../server/api/base'
+import type { UserRoutes } from '../server/api/user'
 
 const client = hc<AppType>('/api')
+const userClient = hc<UserRoutes['createUser']>('/api/user')
 
 const Home = () => {
   return (

+ 31 - 6
src/server/api.ts

@@ -1,22 +1,47 @@
 import { OpenAPIHono } from '@hono/zod-openapi'
+import { cors } from 'hono/cors'
+import { logger } from 'hono/logger'
 import { errorHandler } from './middleware/errorHandler'
+import { authMiddleware } from './middleware/auth.middleware'
+import { checkPermission } from './middleware/permission.middleware'
 import base from './api/base'
+import { userOpenApiApp } from './api/user'
+import { authOpenApiApp } from './api/auth'
 
 const app = new OpenAPIHono()
 
-// Register routes
-app.route('/api', base)
-
-// Error handling
+// Middleware chain
+app.use('*', logger())
+app.use('*', cors(
+  // {
+  //   origin: ['http://localhost:3000'],
+  //   allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
+  //   credentials: true
+  // }
+))
+app.use('/api/v1/*', authMiddleware)
 app.onError(errorHandler)
 
+// Rate limiting
+app.use('/api/v1/*', async (c, next) => {
+  const ip = c.req.header('x-forwarded-for') || c.req.header('cf-connecting-ip')
+  // 实现速率限制逻辑
+  await next()
+})
+
+// Register routes
+app.route('/api/v1', base)
+app.route('/api/v1/users', userOpenApiApp)
+app.route('/api/v1/auth', new AuthController().routes())
+
 // OpenAPI documentation endpoint
 app.doc('/doc', {
   openapi: '3.1.0',
   info: {
     title: 'API Documentation',
     version: '1.0.0'
-  }
-})   
+  },
+  servers: [{ url: '/api/v1' }]
+})
 
 export default app

+ 193 - 0
src/server/api/user.ts

@@ -0,0 +1,193 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { UserService } from '../modules/users/user.service';
+import { z } from 'zod';
+
+const app = new OpenAPIHono()
+const userService = new UserService();
+
+const UserSchema = z.object({
+  id: z.number().openapi({ example: 1 }),
+  username: z.string().openapi({ example: 'john_doe' }),
+  email: z.string().email().openapi({ example: 'john@example.com' }),
+  createdAt: z.string().datetime().openapi({ example: '2025-05-28T00:00:00Z' })
+});
+
+const CreateUserSchema = z.object({
+  username: z.string().min(3).openapi({
+    example: 'john_doe',
+    description: 'Minimum 3 characters'
+  }),
+  password: z.string().min(6).openapi({
+    example: 'password123',
+    description: 'Minimum 6 characters'
+  }),
+  email: z.string().email().openapi({ example: 'john@example.com' })
+});
+
+const UpdateUserSchema = CreateUserSchema.partial();
+
+// 创建用户
+const createUserRoute = createRoute({
+  method: 'post',
+  path: '/users',
+  request: {
+    query: CreateUserSchema
+  },
+  responses: {
+    201: {
+      description: 'Created',
+      content: {
+        'application/json': {
+          schema: UserSchema
+        }
+      }
+    },
+    400: { description: 'Invalid input' }
+  }
+});
+
+export const createUserHandler = app.openapi(createUserRoute, async (c) => {
+  const data = c.req.valid('query');
+  const user = await userService.createUser(data);
+  return c.json(user, 201);
+});
+
+// 获取用户列表
+export const listUsersRoute = createRoute({
+  method: 'get',
+  path: '/users',
+  responses: {
+    200: {
+      description: 'Success',
+      content: {
+        'application/json': {
+          schema: z.array(UserSchema)
+        }
+      }
+    }
+  }
+});
+
+export const listUsersHandler = app.openapi(
+  listUsersRoute,
+  async (c) => {
+    const users = await userService.getUsers();
+    const usersOut = users.map(user => {
+      return {
+        ...user,
+        createdAt: user.createdAt.toISOString(),
+        updatedAt: user.updatedAt?.toISOString()
+      };
+    })
+  
+    return c.json(usersOut);
+  }
+);
+
+// 获取单个用户
+export const getUserRoute = createRoute({
+  method: 'get',
+  path: '/users/{id}',
+  request: {
+    params: z.object({
+      id: z.string().openapi({ example: '1' })
+    })
+  },
+  responses: {
+    200: {
+      description: 'Success',
+      content: {
+        'application/json': {
+          schema: UserSchema
+        }
+      }
+    },
+    404: { description: 'User not found' }
+  }
+});
+
+export const getUserHandler = app.openapi(
+  getUserRoute,
+  async (c) => {
+    const { id } = c.req.valid('param');
+    const user = await userService.getUserById(parseInt(id));
+    if (!user) return c.notFound();
+    return c.json(user);
+  }
+);
+
+// 更新用户
+export const updateUserRoute = createRoute({
+  method: 'patch',
+  path: '/users/{id}',
+  request: {
+    params: z.object({
+      id: z.string().openapi({ example: '1' })
+    }),
+    body: {
+      content: {
+        'application/json': {
+          schema: UpdateUserSchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: 'Success',
+      content: {
+        'application/json': {
+          schema: UserSchema
+        }
+      }
+    },
+    404: { description: 'User not found' }
+  }
+});
+
+export const updateUserHandler = app.openapi(
+  updateUserRoute,
+  async (c) => {
+    const { id } = c.req.valid('param');
+    const data = c.req.valid('json');
+    const user = await userService.updateUser(parseInt(id), data);
+    if (!user) return c.notFound();
+    return c.json(user);
+  }
+);
+
+// 删除用户
+export const deleteUserRoute = createRoute({
+  method: 'delete',
+  path: '/users/{id}',
+  request: {
+    params: z.object({
+      id: z.string().openapi({ example: '1' })
+    })
+  },
+  responses: {
+    204: { description: 'No Content' },
+    404: { description: 'User not found' }
+  }
+});
+
+export const deleteUserHandler = app.openapi(
+  deleteUserRoute,
+  async (c) => {
+    const { id } = c.req.valid('param');
+    await userService.deleteUser(parseInt(id));
+    return c.body(null, 204);
+  }
+);
+
+export const userRoutes = {
+  createUser: createUserHandler,
+  listUsers: listUsersHandler,
+  getUser: getUserHandler,
+  updateUser: updateUserHandler,
+  deleteUser: deleteUserHandler
+};
+
+export type UserRoutes = typeof userRoutes;
+
+export const userOpenApiApp = app;

+ 18 - 0
src/server/data-source.ts

@@ -0,0 +1,18 @@
+import "reflect-metadata"
+import { DataSource } from "typeorm"
+import { User } from "./modules/users/user.entity"
+import { Role } from "./modules/users/role.entity";
+import { CreateUserTables } from "./migrations/001-CreateUserTables";
+
+export  const AppDataSource = new DataSource({
+  type: "mysql",
+  host: "localhost",  // 使用IP地址而非localhost
+  port: 3306,
+  username: "root",
+  password: "", // 请替换为实际密码
+  database: "test",
+  entities: [ User, Role],
+  migrations: [CreateUserTables],
+  synchronize: false,
+  logging: true
+});

+ 33 - 0
src/server/middleware/auth.middleware.ts

@@ -0,0 +1,33 @@
+import { Context, Next } from 'hono';
+import { AuthService } from '../modules/auth/auth.service';
+import { UserService } from '../modules/users/user.service';
+
+export async function authMiddleware(c: Context, next: Next) {
+  try {
+    const authHeader = c.req.header('Authorization');
+    if (!authHeader) {
+      return c.json({ message: 'Authorization header missing' }, 401);
+    }
+
+    const token = authHeader.split(' ')[1];
+    if (!token) {
+      return c.json({ message: 'Token missing' }, 401);
+    }
+
+    const authService = new AuthService();
+    const decoded = authService.verifyToken(token);
+    
+    const userService = new UserService();
+    const user = await userService.getUserById(decoded.id);
+    
+    if (!user) {
+      return c.json({ message: 'User not found' }, 401);
+    }
+
+    c.set('user', user);
+    await next();
+  } catch (error) {
+    console.error('Authentication error:', error);
+    return c.json({ message: 'Invalid token' }, 401);
+  }
+}

+ 39 - 0
src/server/middleware/permission.middleware.ts

@@ -0,0 +1,39 @@
+import { Context, Next } from 'hono';
+import { User } from '../modules/users/user.entity';
+
+type PermissionCheck = (user: User) => boolean | Promise<boolean>;
+
+export function checkPermission(requiredRoles: string[]): PermissionCheck {
+  return (user: User) => {
+    if (!user.roles) return false;
+    return user.roles.some(role => requiredRoles.includes(role.name));
+  };
+}
+
+export function permissionMiddleware(check: PermissionCheck) {
+  return async (c: Context, next: Next) => {
+    try {
+      const user = c.get('user') as User | undefined;
+      if (!user) {
+        return c.json({ message: 'Unauthorized' }, 401);
+      }
+
+      const hasPermission = await check(user);
+      if (!hasPermission) {
+        return c.json({ message: 'Forbidden' }, 403);
+      }
+
+      await next();
+    } catch (error) {
+      console.error('Permission check error:', error);
+      return c.json({ message: 'Internal server error' }, 500);
+    }
+  };
+}
+
+// 示例用法:
+// app.get('/admin', 
+//   authMiddleware,
+//   permissionMiddleware(checkPermission(['admin'])),
+//   (c) => {...}
+// )

+ 47 - 0
src/server/migrations/001-CreateUserTables.ts

@@ -0,0 +1,47 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class CreateUserTables implements MigrationInterface {
+    name = 'CreateUserTables'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`
+            CREATE TABLE role (
+                id int NOT NULL AUTO_INCREMENT,
+                name varchar(255) NOT NULL,
+                description varchar(255) NOT NULL,
+                PRIMARY KEY (id),
+                UNIQUE KEY IDX_ae4578dcaed5adff96595e6166 (name)
+            ) ENGINE=InnoDB
+        `);
+
+        await queryRunner.query(`
+            CREATE TABLE user (
+                id int NOT NULL AUTO_INCREMENT,
+                username varchar(255) NOT NULL,
+                password varchar(255) NOT NULL,
+                email varchar(255) NOT NULL,
+                PRIMARY KEY (id),
+                UNIQUE KEY IDX_78a916df40e02a9deb1c4b75ed (username),
+                UNIQUE KEY IDX_e12875dfb3b1d92d7d7c5377e2 (email)
+            ) ENGINE=InnoDB
+        `);
+
+        await queryRunner.query(`
+            CREATE TABLE user_roles_role (
+                userId int NOT NULL,
+                roleId int NOT NULL,
+                PRIMARY KEY (userId, roleId),
+                KEY IDX_5f9286e6c25594c6b88c108db7 (userId),
+                KEY IDX_4be2f7adf862634f5f803d246b (roleId),
+                CONSTRAINT FK_5f9286e6c25594c6b88c108db77 FOREIGN KEY (userId) REFERENCES user (id) ON DELETE CASCADE ON UPDATE CASCADE,
+                CONSTRAINT FK_4be2f7adf862634f5f803d246b FOREIGN KEY (roleId) REFERENCES role (id) ON DELETE CASCADE ON UPDATE CASCADE
+            ) ENGINE=InnoDB
+        `);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`DROP TABLE user_roles_role`);
+        await queryRunner.query(`DROP TABLE user`);
+        await queryRunner.query(`DROP TABLE role`);
+    }
+}

+ 58 - 0
src/server/modules/auth/auth.service.ts

@@ -0,0 +1,58 @@
+import * as jwt from 'jsonwebtoken';
+import { UserService } from '../users/user.service';
+import { User } from '../users/user.entity';
+
+const JWT_SECRET = 'your-secret-key'; // 生产环境应使用环境变量
+const JWT_EXPIRES_IN = '1h';
+
+export class AuthService {
+  private userService: UserService;
+
+  constructor() {
+    this.userService = new UserService();
+  }
+
+  async login(username: string, password: string): Promise<{ token: string; user: User }> {
+    try {
+      const user = await this.userService.getUserByUsername(username);
+      if (!user) {
+        throw new Error('User not found');
+      }
+
+      const isPasswordValid = await this.userService.verifyPassword(user, password);
+      if (!isPasswordValid) {
+        throw new Error('Invalid password');
+      }
+
+      const token = this.generateToken(user);
+      return { token, user };
+    } catch (error) {
+      console.error('Login error:', error);
+      throw error;
+    }
+  }
+
+  generateToken(user: User): string {
+    const payload = {
+      id: user.id,
+      username: user.username,
+      roles: user.roles?.map(role => role.name) || []
+    };
+    return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
+  }
+
+  verifyToken(token: string): any {
+    try {
+      return jwt.verify(token, JWT_SECRET);
+    } catch (error) {
+      console.error('Token verification failed:', error);
+      throw new Error('Invalid token');
+    }
+  }
+
+  async logout(token: string): Promise<void> {
+    // 实际项目中可能需要将token加入黑名单
+    // 这里简单返回成功
+    return Promise.resolve();
+  }
+}

+ 17 - 0
src/server/modules/users/role.entity.ts

@@ -0,0 +1,17 @@
+import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+
+@Entity()
+export class Role {
+  @PrimaryGeneratedColumn()
+  id!: number;
+
+  @Column({ unique: true })
+  name!: string;
+
+  @Column()
+  description!: string;
+
+  constructor(partial?: Partial<Role>) {
+    Object.assign(this, partial);
+  }
+}

+ 31 - 0
src/server/modules/users/user.entity.ts

@@ -0,0 +1,31 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { Role } from './role.entity';
+
+@Entity()
+export class User {
+  @PrimaryGeneratedColumn()
+  id!: number;
+
+  @Column({ unique: true })
+  username!: string;
+
+  @Column()
+  password!: string;
+
+  @Column({ unique: true })
+  email!: string;
+
+  @ManyToMany(() => Role)
+  @JoinTable()
+  roles!: Role[];
+
+  @CreateDateColumn()
+  createdAt!: Date;
+
+  @UpdateDateColumn()
+  updatedAt!: Date;
+
+  constructor(partial?: Partial<User>) {
+    Object.assign(this, partial);
+  }
+}

+ 111 - 0
src/server/modules/users/user.service.ts

@@ -0,0 +1,111 @@
+import { AppDataSource } from '../../data-source';
+import { User } from './user.entity';
+import * as bcrypt from 'bcrypt';
+import { Repository } from 'typeorm';
+import { Role } from './role.entity';
+
+const SALT_ROUNDS = 10;
+
+export class UserService {
+  private userRepository: Repository<User>;
+  private roleRepository: Repository<Role>;
+
+  constructor() {
+    this.userRepository = AppDataSource.getRepository(User);
+    this.roleRepository = AppDataSource.getRepository(Role);
+  }
+
+  async createUser(userData: Partial<User>): Promise<User> {
+    try {
+      if (userData.password) {
+        userData.password = await bcrypt.hash(userData.password, SALT_ROUNDS);
+      }
+      const user = this.userRepository.create(userData);
+      return await this.userRepository.save(user);
+    } catch (error) {
+      console.error('Error creating user:', error);
+      throw new Error('Failed to create user');
+    }
+  }
+
+  async getUserById(id: number): Promise<User | null> {
+    try {
+      return await this.userRepository.findOne({ 
+        where: { id },
+        relations: ['roles']
+      });
+    } catch (error) {
+      console.error('Error getting user:', error);
+      throw new Error('Failed to get user');
+    }
+  }
+
+  async getUserByUsername(username: string): Promise<User | null> {
+    try {
+      return await this.userRepository.findOne({ 
+        where: { username },
+        relations: ['roles']
+      });
+    } catch (error) {
+      console.error('Error getting user:', error);
+      throw new Error('Failed to get user');
+    }
+  }
+
+  async updateUser(id: number, updateData: Partial<User>): Promise<User | null> {
+    try {
+      if (updateData.password) {
+        updateData.password = await bcrypt.hash(updateData.password, SALT_ROUNDS);
+      }
+      await this.userRepository.update(id, updateData);
+      return this.getUserById(id);
+    } catch (error) {
+      console.error('Error updating user:', error);
+      throw new Error('Failed to update user');
+    }
+  }
+
+  async deleteUser(id: number): Promise<void> {
+    try {
+      await this.userRepository.delete(id);
+    } catch (error) {
+      console.error('Error deleting user:', error);
+      throw new Error('Failed to delete user');
+    }
+  }
+
+  async verifyPassword(user: User, password: string): Promise<boolean> {
+    return bcrypt.compare(password, user.password);
+  }
+
+  async assignRoles(userId: number, roleIds: number[]): Promise<User | null> {
+    try {
+      const user = await this.getUserById(userId);
+      if (!user) return null;
+
+      const roles = await this.roleRepository.findByIds(roleIds);
+      user.roles = roles;
+      return await this.userRepository.save(user);
+    } catch (error) {
+      console.error('Error assigning roles:', error);
+      throw new Error('Failed to assign roles');
+    }
+  }
+
+  async getUsers(): Promise<User[]> {
+    try {
+      const users = await this.userRepository.find({
+        relations: ['roles']
+      });
+      return users;
+    } catch (error) {
+      console.error('Error getting users:', error);
+      throw new Error('Failed to get users');
+    }
+  }
+
+
+  getUserRepository(): Repository<User> {
+    return this.userRepository;
+  }
+}

+ 5 - 1
tsconfig.json

@@ -14,6 +14,10 @@
       "vite/client"
     ],
     "jsx": "react-jsx",
-    "jsxImportSource": "react"
+    "jsxImportSource": "react",
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true
   },
+  "include": ["src"],
+  "exclude": ["node_modules"]
 }