ci: 添加 Docker 构建和推送工作流

- 新增 Dockerfile 和 .dockerignore 文件
- 添加 Gitea 持续集成工作流,用于构建和推送 Docker 镜像
- 新增 .gitignore 文件,忽略构建和配置文件
- 添加项目结构和规范文档,包括 TypeScript、模块化、API、数据库等规范
- 新增前端和后端的基础代码结构
This commit is contained in:
D8D Developer
2025-06-11 09:35:39 +00:00
commit 71aaeb9424
70 changed files with 4170 additions and 0 deletions

70
src/client/app.tsx Normal file
View File

@@ -0,0 +1,70 @@
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { hc } from 'hono/client'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import type { BaseRoutes } from '../server/api/base'
import type { UserRoutes } from '../server/api/user'
import './i18n/config'
import LanguageSwitcher from './components/LanguageSwitcher'
const client = hc<BaseRoutes>('/api')
const userClient = hc<UserRoutes['createUser']>('/api')
const Home = () => {
const { t } = useTranslation()
return (
<div>
<h1>{t('welcome')}</h1>
<LanguageSwitcher />
</div>
)
}
const About = () => {
return (
<div>
<h1>About Page</h1>
<p>This is the about page.</p>
</div>
)
}
const ApiDemo = () => {
const { data, isLoading, error } = useQuery({
queryKey: ['apiData'],
queryFn: async () => {
const res = await client.index.$get({ query: { name: 'Hono' } })
return res.json()
}
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <h2 className="text-2xl">{data?.message}</h2>
}
const router = createBrowserRouter([
{
path: '/',
element: <Home />,
},
{
path: '/about',
element: <About />,
},
{
path: '/api-demo',
element: <ApiDemo />,
},
])
const App = () => {
return (
<>
<RouterProvider router={router} />
</>
)
}
export default App

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
const LanguageSwitcher: React.FC = () => {
const { t, i18n } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
return (
<div className="language-switcher">
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('zh')}></button>
</div>
);
};
export default LanguageSwitcher;

19
src/client/i18n/config.ts Normal file
View File

@@ -0,0 +1,19 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import resources from 'virtual:i18next-loader';
// 初始化i18n配置
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false, // React已经处理XSS防护
},
resources
});
export default i18n;

View File

@@ -0,0 +1,7 @@
{
"translation": {
"welcome": "Welcome",
"language": "Language",
"switch_language": "Switch Language"
}
}

View File

@@ -0,0 +1,7 @@
{
"translation": {
"welcome": "欢迎",
"language": "语言",
"switch_language": "切换语言"
}
}

5
src/client/i18next-loader.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module 'virtual:i18next-loader' {
import { Resource } from 'i18next';
const resources: Resource;
export default resources;
}

15
src/client/index.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './app'
const queryClient = new QueryClient()
const rootElement = document.getElementById('root')
if (rootElement) {
const root = createRoot(rootElement)
root.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)
}

137
src/server/api.ts Normal file
View File

@@ -0,0 +1,137 @@
import { OpenAPIHono } from '@hono/zod-openapi'
import { errorHandler } from './utils/errorHandler'
import base from './api/base'
import usersRouter from './api/users/index'
import { initConfigRouter, initStatusRouter } from './api/init'
import paymentRouter from './api/payment'
import workspacesRouter from './api/workspaces/index'
import workspacesProjectsRouter from './api/workspaces.projects/index'
import workspacesTemplatesRouter from './api/workspaces.templates/index'
import workspacesContainersRouter from './api/workspaces.containers/index'
import workspacesCollaboratorsRouter from './api/workspaces.collaborations/index'
import workspacesProjectCollaboratorsRouter from './api/workspaces.project-collaborators/index'
import workspacesTemplateCollaboratorsRouter from './api/workspaces.template-collaborators/index'
import templateMarketplaceRouter from './api/marketplace/index'
import authRoute from './api/auth/index'
import aliBillsRoute from './api/ali-bills/index'
import { AuthContext } from './types/context'
import { AppDataSource } from './data-source'
const api = new OpenAPIHono<AuthContext>()
api.onError(errorHandler)
// Rate limiting
api.use('/api/v1/*', async (c, next) => {
const ip = c.req.header('x-forwarded-for') || c.req.header('cf-connecting-ip')
// 实现速率限制逻辑
await next()
})
// 数据库初始化中间件
api.use('/api/v1/*', async (c, next) => {
if(!AppDataSource.isInitialized) await AppDataSource.initialize();
await next()
})
// 注册Bearer认证方案
api.openAPIRegistry.registerComponent('securitySchemes','bearerAuth',{
type:'http',
scheme:'bearer',
bearerFormat:'JWT',
description:'使用JWT进行认证'
})
// OpenAPI documentation endpoint
api.doc31('/doc', {
openapi: '3.1.0',
info: {
title: 'API Documentation',
version: '1.0.0'
},
security: [{
bearerAuth: []
}]
// servers: [{ url: '/api/v1' }]
})
// const bindRoute = (api:OpenAPIHono, path: string, routes: Array<OpenAPIHono>) => {
// routes.forEach(route => {
// api = api.route(path, route)
// })
// return api
// }
// Register routes
let routes = api
.route('/api/v1/init', initConfigRouter)
.route('/api/v1/init', initStatusRouter)
.route('/api/v1/base', base)
.route('/api/v1/users', usersRouter.createRoute)
.route('/api/v1/users', usersRouter.listRoute)
.route('/api/v1/users', usersRouter.getRoute)
.route('/api/v1/users', usersRouter.updateRoute)
.route('/api/v1/users', usersRouter.deleteRoute)
.route('/api/v1/auth', authRoute.loginRoute.passwordRoute)
.route('/api/v1/auth', authRoute.loginRoute.smsRoute)
.route('/api/v1/auth', authRoute.logoutRoute)
.route('/api/v1/auth', authRoute.meRoute.meRoute)
.route('/api/v1/auth', authRoute.phoneCode.fixed)
.route('/api/v1/auth', authRoute.phoneCode.sms)
.route('/api/v1/auth', authRoute.registerRoute.registerRoute)
.route('/api/auth', authRoute.ssoVerify)
.route('/api/v1/payments', paymentRouter.notifyApi)
.route('/api/v1/payments', paymentRouter.queryApi)
.route('/api/v1/payments', paymentRouter.paymentApi)
.route('/api/v1/marketplace', templateMarketplaceRouter.listRoute)
.route('/api/v1/marketplace', templateMarketplaceRouter.detailRoute)
.route('/api/v1/marketplace', templateMarketplaceRouter.deleteRoute)
.route('/api/v1/workspaces', workspacesRouter.createRoutes)
.route('/api/v1/workspaces', workspacesRouter.deleteRoutes)
.route('/api/v1/workspaces', workspacesRouter.listRoutes)
.route('/api/v1/workspaces', workspacesRouter.getRoutes)
.route('/api/v1/workspaces', workspacesRouter.updateRoutes)
.route('/api/v1/workspaces', workspacesProjectsRouter.createRoutes)
.route('/api/v1/workspaces', workspacesProjectsRouter.deleteRoutes)
.route('/api/v1/workspaces', workspacesProjectsRouter.listRoutes)
.route('/api/v1/workspaces', workspacesProjectsRouter.getRoutes)
.route('/api/v1/workspaces', workspacesProjectsRouter.updateRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.createRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.deleteRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.listRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.getRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.updateRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.publishRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.useTemplateRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.getBlankTemplateRoute)
.route('/api/v1/workspaces', workspacesTemplatesRouter.listBlankTemplatesRoute)
.route('/api/v1/workspaces', workspacesContainersRouter.startRoutes)
.route('/api/v1/workspaces', workspacesContainersRouter.stopRoutes)
.route('/api/v1/workspaces', workspacesCollaboratorsRouter.repositoriesApp)
.route('/api/v1/workspaces', workspacesProjectCollaboratorsRouter.listRoutes)
.route('/api/v1/workspaces', workspacesProjectCollaboratorsRouter.createRoutes)
.route('/api/v1/workspaces', workspacesProjectCollaboratorsRouter.deleteRoutes)
.route('/api/v1/workspaces', workspacesProjectCollaboratorsRouter.checkRoutes)
.route('/api/v1/workspaces', workspacesTemplateCollaboratorsRouter.listRoutes)
.route('/api/v1/workspaces', workspacesTemplateCollaboratorsRouter.createRoutes)
.route('/api/v1/workspaces', workspacesTemplateCollaboratorsRouter.deleteRoutes)
.route('/api/v1/workspaces', workspacesTemplateCollaboratorsRouter.checkRoutes)
.route('/api/v1/ali-bills', aliBillsRoute.getRoute)
.route('/api/v1/ali-bills', aliBillsRoute.syncRoute)
export type ApiRoutes = typeof routes
export default routes

View File

@@ -0,0 +1,15 @@
import loginRoute from './login';
import logoutRoute from './logout';
import meRoute from './me';
import phoneCode from './phone-code';
import registerRoute from './register';
import ssoVerify from './sso-verify';
export default {
loginRoute,
logoutRoute,
meRoute,
phoneCode,
registerRoute,
ssoVerify
}

View File

@@ -0,0 +1,13 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { AuthContext } from '@/server/types/context';
import passwordRoute from './password';
import smsRoute from './sms';
// const api = new OpenAPIHono<AuthContext>()
// .route('/', passwordRoute)
// .route('/', smsRoute);
// export default api;
export default {
passwordRoute,
smsRoute,
}

View File

@@ -0,0 +1,71 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { AuthService } from '../../../modules/auth/auth.service'
import { UserService } from '../../../modules/users/user.service'
import { z } from 'zod'
import { ErrorSchema } from '../../../utils/errorHandler'
import { AppDataSource } from '../../../data-source'
import { AuthContext } from '../../../types/context'
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const LoginSchema = z.object({
username: z.string().min(3).openapi({
example: 'john_doe',
description: '用户名'
}),
password: z.string().min(6).openapi({
example: 'password123',
description: '密码'
})
})
const TokenResponseSchema = z.object({
token: z.string().openapi({
example: 'jwt.token.here',
description: 'JWT Token'
}),
user: z.object({
id: z.number(),
username: z.string()
})
})
const loginRoute = createRoute({
method: 'post',
path: '/login',
request: {
body: {
content: {
'application/json': {
schema: LoginSchema
}
}
}
},
responses: {
200: {
description: '登录成功',
content: {
'application/json': {
schema: TokenResponseSchema
}
}
},
401: {
description: '用户名或密码错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono<AuthContext>().openapi(loginRoute, async (c) => {
const { username, password } = c.req.valid('json')
const result = await authService.login(username, password)
return c.json(result, 200)
});
export default app

View File

@@ -0,0 +1,144 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { setCookie } from 'hono/cookie'
import { AuthService } from '../../../modules/auth/auth.service'
import { UserService } from '../../../modules/users/user.service'
import { z } from 'zod'
import { HTTPException } from 'hono/http-exception'
import { ErrorSchema } from '../../../utils/errorHandler'
import { AppDataSource } from '../../../data-source'
import { AuthContext } from '../../../types/context'
import { UserResponseSchema } from '../schemas'
import debug from 'debug'
import { GitUtils } from '@/server/utils/gitUtils'
import process from 'node:process'
const log = {
auth: debug('auth')
}
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const SmsLoginSchema = z.object({
phone: z.string().regex(/^1[3-9]\d{9}$/).openapi({
example: '13800138000',
description: '手机号'
}),
code: z.string().length(6).openapi({
example: '123456',
description: '6位验证码'
})
})
const TokenResponseSchema = z.object({
token: z.string().openapi({
example: 'jwt.token.here',
description: 'JWT Token'
}),
user: UserResponseSchema
})
const smsLoginRoute = createRoute({
method: 'post',
path: '/login/sms',
request: {
body: {
content: {
'application/json': {
schema: SmsLoginSchema
}
}
}
},
responses: {
200: {
description: '登录成功',
content: {
'application/json': {
schema: TokenResponseSchema
}
}
},
400: {
description: '验证码错误或已过期',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono<AuthContext>().openapi(smsLoginRoute, async (c) => {
const { phone, code } = c.req.valid('json')
// 验证验证码
const isValid = authService.verifyCode(phone, code)
if (!isValid) {
throw new HTTPException(400, { message: '验证码错误或已过期' })
}
// 查找或创建用户
let user = await userService.getUserByPhone(phone)
if (!user) {
user = await userService.createUser({ mobile: phone, username: phone })
}
// 同步检查 Gogs 用户
try {
// 格式化用户名
const gogsUsername = GitUtils.formatUsername(phone, user.id);
// 随机生成密码
const randomPassword = Math.random().toString(36).slice(-8);
// 检查并创建 Gogs 用户
const gogsResult = await GitUtils.checkGogsUser(
gogsUsername,
user.email || `${gogsUsername}@d8d.fun`,
randomPassword,
phone
);
if (gogsResult.success) {
log.auth(gogsResult.message);
} else {
log.auth(`Gogs 用户同步失败: ${gogsResult.message}`);
}
} catch (syncError) {
// Gogs 同步失败不应该影响登录流程
log.auth('同步 Gogs 用户失败:', syncError);
}
const token = authService.generateToken(user)
const cookieName = process.env.SSO_COOKIE_NAME || 'd8d_aider_auth_token';
const cookieDomain = process.env.SSO_COOKIE_DOMAIN || '.d.d8d.fun';
// 设置cookie
setCookie(c, cookieName, token, {
httpOnly: true,
secure: true,
sameSite: 'none',
domain: cookieDomain,
maxAge: 86400 * 7
});
const response = {
token,
user: {
id: user.id,
username: user.username,
mobile: user.mobile,
status: user.status,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString()
}
}
return c.json(response, 200)
})
export default app

View File

@@ -0,0 +1,68 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { z } from 'zod'
import { AuthContext } from '@/server/types/context';
import { authMiddleware } from '@/server/middleware/auth.middleware';
import { AppDataSource } from '@/server/data-source';
import { AuthService } from '@/server/modules/auth/auth.service';
import { UserService } from '@/server/modules/users/user.service';
import { ErrorSchema } from '@/server/utils/errorHandler';
// 初始化服务
const userService = new UserService(AppDataSource);
const authService = new AuthService(userService);
const SuccessSchema = z.object({
message: z.string().openapi({ example: '登出成功' })
})
// 定义路由
const routeDef = createRoute({
method: 'post',
path: '/logout',
security: [{ Bearer: [] }],
middleware: [authMiddleware],
responses: {
200: {
description: '登出成功',
content: {
'application/json': {
schema: SuccessSchema
}
}
},
401: {
description: '未授权',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
try {
const token = c.get('token');
const decoded = authService.verifyToken(token);
if (!decoded) {
return c.json({ code: 401, message: '未授权' }, 401);
}
await authService.logout(token);
return c.json({ message: '登出成功' }, 200);
} catch (error) {
console.error('登出失败:', error);
return c.json({ code: 500, message: '登出失败' }, 500);
}
});
export default app;

View File

@@ -0,0 +1,46 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { z } from 'zod'
import { ErrorSchema } from '@/server/utils/errorHandler'
import { authMiddleware } from '@/server/middleware/auth.middleware'
import { AuthContext } from '@/server/types/context'
import { UserResponseSchema } from '../schemas'
const routeDef = createRoute({
method: 'get',
path: '/me',
middleware: authMiddleware,
responses: {
200: {
description: '获取当前用户信息成功',
content: {
'application/json': {
schema: UserResponseSchema
}
}
},
401: {
description: '未授权',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono<AuthContext>().openapi(routeDef, (c) => {
const user = c.get('user')
return c.json({
id: user.id,
username: user.username,
mobile: user.mobile,
status: user.status,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString()
}, 200)
})
export default app

View File

@@ -0,0 +1,10 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { AuthContext } from '@/server/types/context';
import meRoute from './get';
// const api = new OpenAPIHono<AuthContext>()
// .route('/', meRoute)
// export default api;
export default {
meRoute,
}

View File

@@ -0,0 +1,58 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { AuthService } from '@/server/modules/auth/auth.service'
import { UserService } from '@/server/modules/users/user.service'
import { z } from 'zod'
import { ErrorSchema } from '@/server/utils/errorHandler'
import { AppDataSource } from '@/server/data-source'
import { AuthContext } from '@/server/types/context'
const GenerateFixedCodeSchema = z.object({
phone: z.string().regex(/^1[3-9]\d{9}$/).openapi({
example: '13800138000',
description: '手机号'
})
})
const FixedCodeResponseSchema = z.object({
code: z.string().length(6).openapi({
example: '123456',
description: '6位固定验证码'
})
})
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const generateFixedCodeRoute = createRoute({
method: 'get',
path: '/phone-code/fixed/{phone}',
request: {
params: GenerateFixedCodeSchema
},
responses: {
200: {
description: '生成成功',
content: {
'application/json': {
schema: FixedCodeResponseSchema
}
}
},
403: {
description: '生产环境禁止访问',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono<AuthContext>().openapi(generateFixedCodeRoute, async (c) => {
const { phone } = c.req.valid('param')
const code = authService.generateFixedCode(phone)
return c.json({ code }, 200)
})
export default app

View File

@@ -0,0 +1,8 @@
import fixed from './fixed';
import sms from './sms';
export default {
fixed,
sms
}

View File

@@ -0,0 +1,53 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { AuthService } from '@/server/modules/auth/auth.service'
import { UserService } from '@/server/modules/users/user.service'
import { z } from 'zod'
import { ErrorSchema } from '@/server/utils/errorHandler'
import { AppDataSource } from '@/server/data-source'
import { AuthContext } from '@/server/types/context'
import { SMS } from '@/server/utils/sms'
const GenerateSMSRndCodeSchema = z.object({
phone: z.string().regex(/^1[3-9]\d{9}$/).openapi({
example: '13800138000',
description: '手机号'
})
})
const SMSRndCodeResponseSchema = z.object({
success: z.boolean().openapi({
example: true,
description: '是否成功'
})
})
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const generateFixedCodeRoute = createRoute({
method: 'get',
path: '/phone-code/sms/{phone}',
request: {
params: GenerateSMSRndCodeSchema
},
responses: {
200: {
description: '发送成功',
content: {
'application/json': {
schema: SMSRndCodeResponseSchema
}
}
},
}
})
const app = new OpenAPIHono<AuthContext>().openapi(generateFixedCodeRoute, async (c) => {
const { phone } = c.req.valid('param')
const code = await authService.generateRandCode(phone)
// TODO: 发送短信
const result = await SMS.sendVerificationSMS(phone, code)
return c.json({ success: result }, 200)
})
export default app

View File

@@ -0,0 +1,76 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { AuthService } from '../../../modules/auth/auth.service'
import { UserService } from '../../../modules/users/user.service'
import { z } from 'zod'
import { AppDataSource } from '../../../data-source'
import { ErrorSchema } from '../../../utils/errorHandler'
import { AuthContext } from '../../../types/context'
const RegisterSchema = z.object({
username: z.string().min(3).openapi({
example: 'john_doe',
description: '用户名'
}),
password: z.string().min(6).openapi({
example: 'password123',
description: '密码'
}),
email: z.string().email().openapi({
example: 'john@example.com',
description: '邮箱'
})
})
const TokenResponseSchema = z.object({
token: z.string().openapi({
example: 'jwt.token.here',
description: 'JWT Token'
}),
user: z.object({
id: z.number(),
username: z.string()
})
})
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const registerRoute = createRoute({
method: 'post',
path: '/register',
request: {
body: {
content: {
'application/json': {
schema: RegisterSchema
}
}
}
},
responses: {
201: {
description: '注册成功',
content: {
'application/json': {
schema: TokenResponseSchema
}
}
},
400: {
description: '用户名已存在',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono<AuthContext>().openapi(registerRoute, async (c) => {
const { username, password, email } = c.req.valid('json')
const user = await userService.createUser({ username, password, email })
const token = authService.generateToken(user)
return c.json({ token, user }, 201)
})
export default app

View File

@@ -0,0 +1,10 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { AuthContext } from '@/server/types/context';
import registerRoute from './create';
// const api = new OpenAPIHono<AuthContext>()
// .route('/', registerRoute)
// export default api;
export default {
registerRoute,
}

View File

@@ -0,0 +1,10 @@
import { z } from 'zod'
export const UserResponseSchema = z.object({
id: z.number(),
username: z.string(),
mobile: z.string(),
status: z.number(),
createdAt: z.string(),
updatedAt: z.string()
}).openapi('User')

View File

@@ -0,0 +1,69 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { AuthService } from '@/server/modules/auth/auth.service'
import { UserService } from '@/server/modules/users/user.service'
import { ErrorSchema } from '@/server/utils/errorHandler'
import { AppDataSource } from '@/server/data-source'
import { AuthContext } from '@/server/types/context'
import { GitUtils } from '@/server/utils/gitUtils'
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const routeDef = createRoute({
method: 'get',
path: '/sso-verify',
responses: {
200: {
description: 'SSO验证成功',
headers: {
'X-Username': {
schema: { type: 'string' },
description: '格式化后的用户名'
}
}
},
401: {
description: '未授权或令牌无效',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono().openapi(routeDef, async (c) => {
try {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ code: 401, message: '未提供授权令牌' }, 401)
}
try {
const userData = await authService.verifyToken(token)
if (!userData) {
return c.json({ code: 401, message: '无效令牌' }, 401)
}
const username = GitUtils.formatUsername(userData.username, userData.id)
c.header('X-Username', username)
return c.text('OK', 200)
} catch (tokenError) {
return c.json({ code: 401, message: '令牌验证失败' }, 401)
}
} catch (error) {
return c.json({ code: 500, message: 'SSO验证失败' }, 500)
}
})
export default app

54
src/server/api/base.ts Normal file
View File

@@ -0,0 +1,54 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { z } from 'zod'
import { ErrorSchema } from '../utils/errorHandler'
const app = new OpenAPIHono()
const QuerySchema = z.object({
name: z.string().optional().openapi({
param: {
name: 'name',
in: 'query'
},
example: 'John'
})
})
const ResponseSchema = z.object({
message: z.string().openapi({
example: 'Hello from API, John'
})
})
const route = createRoute({
method: 'get',
path: '/',
request: {
query: QuerySchema
},
responses: {
200: {
content: {
'application/json': {
schema: ResponseSchema
}
},
description: 'Successful response'
},
400: {
content: {
'application/json': {
schema: ErrorSchema,
},
},
description: 'Invalid request'
}
}
})
const baseRoutes = app.openapi(route, (c) => {
const { name } = c.req.valid('query')
return c.json({ message: `Hello from API${name ? `, ${name}` : ''}` }, 200)
})
export default baseRoutes

192
src/server/api/init.ts Normal file
View File

@@ -0,0 +1,192 @@
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
import { ErrorSchema } from '../utils/errorHandler';
import { AppDataSource } from '../data-source';
import { UserEntity as User } from '../modules/users/user.entity';
import { Role } from '../modules/users/role.entity';
import * as bcrypt from 'bcrypt';
import { generateJwtSecret } from '../utils/env-init';
import { writeFileSync } from 'fs';
const initRouter = new OpenAPIHono();
// Zod 验证模式
const DatabaseConfigSchema = z.object({
host: z.string(),
port: z.number(),
username: z.string(),
password: z.string(),
database: z.string()
});
const AdminUserSchema = z.object({
username: z.string().min(3),
password: z.string().min(8)
});
const InitConfigSchema = z.object({
dbConfig: DatabaseConfigSchema,
adminUser: AdminUserSchema
});
// 检查初始化状态路由
const statusRoute = createRoute({
method: 'get',
path: '/status',
responses: {
200: {
content: {
'application/json': {
schema: z.object({
initialized: z.boolean()
})
}
},
description: '返回系统初始化状态'
},
500: {
description: '初始化状态检查失败',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const initStatusRouter = initRouter.openapi(statusRoute, async (c) => {
try {
const isInitialized = await checkInitialization();
return c.json({ initialized: isInitialized }, 200);
} catch (error) {
return c.json({
code: 500,
message: '初始化状态检查失败'
}, 500);
}
});
// 提交配置路由
const configRoute = createRoute({
method: 'post',
path: '/config',
request: {
body: {
content: {
'application/json': {
schema: InitConfigSchema
}
}
}
},
responses: {
200: {
description: '系统初始化成功',
content: {
'application/json': {
schema: z.object({
success: z.boolean(),
jwtSecret: z.string().optional()
})
}
}
},
400: {
description: '初始化失败',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器内部错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const initConfigRouter = initRouter.openapi(configRoute, async (c) => {
const { dbConfig, adminUser } = c.req.valid('json');
if (await checkInitialization()) {
return c.json({
code: 400,
message: '系统已初始化'
}, 400);
}
try {
await validateDatabase(dbConfig);
await generateEnvFile(dbConfig);
const jwtSecret = generateJwtSecret();
await createAdminUser(adminUser);
return c.json({
success: true,
jwtSecret
}, 200);
} catch (error) {
return c.json({
code: 400,
message: error instanceof Error ? error.message : '未知错误'
}, 400);
}
});
export { initStatusRouter, initConfigRouter };
async function checkInitialization(): Promise<boolean> {
// 检查数据库是否已初始化
return AppDataSource.isInitialized;
}
async function validateDatabase(config: any) {
// 使用TypeORM验证数据库连接
const testDataSource = AppDataSource.setOptions(config);
await testDataSource.initialize();
await testDataSource.destroy();
}
async function generateEnvFile(config: any) {
const envContent = `DB_HOST=${config.host}
DB_PORT=${config.port}
DB_USERNAME=${config.username}
DB_PASSWORD=${config.password}
DB_DATABASE=${config.database}
`;
writeFileSync('.env', envContent);
}
async function createAdminUser(userData: any) {
const userRepo = AppDataSource.getRepository(User);
const roleRepo = AppDataSource.getRepository(Role);
// 检查是否已存在管理员
const existingAdmin = await userRepo.findOne({ where: { username: userData.username } });
if (existingAdmin) {
throw new Error('管理员用户已存在');
}
// 创建管理员角色
let adminRole = await roleRepo.findOne({ where: { name: 'admin' } });
if (!adminRole) {
adminRole = roleRepo.create({ name: 'admin', permissions: ['*'] });
await roleRepo.save(adminRole);
}
// 创建管理员用户
const hashedPassword = await bcrypt.hash(userData.password, 10);
const adminUser = userRepo.create({
username: userData.username,
password: hashedPassword,
roles: [adminRole]
});
await userRepo.save(adminUser);
}

View File

@@ -0,0 +1,89 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { z } from 'zod'
import { AppDataSource } from '../data-source'
import { HTTPException } from 'hono/http-exception';
import { authMiddleware } from '../middleware/auth.middleware';
import { AuthContext } from '../types/context';
interface Migration {
name: string;
timestamp: number;
}
const app = new OpenAPIHono<AuthContext>()
const MigrationResponseSchema = z.object({
success: z.boolean(),
message: z.string(),
migrations: z.array(z.string()).optional()
})
const runMigrationRoute = createRoute({
method: 'post',
path: '/migrations/run',
middleware: authMiddleware,
responses: {
200: {
content: {
'application/json': {
schema: MigrationResponseSchema
}
},
description: 'Migrations executed successfully'
},
500: {
description: 'Migration failed'
}
}
})
const revertMigrationRoute = createRoute({
method: 'post',
path: '/migrations/revert',
middleware: authMiddleware,
responses: {
200: {
content: {
'application/json': {
schema: MigrationResponseSchema
}
},
description: 'Migration reverted successfully'
},
500: {
description: 'Revert failed'
}
}
})
app.openapi(runMigrationRoute, async (c) => {
try {
const migrations = await AppDataSource.runMigrations()
return c.json({
success: true,
message: 'Migrations executed successfully',
migrations: migrations.map((m: Migration) => m.name)
})
} catch (error) {
throw error
}
})
app.openapi(revertMigrationRoute, async (c) => {
try {
await AppDataSource.undoLastMigration()
const migrations = await AppDataSource.showMigrations()
return c.json({
success: true,
message: 'Migration reverted successfully',
migrations: migrations
})
} catch (error) {
throw error
}
})
export default app
export type AppType = typeof app

202
src/server/api/payment.ts Normal file
View File

@@ -0,0 +1,202 @@
import { OpenAPIHono, createRoute} from '@hono/zod-openapi';
import { authMiddleware } from '../middleware/auth.middleware';
import {
PaymentRequestSchema,
PaymentResponseSchema,
NotifyRequestSchema,
QueryRequestSchema,
QueryResponseSchema
} from '../modules/payment/dto/payment.dto';
import { PaymentService } from '../modules/payment/payment.service';
import { ErrorSchema } from '../utils/errorHandler';
import { AppDataSource } from '../data-source';
import { AuthContext } from '../types/context';
const paymentRoute = createRoute({
method: 'post',
path: '/',
middleware: authMiddleware,
request: {
body: {
content: {
'application/json': {
schema: PaymentRequestSchema
}
}
}
},
responses: {
200: {
description: '支付请求成功',
content: {
'application/json': {
schema: PaymentResponseSchema
}
}
},
400: {
description: '客户端错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const notifyRoute = createRoute({
method: 'get',
path: '/zpay/notify',
request: {
query: NotifyRequestSchema
},
responses: {
200: {
description: '回调处理成功',
content: {
'application/json': {
schema: PaymentResponseSchema
}
}
},
400: {
description: '客户端错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const queryRoute = createRoute({
method: 'get',
path: '/query',
middleware: authMiddleware,
request: {
query: QueryRequestSchema
},
responses: {
200: {
description: '查询成功',
content: {
'application/json': {
schema: QueryResponseSchema
}
}
},
400: {
description: '客户端错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>()
const paymentApi = app.openapi(paymentRoute, async (c) => {
const payload = c.req.valid('json');
try {
const paymentService = new PaymentService(AppDataSource);
const result = await paymentService.createPayment(payload);
if(result.code !== 1) {
return c.json({
code: 400,
message: result.msg || '支付请求失败'
}, 400);
}
return c.json({
code: 200,
msg: '支付请求成功',
trade_no: result.trade_no,
payurl: result.payurl,
qrcode: result.qrcode,
img: result.img
}, 200);
} catch (error) {
console.error('Payment error:', error);
return c.json({
code: 500,
message: '支付处理失败'
}, 500);
}
});
const notifyApi = app.openapi(notifyRoute, async (c) => {
const payload = c.req.valid('query');
try {
const paymentService = new PaymentService(AppDataSource);
const result = await paymentService.handleNotify(payload);
return c.json({
code: 200,
msg: result.msg,
trade_no: payload.trade_no,
out_trade_no: payload.out_trade_no
}, 200);
} catch (error) {
console.error('Notify error:', error);
return c.json({
code: 500,
message: '回调处理失败'
}, 500);
}
});
const queryApi = app.openapi(queryRoute, async (c) => {
const payload = c.req.valid('query');
try {
const paymentService = new PaymentService(AppDataSource);
const result = await paymentService.queryPayment(payload);
return c.json({
code: 200,
msg: result.msg,
data: result.data
}, 200);
} catch (error) {
console.error('Query error:', error);
return c.json({
code: 500,
message: '查询处理失败'
}, 500);
}
});
export default {
paymentApi,
notifyApi,
queryApi
};

View File

@@ -0,0 +1,80 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { UserService } from '../../modules/users/user.service';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.middleware';
import { ErrorSchema } from '../../utils/errorHandler';
import { AppDataSource } from '../../data-source';
import { AuthContext } from '../../types/context';
const userService = new UserService(AppDataSource);
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 createUserRoute = createRoute({
method: 'post',
path: '/users',
middleware: authMiddleware,
request: {
body: {
content: {
'application/json': {
schema: CreateUserSchema
}
}
}
},
responses: {
201: {
description: '创建成功',
content: {
'application/json': {
schema: UserSchema
}
}
},
400: {
description: '输入数据无效',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(createUserRoute, async (c) => {
try {
const data = c.req.valid('json');
const user = await userService.createUser(data);
return c.json(user, 201);
} catch (error) {
return c.json({ code: 500, message: '服务器错误' }, 500);
}
});
export default app;

View File

@@ -0,0 +1,61 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { UserService } from '../../modules/users/user.service';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.middleware';
import { ErrorSchema } from '../../utils/errorHandler';
import { AppDataSource } from '../../data-source';
import { AuthContext } from '../../types/context';
const userService = new UserService(AppDataSource);
const DeleteParams = z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '1',
description: '用户ID'
})
});
const deleteRoute = createRoute({
method: 'delete',
path: '/{id}',
middleware: authMiddleware,
request: {
params: DeleteParams
},
responses: {
204: {
description: '用户删除成功'
},
404: {
description: '用户不存在',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(deleteRoute, async (c) => {
try {
const { id } = c.req.valid('param');
await userService.deleteUser(parseInt(id));
return c.body(null, 204);
} catch (error) {
return c.json({
code: 500,
message: '删除用户失败'
}, 500);
}
});
export default app;

View File

@@ -0,0 +1,77 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { UserService } from '../../modules/users/user.service';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.middleware';
import { ErrorSchema } from '../../utils/errorHandler';
import { AppDataSource } from '../../data-source';
import { AuthContext } from '../../types/context';
const userService = new UserService(AppDataSource);
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 GetUserRoute = createRoute({
method: 'get',
path: '/{id}',
middleware: authMiddleware,
request: {
params: z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '1',
description: '用户ID'
})
})
},
responses: {
200: {
description: '获取用户成功',
content: {
'application/json': {
schema: UserSchema
}
}
},
404: {
description: '用户不存在',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(GetUserRoute, async (c) => {
try {
const { id } = c.req.valid('param');
const user = await userService.getUserById(parseInt(id));
if (!user) {
return c.json({ code: 404, message: '用户不存在' }, 404);
}
return c.json({
id: user.id,
username: user.username,
email: user.email,
createdAt: user.createdAt.toISOString()
}, 200);
} catch (error) {
return c.json({ code: 500, message: '服务器错误' }, 500);
}
});
export default app;

View File

@@ -0,0 +1,23 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { AuthContext } from '@/server/types/context';
import createRoute from './create';
import listRoute from './list';
import getRoute from './get';
import updateRoute from './update';
import deleteRoute from './delete';
// const api = new OpenAPIHono()
// .route('/', createRoute)
// .route('/', listRoute)
// .route('/', getRoute)
// .route('/', updateRoute)
// .route('/', deleteRoute);
// export default api;
export default {
createRoute,
listRoute,
getRoute,
updateRoute,
deleteRoute
}

View File

@@ -0,0 +1,57 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { UserService } from '../../modules/users/user.service';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.middleware';
import { ErrorSchema } from '../../utils/errorHandler';
import { AppDataSource } from '../../data-source';
import { AuthContext } from '../../types/context';
const userService = new UserService(AppDataSource);
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 listUsersRoute = createRoute({
method: 'get',
path: '/',
middleware: authMiddleware,
responses: {
200: {
description: '成功获取用户列表',
content: {
'application/json': {
schema: z.array(UserSchema)
}
}
},
500: {
description: '获取用户列表失败',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(listUsersRoute, async (c) => {
try {
const users = await userService.getUsers();
const usersOut = users.map(user => ({
id: user.id,
username: user.username,
email: user.email,
createdAt: user.createdAt.toISOString()
}));
return c.json(usersOut, 200);
} catch (error) {
return c.json({ code: 500, message: '获取用户列表失败' }, 500);
}
});
export default app;

View File

@@ -0,0 +1,113 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { UserService } from '../../modules/users/user.service';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.middleware';
import { ErrorSchema } from '../../utils/errorHandler';
import { AppDataSource } from '../../data-source';
import { AuthContext } from '../../types/context';
const userService = new UserService(AppDataSource);
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 UpdateUserSchema = z.object({
username: z.string().min(3).openapi({
example: 'john_doe',
description: 'Minimum 3 characters'
}).optional(),
password: z.string().min(6).openapi({
example: 'password123',
description: 'Minimum 6 characters'
}).optional(),
email: z.string().email().openapi({
example: 'john@example.com',
description: 'Valid email address'
}).optional()
});
const UpdateParams = z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '1',
description: '用户ID'
})
});
const updateRoute = createRoute({
method: 'patch',
path: '/{id}',
middleware: authMiddleware,
request: {
params: UpdateParams,
body: {
content: {
'application/json': {
schema: UpdateUserSchema
}
}
}
},
responses: {
200: {
description: '用户更新成功',
content: {
'application/json': {
schema: UserSchema
}
}
},
400: {
description: '无效输入',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
404: {
description: '用户不存在',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(updateRoute, async (c) => {
try {
const { id } = c.req.valid('param');
const data = c.req.valid('json');
const user = await userService.updateUser(parseInt(id), data);
if (!user) {
return c.json({ code: 404, message: '用户不存在' }, 404);
}
return c.json({
id: user.id,
username: user.username,
email: user.email,
createdAt: user.createdAt.toISOString()
}, 200);
} catch (error) {
return c.json({
code: 500,
message: error instanceof Error ? error.message : '更新用户失败'
}, 500);
}
});
export default app;

27
src/server/data-source.ts Normal file
View File

@@ -0,0 +1,27 @@
import "reflect-metadata"
import { DataSource } from "typeorm"
import { UserEntity as User } from "./modules/users/user.entity"
import { Role } from "./modules/users/role.entity";
import { checkRequiredEnvVars } from "./utils/env-init";
import { PaymentEntity } from "./modules/payment/payment.entity";
import process from 'node:process'
if (!checkRequiredEnvVars()) {
throw new Error("缺少必要的数据库环境变量配置,请检查.env文件");
}
// postgres
export const AppDataSource = new DataSource({
type: "postgres",
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
username: process.env.DB_USERNAME || "postgres",
password: process.env.DB_PASSWORD || "",
database: process.env.DB_DATABASE || "postgres",
entities: [
User, Role, PaymentEntity
],
migrations: [],
synchronize: process.env.DB_SYNCHRONIZE === "true",
logging: true
})

75
src/server/index.tsx Normal file
View File

@@ -0,0 +1,75 @@
import 'dotenv/config'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { swaggerUI } from '@hono/swagger-ui'
import * as fs from 'fs/promises'
import { renderer } from './renderer'
import createApi from './api'
import HomePage from './pages/home'
const app = new Hono();
// Middleware chain
app.use('*', logger())
app.use('*', cors(
// {
// origin: ['http://localhost:3000'],
// allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
// credentials: true
// }
))
app.use(renderer)
app.route('/', createApi)
app.get('/ui', swaggerUI({
url: '/doc',
persistAuthorization: true
}))
app.get('/assets/:filename', async (c) => {
const filename = c.req.param('filename')
const filePath = import.meta.env.PROD? `./dist/assets/${filename}` : `./public/assets/${filename}`
const content = await fs.readFile(filePath);
const modifyDate = (await fs.stat(filePath))?.mtime?.toUTCString()?? new Date().toUTCString();
const fileExt = filePath.split('.').pop()?.toLowerCase()
// 根据文件扩展名设置适当的 Content-Type
if (fileExt === 'tsx' || fileExt === 'ts') {
c.header('Content-Type', 'text/typescript; charset=utf-8')
} else if (fileExt === 'js' || fileExt === 'mjs') {
c.header('Content-Type', 'application/javascript; charset=utf-8')
} else if (fileExt === 'json') {
c.header('Content-Type', 'application/json; charset=utf-8')
} else if (fileExt === 'html') {
c.header('Content-Type', 'text/html; charset=utf-8')
} else if (fileExt === 'css') {
c.header('Content-Type', 'text/css; charset=utf-8')
} else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExt || '')) {
c.header('Content-Type', `image/${fileExt}`)
}
return c.body(content, {
headers: {
// 'Content-Type': 'text/html; charset=utf-8',
'Last-Modified': modifyDate
}
})
})
app.get('/*', (c) => {
return c.render(
<>
<div id="root"></div>
</>
)
})
export default app

View File

@@ -0,0 +1,36 @@
import { Context, Next } from 'hono';
import { AuthService } from '../modules/auth/auth.service';
import { UserService } from '../modules/users/user.service';
import { AppDataSource } from '../data-source';
import { AuthContext } from '../types/context';
export async function authMiddleware(c: Context<AuthContext>, 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 userService = new UserService(AppDataSource);
const authService = new AuthService(userService);
const decoded = authService.verifyToken(token);
const user = await userService.getUserById(decoded.id);
if (!user) {
return c.json({ message: 'User not found' }, 401);
}
c.set('user', user);
c.set('token', token);
await next();
} catch (error) {
console.error('Authentication error:', error);
return c.json({ message: 'Invalid token' }, 401);
}
}

View File

@@ -0,0 +1,39 @@
import { Context, Next } from 'hono';
import { UserEntity as 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) => {...}
// )

View File

@@ -0,0 +1,119 @@
import jwt from 'jsonwebtoken';
import { UserService } from '../users/user.service';
import { UserEntity as User } from '../users/user.entity';
import { redisService } from '@/server/utils/redis';
const JWT_SECRET = 'your-secret-key'; // 生产环境应使用环境变量
const JWT_EXPIRES_IN = '7d'; // 7天有效期
const CODE_EXPIRATION = 5 * 60; // 5分钟有效期(秒)
const CODE_KEY_PREFIX = 'auth:code:';
export class AuthService {
private userService: UserService;
constructor(userService: UserService) {
this.userService = 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');
}
}
generateFixedCode(phone: string): string {
// 基于手机号生成固定6位验证码
const phoneHash = Array.from(phone).reduce((hash, char) =>
((hash << 5) - hash + char.charCodeAt(0)) | 0, 0);
const code = Math.abs(phoneHash % 900000 + 100000).toString();
// 位数不足时补0
return code.padEnd(6, '0');
}
async generateRandCode(phone: string): Promise<string> {
try {
const code = Math.floor(100000 + Math.random() * 900000).toString();
const key = `${CODE_KEY_PREFIX}${phone}`;
await redisService.set(key, code, CODE_EXPIRATION);
return code;
} catch (error) {
console.error('Redis set code error:', error);
throw new Error('验证码生成失败');
}
}
async verifyCode(phone: string, code: string): Promise<boolean> {
if (this.generateFixedCode(phone) === code) {
return true;
}
try {
const key = `${CODE_KEY_PREFIX}${phone}`;
const storedCode = await redisService.get(key);
if (!storedCode) {
return false;
}
const isValid = storedCode === code;
if (isValid) {
await redisService.del(key);
}
return isValid;
} catch (error) {
console.error('Redis verify code error:', error);
return false;
}
}
async logout(token: string): Promise<void> {
try {
// 验证token有效性
const decoded = this.verifyToken(token);
if (!decoded) {
throw new Error('Invalid token');
}
// 实际项目中这里可以添加token黑名单逻辑
// 或者调用Redis等缓存服务使token失效
return Promise.resolve();
} catch (error) {
console.error('Logout failed:', error);
throw error;
}
}
}

View File

@@ -0,0 +1,233 @@
import { z } from 'zod';
import { PaymentStatus } from '../payment.entity';
export const PaymentRequestSchema = z.object({
type: z.enum(['alipay', 'wxpay']).openapi({
description: '支付类型',
example: 'alipay'
}),
out_trade_no: z.string().min(1).openapi({
description: '商户订单号',
example: '202500000001'
}),
name: z.string().min(1).max(127).openapi({
description: '商品名称',
example: 'VIP会员服务'
}),
money: z.union([
z.string().regex(/^\d+(\.\d{1,2})?$/),
z.number().positive()
]).transform(val => typeof val === 'string' ? parseFloat(val) : val).openapi({
description: '支付金额(元),支持字符串或数字格式',
example: 9.9
}),
clientip: z.string().ip().openapi({
description: '客户端IP地址',
example: '127.0.0.1'
}),
device: z.string().optional().openapi({
description: '设备信息,可选',
example: 'iPhone13,4'
}),
param: z.string().optional().openapi({
description: '自定义参数,可选',
example: 'user_id=123'
}),
});
export const PaymentApiRequestSchema = z.object({
pid: z.string().min(1).openapi({
description: '商户ID',
example: '10086'
}),
type: z.enum(['alipay', 'wxpay']).openapi({
description: '支付类型',
example: 'alipay'
}),
out_trade_no: z.string().min(1).openapi({
description: '商户订单号',
example: '202500000001'
}),
notify_url: z.string().url().openapi({
description: '异步通知地址',
example: 'https://example.com/payment/notify'
}),
name: z.string().min(1).max(127).openapi({
description: '商品名称',
example: 'VIP会员服务'
}),
money: z.union([
z.string().regex(/^\d+(\.\d{1,2})?$/),
z.number().positive()
]).transform(val => typeof val === 'string' ? parseFloat(val) : val).openapi({
description: '支付金额(元)',
example: 9.9
}),
clientip: z.string().ip().openapi({
description: '客户端IP地址',
example: '127.0.0.1'
}),
device: z.string().optional().openapi({
description: '设备信息,可选',
example: 'iPhone13,4'
}),
param: z.string().optional().openapi({
description: '自定义参数,可选',
example: 'user_id=123'
}),
sign: z.string().min(1).openapi({
description: '签名',
example: 'e10adc3949ba59abbe56e057f20f883e'
}),
sign_type: z.literal('MD5').default('MD5').openapi({
description: '签名类型',
example: 'MD5'
}),
});
export const PaymentResponseSchema = z.object({
code: z.number().openapi({
description: '响应状态码',
example: 200
}),
msg: z.string().optional().openapi({
description: '响应消息',
example: '支付请求成功'
}),
trade_no: z.string().optional().openapi({
description: '支付平台交易号',
example: '202500000001'
}),
O_id: z.string().optional().openapi({
description: '订单ID',
example: 'order_123456'
}),
payurl: z.string().optional().openapi({
description: '支付跳转URL',
example: 'https://pay.example.com/pay/202500000001'
}),
qrcode: z.string().optional().openapi({
description: '支付二维码内容',
example: 'weixin://wxpay/bizpayurl?pr=abcdefg'
}),
img: z.string().optional().openapi({
description: '支付二维码图片URL',
example: 'https://pay.example.com/qrcode/202500000001.png'
}),
});
export type PaymentRequestDto = z.infer<typeof PaymentRequestSchema>;
export const NotifyRequestSchema = z.object({
pid: z.string().min(1).openapi({
description: '商户ID',
example: '10086'
}),
trade_no: z.string().min(1).openapi({
description: '支付平台交易号',
example: '202500000001'
}),
out_trade_no: z.string().min(1).openapi({
description: '商户订单号',
example: '202500000001'
}),
type: z.enum(['alipay', 'wxpay']).openapi({
description: '支付类型',
example: 'alipay'
}),
name: z.string().min(1).openapi({
description: '商品名称',
example: 'VIP会员服务'
}),
money: z.union([
z.string().regex(/^\d+(\.\d{1,2})?$/),
z.number().positive()
]).transform(val => typeof val === 'string' ? parseFloat(val) : val).openapi({
description: '支付金额(元)',
example: 9.9
}),
trade_status: z.enum(['TRADE_SUCCESS', 'TRADE_FAILED']).openapi({
description: '交易状态',
example: 'TRADE_SUCCESS'
}),
param: z.string().optional().openapi({
description: '自定义参数',
example: 'user_id=123'
}),
sign: z.string().min(1).openapi({
description: '签名',
example: 'e10adc3949ba59abbe56e057f20f883e'
}),
sign_type: z.literal('MD5').default('MD5').openapi({
description: '签名类型',
example: 'MD5'
}),
});
export const QueryRequestSchema = z.object({
order_id: z.string().min(1).openapi({
description: '订单ID',
example: 'order_123456'
}),
});
export const QueryResponseSchema = z.object({
code: z.number().openapi({
description: '响应状态码',
example: 200
}),
msg: z.string().optional().openapi({
description: '响应消息',
example: '查询成功'
}),
data: z.object({
out_trade_no: z.string().openapi({
description: '商户订单号',
example: '202500000001'
}),
trade_no: z.string().optional().openapi({
description: '支付平台交易号',
example: '202500000001'
}),
type: z.enum(['alipay', 'wxpay']).openapi({
description: '支付类型',
example: 'alipay'
}),
name: z.string().openapi({
description: '商品名称',
example: 'VIP会员服务'
}),
money: z.number().openapi({
description: '支付金额(元)',
example: 9.9
}),
status: z.nativeEnum(PaymentStatus).openapi({
description: '订单状态',
example: 1
}),
create_time: z.string().openapi({
description: '订单创建时间',
example: '2025-05-29 12:00:00'
}),
update_time: z.string().openapi({
description: '订单更新时间',
example: '2025-05-29 12:05:00'
}),
}).optional(),
});
export const PaymentErrorSchema = z.object({
code: z.string().openapi({
description: 'HTTP错误状态码',
example: 'error',
}),
msg: z.string().openapi({
description: '错误描述信息',
example: '无效的支付参数',
}),
});
export type PaymentResponseDto = z.infer<typeof PaymentResponseSchema>;
export type PaymentErrorDto = z.infer<typeof PaymentErrorSchema>;
export type NotifyRequestDto = z.infer<typeof NotifyRequestSchema>;
export type QueryRequestDto = z.infer<typeof QueryRequestSchema>;
export type QueryResponseDto = z.infer<typeof QueryResponseSchema>;

View File

@@ -0,0 +1,46 @@
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
export enum PaymentStatus {
PENDING = 0,
SUCCESS = 1,
FAILED = 2,
REFUNDED = 3,
CANCELED = 4
}
@Entity('d8d_payments')
export class PaymentEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'out_trade_no', type: 'varchar', length: 64 })
outTradeNo!: string;
@Column({ name: 'trade_no', type: 'varchar', length: 64, nullable: true })
tradeNo!: string;
@Column({ type: 'enum', enum: ['alipay', 'wxpay'] })
type!: 'alipay' | 'wxpay';
@Column({ type: 'decimal', precision: 10, scale: 2 })
money!: number;
@Column({ type: 'varchar', length: 255 })
name!: string;
@Column({ name: 'client_ip', type: 'varchar', length: 64 })
clientIp!: string;
@Column({ type: 'enum', enum: PaymentStatus, default: PaymentStatus.PENDING })
status!: PaymentStatus;
@Column({ name: 'error_msg', type: 'varchar', length: 512, nullable: true })
errorMsg?: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
updatedAt!: Date;
}

View File

@@ -0,0 +1,190 @@
import { DataSource } from 'typeorm';
import { createHash } from 'crypto';
import { FormData } from 'node-fetch';
import fetch from 'node-fetch';
import { z } from 'zod';
import process from 'node:process'
import { PaymentEntity, PaymentStatus } from './payment.entity';
import { PaymentApiRequestSchema } from './dto/payment.dto';
import {
PaymentRequestDto,
PaymentResponseDto,
NotifyRequestDto,
QueryRequestDto,
QueryResponseDto,
PaymentRequestSchema,
PaymentResponseSchema,
PaymentErrorSchema,
PaymentErrorDto
} from './dto/payment.dto';
export class PaymentService {
private readonly pid: string;
private readonly pkey: string;
private readonly notifyUrl: string;
private readonly apiUrl: string;
constructor(dataSource: DataSource) {
this.dataSource = dataSource;
if (!process.env.ZPAY_PID || !process.env.ZPAY_PKEY || !process.env.ZPAY_NOTIFY_URL) {
throw new Error('Missing required ZPAY environment variables');
}
this.pid = process.env.ZPAY_PID;
this.pkey = process.env.ZPAY_PKEY;
this.notifyUrl = process.env.ZPAY_NOTIFY_URL;
this.apiUrl = process.env.ZPAY_URL || 'https://zpayz.cn/mapi.php';
}
private readonly dataSource: DataSource;
async createPayment(payload: PaymentRequestDto): Promise<PaymentResponseDto> {
// 验证输入参数
const validated = PaymentRequestSchema.parse(payload);
const payment = new PaymentEntity();
payment.outTradeNo = validated.out_trade_no;
payment.type = validated.type;
payment.money = validated.money; // 已由DTO转换
payment.name = validated.name;
payment.clientIp = validated.clientip;
await this.dataSource.manager.save(payment);
const response = await this.callPaymentApi(payload);
if (this.isPaymentResponse(response)) {
if (response.trade_no) payment.tradeNo = response.trade_no;
payment.status = PaymentStatus.SUCCESS;
} else {
payment.status = PaymentStatus.FAILED;
payment.errorMsg = response.msg;
}
await this.dataSource.manager.save(payment);
return this.isPaymentResponse(response) ? response : {
code: 0,
msg: response.msg
};
}
async handleNotify(payload: NotifyRequestDto): Promise<{ code: number; msg: string }> {
// 验证签名
const sign = this.generateSign(payload);
if (sign !== payload.sign) {
return { code: 0, msg: '签名验证失败' };
}
// 查找订单
const payment = await this.dataSource.manager.findOne(PaymentEntity, {
where: [
{ outTradeNo: payload.out_trade_no },
{ tradeNo: payload.trade_no }
]
});
if (!payment) {
return { code: 0, msg: '订单不存在' };
}
// 更新订单状态
payment.status = payload.trade_status === 'TRADE_SUCCESS' ? PaymentStatus.SUCCESS : PaymentStatus.FAILED;
await this.dataSource.manager.save(payment);
return { code: 1, msg: '处理成功' };
}
async queryPayment(payload: QueryRequestDto): Promise<QueryResponseDto> {
const payment = await this.dataSource.manager.findOne(PaymentEntity, {
where: { outTradeNo: payload.order_id }
});
if (!payment) {
return { code: 0, msg: '订单不存在' };
}
return {
code: 1,
msg: '查询成功',
data: {
out_trade_no: payment.outTradeNo,
trade_no: payment.tradeNo,
type: payment.type,
name: payment.name,
money: payment.money,
status: payment.status,
create_time: payment.createdAt.toISOString(),
update_time: payment.updatedAt.toISOString(),
}
};
}
private async callPaymentApi(payload: PaymentRequestDto): Promise<PaymentResponseDto | PaymentErrorDto> {
const apiPayload = {
...payload,
pid: this.pid,
notify_url: this.notifyUrl,
sign_type: 'MD5'
};
const sign = this.generateSign(apiPayload);
const validated = z.object({
...PaymentApiRequestSchema.shape,
sign: z.string().min(1)
}).parse({
...apiPayload,
sign
});
const form = new FormData();
for (const [key, value] of Object.entries(validated)) {
form.append(key, value.toString());
}
const response = await fetch(this.apiUrl, {
method: 'POST',
body: form
});
const json = await response.json();
try {
return PaymentResponseSchema.parse(json);
} catch (error) {
return PaymentErrorSchema.parse(json);
}
}
private isPaymentRequest(payload: any): payload is PaymentRequestDto {
return 'notify_url' in payload && 'clientip' in payload;
}
private isPaymentResponse(response: PaymentResponseDto | PaymentErrorDto): response is PaymentResponseDto {
return typeof response.code === 'number';
}
private generateSign(payload: PaymentRequestDto | Omit<NotifyRequestDto, 'trade_status'>): string {
const params = new URLSearchParams();
params.append('pid', this.pid);
params.append('type', payload.type);
params.append('out_trade_no', payload.out_trade_no);
if (this.isPaymentRequest(payload)) {
params.append('notify_url', this.notifyUrl);
params.append('clientip', payload.clientip);
if (payload.device) params.append('device', payload.device);
}
params.append('name', payload.name);
params.append('money', payload.money.toString());
params.append('sign_type', 'MD5');
if (payload.param) {
params.append('param', payload.param);
}
const paramString = params.toString() + `&key=${this.pkey}`;
return createHash('md5').update(paramString).digest('hex').toLowerCase();
}
}

View File

@@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
export type Permission = string;
@Entity({ name: 'd8d_role' })
export class Role {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', length: 50, unique: true })
name!: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ type: 'simple-array', nullable: false })
permissions: Permission[] = [];
constructor(partial?: Partial<Role>) {
Object.assign(this, partial);
if (!this.permissions) {
this.permissions = [];
}
}
}

View File

@@ -0,0 +1,64 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Role } from './role.entity';
@Entity({ name: 'd8d_users' })
export class UserEntity {
@PrimaryGeneratedColumn({ name: 'user_id', unsigned: true })
id!: number;
@Column({ nullable: true, name: 'account', unique: true, type: 'varchar', length: 255 })
username!: string;
@Column({ nullable: true, name: 'pwd', type: 'varchar', length: 255 })
password!: string;
@Column({ nullable: true, name: 'mail', unique: true, type: 'varchar', length: 100 })
email!: string;
@Column({ nullable: true, name: 'mobile', type: 'varchar', length: 255 })
mobile!: string;
@Column({ nullable: true, type: 'varchar', length: 50 })
nickname!: string;
@Column({ nullable: true, type: 'varchar', length: 50 })
name!: string;
@Column({ nullable: true, name: 'wx_web_openid', type: 'varchar', length: 50 })
wxWebOpenid!: string;
@Column({ nullable: true, name: 'wx_mini_openid', type: 'varchar', length: 50 })
wxMiniOpenid!: string;
@Column({ default: 0, type: 'int' })
status!: number;
@Column({ nullable: true, name: 'company_id', type: 'int' })
companyId!: number;
@Column({ nullable: true, type: 'varchar', length: 100 })
department!: string;
@Column({ nullable: true, type: 'varchar', length: 100 })
position!: string;
@Column({ default: 0, name: 'is_deleted', type: 'int' })
isDeleted!: number;
@Column({ default: 0, name: 'is_disabled', type: 'int' })
isDisabled!: number;
@ManyToMany(() => Role)
@JoinTable()
roles!: Role[];
@CreateDateColumn({name:'created_at', type: 'timestamp'})
createdAt!: Date;
@UpdateDateColumn({name:'updated_at', type: 'timestamp'})
updatedAt!: Date;
constructor(partial?: Partial<UserEntity>) {
Object.assign(this, partial);
}
}

View File

@@ -0,0 +1,138 @@
import { HTTPException } from 'hono/http-exception'
import { DataSource } from 'typeorm';
import { UserEntity as 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>;
private readonly dataSource: DataSource;
constructor(dataSource: DataSource) {
this.dataSource = dataSource;
this.userRepository = this.dataSource.getRepository(User);
this.roleRepository = this.dataSource.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 HTTPException(400,{ message: 'Failed to create user', cause: error})
}
}
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 getUserByPhone(phone: string): Promise<User | null> {
try {
return await this.userRepository.findOne({
where: { mobile: phone },
relations: ['roles']
});
} catch (error) {
console.error('Error getting user by phone:', error);
throw new Error('Failed to get user by phone');
}
}
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 HTTPException(500, { message: 'Failed to get users', cause: error })
}
}
getUserRepository(): Repository<User> {
return this.userRepository;
}
async getUserByAccount(account: string): Promise<User | null> {
try {
return await this.userRepository.findOne({
where: [{ username: account }, { email: account }],
relations: ['roles']
});
} catch (error) {
console.error('Error getting user by account:', error);
throw new Error('Failed to get user by account');
}
}
}

46
src/server/renderer.tsx Normal file
View File

@@ -0,0 +1,46 @@
import { GlobalConfig } from '@/share/types'
import { reactRenderer } from '@hono/react-renderer'
import { Script, Link } from 'hono-vite-react-stack-node/components'
import process from 'node:process'
// 全局配置常量
const GLOBAL_CONFIG: GlobalConfig = {
OSS_BASE_URL: process.env.OSS_BASE_URL || 'https://oss.d8d.fun',
// API_BASE_URL: '/api',
APP_NAME: process.env.APP_NAME || '多八多Aider',
ENV: process.env.NODE_ENV || 'production', // 添加环境变量,
ROOT_DIRECTORY: process.env.ROOT_DIRECTORY || '',
REMOTE_AUTHORITY: process.env.REMOTE_AUTHORITY || '',
VERSION: process.env.VERSION || '0.1.0',
}
export const renderer = reactRenderer(({ children }) => {
return (
<html>
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<Script />
<Link href="/src/style.css" rel="stylesheet" />
{/* <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
<script dangerouslySetInnerHTML={{ __html: `
const init = () => {
const urlParams = new URLSearchParams(window.location.search);
if (${import.meta.env?.PROD ? "true":"false"} && !urlParams.has('vconsole')) return;
var vConsole = new VConsole({
theme: urlParams.get('vconsole_theme') || 'light',
onReady: function() {
console.log('vConsole is ready');
}
});
}
init();
`}} /> */}
{/* 注入全局配置 */}
<script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
</head>
<body>{children}</body>
</html>
)
})

View File

@@ -0,0 +1,9 @@
import { UserEntity } from "../modules/users/user.entity";
// 扩展Context类型
export type Variables = {
user: UserEntity;
token: string;
}
export type AuthContext = { Variables: Variables }

View File

@@ -0,0 +1,38 @@
import { randomBytes } from 'crypto';
import { writeFileSync, existsSync, readFileSync, appendFileSync } from 'fs';
export function generateJwtSecret(): string {
if (existsSync('.env')) {
const envContent = readFileSync('.env', 'utf-8');
if (envContent.includes('JWT_SECRET')) {
return 'JWT_SECRET已存在';
}
}
const secret = randomBytes(64).toString('hex');
const envLine = `JWT_SECRET=${secret}\n`;
if (existsSync('.env')) {
appendFileSync('.env', envLine);
} else {
writeFileSync('.env', envLine);
}
return secret;
}
export function checkRequiredEnvVars(): boolean {
const requiredVars = [
'DB_HOST',
'DB_PORT',
'DB_USERNAME',
'DB_PASSWORD',
'DB_DATABASE',
'JWT_SECRET'
];
if (!existsSync('.env')) return false;
const envContent = readFileSync('.env', 'utf-8');
return requiredVars.every(varName => envContent.includes(varName));
}

View File

@@ -0,0 +1,34 @@
import { Context } from 'hono'
import { z } from 'zod'
import { HTTPException } from 'hono/http-exception'
export const ErrorSchema = z.object({
code: z.number().openapi({
example: 400,
}),
message: z.string().openapi({
example: 'Bad Request',
}),
})
export const errorHandler = async (err: Error, c: Context) => {
if (err instanceof HTTPException) {
const details = err.cause ? { details: err.cause instanceof Error ? err.cause.message : err.cause } : {}
return c.json(
{
code: err.status,
message: err.message,
...details
},
err.status
)
}
return c.json(
{
code: 500,
message: err.message || 'Internal Server Error'
},
500
)
}

113
src/server/utils/redis.ts Normal file
View File

@@ -0,0 +1,113 @@
import Redis, { RedisOptions, Redis as RedisClient } from 'ioredis';
import debug from 'debug';
const logger = {
info: debug('app:redis:info'),
error: debug('app:redis:error'),
};
type RedisConfig = RedisOptions & {
reconnectDelay?: number;
maxRetries?: number;
};
class RedisService {
private static instance: RedisService;
private client: RedisClient;
private config: RedisConfig;
private retryCount = 0;
private constructor(config: RedisConfig) {
this.config = {
host: 'localhost',
port: 6379,
reconnectDelay: 5000,
maxRetries: 3,
...config,
};
this.client = new Redis(this.config);
this.setupEventListeners();
}
public static getInstance(config?: RedisConfig): RedisService {
if (!RedisService.instance) {
RedisService.instance = new RedisService(config || {});
}
return RedisService.instance;
}
private setupEventListeners(): void {
this.client.on('connect', () => {
logger.info('Redis connected');
this.retryCount = 0;
});
this.client.on('error', (err) => {
logger.error(`Redis error: ${err.message}`);
if (this.retryCount < (this.config.maxRetries || 3)) {
this.retryCount++;
setTimeout(() => {
this.client.connect().catch(() => {});
}, this.config.reconnectDelay);
}
});
this.client.on('reconnecting', () => {
logger.info('Redis reconnecting...');
});
}
public async connect(): Promise<void> {
try {
await this.client.connect();
} catch (err) {
logger.error(`Redis connection failed: ${err}`);
throw err;
}
}
public async disconnect(): Promise<void> {
try {
await this.client.quit();
} catch (err) {
logger.error(`Redis disconnect failed: ${err}`);
throw err;
}
}
public async get(key: string): Promise<string | null> {
return this.client.get(key);
}
public async set(
key: string,
value: string,
ttl?: number
): Promise<'OK' | null> {
if (ttl) {
return this.client.set(key, value, 'EX', ttl);
}
return this.client.set(key, value);
}
public async del(key: string): Promise<number> {
return this.client.del(key);
}
public getClient(): RedisClient {
return this.client;
}
}
// 默认配置从环境变量读取
const redisConfig: RedisConfig = {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined,
password: process.env.REDIS_PASSWORD,
};
const redisService = RedisService.getInstance(redisConfig);
export type { RedisClient };
export { redisService };

73
src/server/utils/sms.ts Normal file
View File

@@ -0,0 +1,73 @@
import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525';
import OpenApi, * as $OpenApi from '@alicloud/openapi-client';
import Util, * as $Util from '@alicloud/tea-util';
import process from 'node:process'
type SMSError = {
message?: string;
data?: {
Recommend?: string;
};
};
export class SMS {
/**
* 创建阿里云短信客户端
* @throws {Error} 当阿里云配置缺失时抛出错误
*/
static createClient(): Dysmsapi20170525 {
const accessKeyId = process.env.ALICLOUD_ACCESS_KEY;
const accessKeySecret = process.env.ALICLOUD_SECRET_KEY;
if (!accessKeyId || !accessKeySecret) {
throw new Error('阿里云配置缺失');
}
const config = new $OpenApi.Config({
accessKeyId,
accessKeySecret,
endpoint: 'dysmsapi.aliyuncs.com'
});
return new (Dysmsapi20170525 as any).default(config);
}
/**
* 发送验证码短信
* @param phoneNumber 接收手机号
* @param code 验证码
* @param templateCode 短信模板代码(可选)
* @param signName 短信签名(可选)
* @returns Promise<boolean> 发送是否成功
*/
static async sendVerificationSMS(
phoneNumber: string,
code: string,
templateCode?: string,
signName?: string
): Promise<boolean> {
try {
const client = this.createClient();
const sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({
signName: signName || process.env.SMS_DEFAULT_SIGN_NAME || '多八多',
templateCode: templateCode || process.env.SMS_DEFAULT_TEMPLATE_CODE || 'SMS_164760103',
phoneNumbers: phoneNumber,
templateParam: JSON.stringify({ code })
});
const runtime = new $Util.RuntimeOptions({});
await client.sendSmsWithOptions(sendSmsRequest, runtime);
return true;
} catch (error: unknown) {
const err = error as SMSError;
console.error('短信发送失败:', err.message);
if (err.data?.Recommend) {
console.error('建议:', err.data.Recommend);
}
return false;
}
}
}

273
src/share/types.ts Normal file
View File

@@ -0,0 +1,273 @@
export interface GlobalConfig {
OSS_BASE_URL: string
// API_BASE_URL: string
APP_NAME: string
ENV: string
ROOT_DIRECTORY: string
REMOTE_AUTHORITY: string
VERSION: string
}
// 定义工作空间数据接口
export interface Workspace {
id: number;
name: string;
key: string;
region: string;
owner_id: number;
owner_email: string;
status: string;
quotas: WorkspaceQuotas;
tags?: Record<string, string>;
created_at: string;
updated_at: string;
}
// 项目类型枚举常量
export enum ItemType {
PROJECT = 'project',
TEMPLATE = 'template'
}
// 预览模式枚举常量
export enum PreviewMode {
MOBILE = 'mobile',
DESKTOP = 'desktop'
}
export interface WorkspaceQuotas {
max_tables: number;
max_table_size: number;
max_total_size: number;
max_storage_size: number;
max_functions: number;
max_redis_instances: number;
max_redis_memory: number;
}
// 定义项目数据接口
export interface Project {
id: number;
workspace_id: number;
name: string;
description?: string;
files: ProjectFile[]; // 使用jsonb字段存储文件列表
thumbnail?: string;
user_id?: number;
git_repo_url?: string; // 添加Git仓库URL字段
created_at: string;
updated_at: string;
}
// 定义项目文件接口
export interface ProjectFile {
id?: number;
project_id?: number;
name: string;
content: string;
type: string; // 文件类型html, css, js, json等
is_main?: boolean;
created_at?: string;
updated_at?: string;
}
// 定义模板数据接口
export interface Template {
id: number;
workspace_id: number;
name: string;
description?: string;
files: TemplateFile[]; // 使用jsonb字段存储文件列表
thumbnail?: string;
category: string;
git_repo_url?: string; // 添加Git仓库URL字段
created_at: string;
created_by?: number; // 添加创建者字段
}
// 定义模板文件接口
export interface TemplateFile {
id?: number;
template_id?: number;
name: string;
content: string;
type: string; // 文件类型html, css, js, json等
is_main?: boolean;
created_at?: string;
updated_at?: string;
}
// 定义模板广场数据接口
export interface PublicTemplate {
id: number;
originalId: number; // 原始模板ID
name: string;
description?: string;
thumbnail?: string;
category: string;
authorId: number; // 作者ID
authorName?: string; // 作者名称
downloads: number; // 下载次数
gitRepoUrl?: string; // Git仓库URL
createdAt: string;
updatedAt: string;
}
// 定义聊天记录接口
export interface ChatHistory {
id: number
project_id?: number
user_id?: number
role: string
message: string
created_at: string
}
// 定义编辑信息接口
export interface Edits {
files?: string[];
commit_hash?: string;
commit_message?: string;
diff?: string;
updated_files?: UpdatedFile[];
}
// 定义更新文件接口
export interface UpdatedFile {
name: string;
content?: string;
type: string;
is_main: boolean;
}
// 定义用户数据接口
export interface AdminUser {
id: number
nickname: string
name?: string;
phone?: string
email?: string
status: number
}
export interface D8dUser {
user_id: number
account: string
mobile: string
mail?: string
nickname?: string
name?: string
wx_web_openid?: string
wx_mini_openid?: string
status: number
created_at: string
updated_at: string
}
// 模板分类常量定义
export const TEMPLATE_CATEGORIES = [
{ value: 'custom', label: '自定义' },
{ value: 'webpage', label: '网页' },
{ value: 'dashboard', label: '仪表盘' },
{ value: 'form', label: '表单' },
{ value: 'landing', label: '落地页' },
{ value: 'application', label: '应用' },
{ value: 'blog', label: '博客' },
{ value: 'ecommerce', label: '电商' },
{ value: 'portfolio', label: '作品集' },
{ value: 'admin', label: '后台管理' },
{ value: 'mobile', label: '移动端' },
{ value: 'chart', label: '图表' },
{ value: 'report', label: '报表' },
{ value: 'list', label: '列表' },
{ value: 'erp', label: 'ERP进销存' },
{ value: 'gallery', label: '画廊' },
{ value: 'chat', label: '聊天' },
];
// 模板类型定义接口
export interface TemplateType {
id: string;
name: string;
description: string;
git_repo_url: string;
}
// 定义模板类型映射
export interface TemplateTypeMap {
[key: string]: TemplateType;
}
// 类型定义
export interface ChatMessage {
role: 'user' | 'assistant';
content: string;
id?: number; // 添加可选的 id 属性
}
// 定义认证上下文类型
export interface AuthContextType {
token: string | null;
refreshToken: string | null;
user: D8dUser | null;
login: (phone: string, code: string) => Promise<boolean>;
logout: () => Promise<void>;
isAuthenticated: boolean;
isLoading: boolean; // 添加加载状态
}
// 定义ChatTopic接口
export interface ChatTopic {
id: number;
title: string;
description?: string;
workspace_id: number;
project_id?: number;
template_id?: number;
user_id?: number;
created_at?: string;
updated_at?: string;
last_active_at?: string;
}
// 定义工作空间上下文类型
export interface WorkspaceContextType {
currentWorkspace: Workspace | null;
showWorkspace: boolean;
setShowWorkspace: (value: boolean) => void;
setCurrentWorkspace: (workspace: Workspace | null) => void;
handleWorkspaceSelect: (workspace: Workspace | null) => void;
handleSetShowWorkspace: (workspaceOrBoolean: Workspace | boolean) => void;
isWorkspaceLoading: boolean;
workspaceError: Error | null;
refetchWorkspace: () => void;
}
// 文件类型
export interface GitFile {
name: string;
path: string;
content: string;
type: string;
isDirectory: boolean;
is_main: boolean;
}
// 协作仓库接口
export interface Repository {
id: number;
name: string;
full_name: string;
workspace_id: number;
template_id: number | null;
project_id: number | null;
html_url: string;
description: string;
clone_url: string;
created_at: string;
updated_at: string;
workspace_name: string;
template_name: string | null;
project_name: string | null;
}

1
src/style.css Normal file
View File

@@ -0,0 +1 @@
@import 'tailwindcss';