| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- /** @jsxImportSource https://esm.d8d.fun/hono@4.7.4/jsx */
- import { Hono } from 'hono'
- import { Auth } from '@d8d-appcontainer/auth'
- import type { User as AuthUser } from '@d8d-appcontainer/auth'
- import React from 'hono/jsx'
- import type { FC } from 'hono/jsx'
- import { cors } from 'hono/cors'
- import type { Context as HonoContext } from 'hono'
- import { serveStatic } from 'hono/deno'
- import { APIClient } from '@d8d-appcontainer/api'
- import debug from "debug"
- import dayjs from 'dayjs';
- import utc from 'dayjs/plugin/utc';
- import type { SystemSettingRecord, GlobalConfig } from './asset/share/types.ts';
- import { SystemSettingKey, OssType, MapMode } from './asset/share/types.ts';
- import {
- createKnowInfoRoutes,
- createFileCategoryRoutes,
- createFileUploadRoutes,
- createThemeRoutes,
- createSystemSettingsRoutes,
- } from "./routes_sys.ts";
- import {
- createMapRoutes,
- } from "./routes_maps.ts";
- import {
- createChartRoutes,
- } from "./routes_charts.ts";
- import { migrations } from './migrations.ts';
- // 导入基础路由
- import { createAuthRoutes } from "./routes_auth.ts";
- import { createUserRoutes } from "./routes_users.ts";
- dayjs.extend(utc)
- // 初始化debug实例
- const log = {
- app: debug('app:server'),
- auth: debug('auth:server'),
- api: debug('api:server'),
- debug: debug('debug:server')
- }
- const GLOBAL_CONFIG: GlobalConfig = {
- OSS_BASE_URL: Deno.env.get('OSS_BASE_URL') || 'https://d8d-appcontainer-user.oss-cn-beijing.aliyuncs.com',
- OSS_TYPE: Deno.env.get('OSS_TYPE') === OssType.MINIO ? OssType.MINIO : OssType.ALIYUN,
- API_BASE_URL: '/api',
- APP_NAME: Deno.env.get('APP_NAME') || '应用Starter',
- ENV: Deno.env.get('ENV') || 'development',
- DEFAULT_THEME: 'light', // 默认主题
- MAP_CONFIG: {
- KEY: Deno.env.get('AMAP_KEY') || '您的地图API密钥',
- VERSION: '2.0',
- PLUGINS: ['AMap.ToolBar', 'AMap.Scale', 'AMap.HawkEye', 'AMap.MapType', 'AMap.Geolocation'],
- MAP_MODE: Deno.env.get('MAP_MODE') === MapMode.OFFLINE ? MapMode.OFFLINE : MapMode.ONLINE,
- },
- CHART_THEME: 'default', // 图表主题
- ENABLE_THEME_CONFIG: false, // 主题配置开关
- THEME: null
- };
- log.app.enabled = true
- log.auth.enabled = true
- log.api.enabled = true
- log.debug.enabled = true
- // 定义自定义上下文类型
- export interface Variables {
- auth: Auth
- user?: AuthUser
- apiClient: APIClient
- moduleDir: string
- systemSettings?: SystemSettingRecord
- }
- // 定义登录历史类型
- interface LoginHistory {
- id: number
- user_id: number
- login_time: string
- ip_address?: string
- user_agent?: string
- }
- // 定义仪表盘数据类型
- interface DashboardData {
- lastLogin: string
- loginCount: number
- fileCount: number
- userCount: number
- systemInfo: {
- version: string
- lastUpdate: string
- }
- }
- interface EsmScriptConfig {
- src: string
- href: string
- denoJson: string
- refresh: boolean
- prodPath?: string
- prodSrc?: string
- }
- // Auth实例
- let authInstance: Auth | null = null
- // 初始化Auth实例
- const initAuth = async (apiClient: APIClient) => {
- try {
- if (authInstance) {
- return authInstance
- }
- log.auth('正在初始化Auth实例')
-
- authInstance = new Auth(apiClient as any, {
- jwtSecret: Deno.env.get("JWT_SECRET") || 'your-jwt-secret-key',
- initialUsers: [],
- storagePrefix: '',
- userTable: 'users',
- fieldNames: {
- id: 'id',
- username: 'username',
- password: 'password',
- phone: 'phone',
- email: 'email',
- is_disabled: 'is_disabled',
- is_deleted: 'is_deleted'
- },
- tokenExpiry: 24 * 60 * 60,
- refreshTokenExpiry: 7 * 24 * 60 * 60
- })
-
- log.auth('Auth实例初始化完成')
-
- return authInstance
- } catch (error) {
- log.auth('Auth初始化失败:', error)
- throw error
- }
- }
- // 初始化系统设置
- const initSystemSettings = async (apiClient: APIClient) => {
- try {
- const systemSettings = await apiClient.database.table('system_settings')
- .select()
-
- // 将系统设置转换为键值对形式
- const settings = systemSettings.reduce((acc: Record<string, any>, setting: any) => {
- acc[setting.key] = setting.value
- return acc
- }, {}) as SystemSettingRecord
-
- // 更新全局配置
- if (settings[SystemSettingKey.SITE_NAME]) {
- GLOBAL_CONFIG.APP_NAME = String(settings[SystemSettingKey.SITE_NAME])
- }
-
- // 设置其他全局配置项
- if (settings[SystemSettingKey.SITE_FAVICON]) {
- GLOBAL_CONFIG.DEFAULT_THEME = String(settings[SystemSettingKey.SITE_FAVICON])
- }
-
- if (settings[SystemSettingKey.SITE_LOGO]) {
- GLOBAL_CONFIG.MAP_CONFIG.KEY = String(settings[SystemSettingKey.SITE_LOGO])
- }
-
- if (settings[SystemSettingKey.SITE_DESCRIPTION]) {
- GLOBAL_CONFIG.CHART_THEME = String(settings[SystemSettingKey.SITE_DESCRIPTION])
- }
- // 设置主题配置开关
- if (settings[SystemSettingKey.ENABLE_THEME_CONFIG]) {
- GLOBAL_CONFIG.ENABLE_THEME_CONFIG = settings[SystemSettingKey.ENABLE_THEME_CONFIG] === 'true'
- }
- // 查询ID1管理员的主题配置
- const adminTheme = await apiClient.database.table('theme_settings')
- .where('user_id', 1)
- .first()
-
- if (adminTheme) {
- GLOBAL_CONFIG.THEME = adminTheme.settings
- }
-
- return settings
-
- } catch (error) {
- log.app('获取系统设置失败:', error)
- return {} as SystemSettingRecord
- }
- }
- // 初始化数据库
- const initDatabase = async (apiClient: APIClient) => {
- try {
- log.app('正在执行数据库迁移...')
-
- const migrationsResult = await apiClient.database.executeLiveMigrations(migrations)
- // log.app('数据库迁移完成 %O',migrationsResult)
- log.app('数据库迁移完成')
-
- } catch (error) {
- log.app('数据库迁移失败:', error)
- }
- }
- // 中间件:数据库初始化
- const withDatabase = async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
- try {
- const apiClient = c.get('apiClient')
- await initDatabase(apiClient)
- await next()
- } catch (error) {
- log.api('数据库操作失败:', error)
- return c.json({ error: '数据库操作失败' }, 500)
- }
- }
- // 中间件:验证认证
- const withAuth = async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
- try {
- const auth = c.get('auth')
-
- const token = c.req.header('Authorization')?.replace('Bearer ', '')
- if (token) {
- const userData = await auth.verifyToken(token)
- if (userData) {
- c.set('user', userData)
- await next()
- return
- }
- }
-
- return c.json({ error: '未授权' }, 401)
- } catch (error) {
- log.auth('认证失败:', error)
- return c.json({ error: '无效凭证' }, 401)
- }
- }
- // 导出withAuth类型定义
- export type WithAuth = typeof withAuth;
- // 定义模块参数接口
- interface ModuleParams {
- apiClient: APIClient
- app: Hono<{ Variables: Variables }>
- moduleDir: string
- }
- export default function({ apiClient, app, moduleDir }: ModuleParams) {
- const honoApp = app
- // 添加CORS中间件
- honoApp.use('/*', cors())
-
- // 创建API路由
- const api = new Hono<{ Variables: Variables }>()
- // 设置环境变量
- api.use('*', async (c, next) => {
- c.set('apiClient', apiClient)
- c.set('moduleDir', moduleDir)
- c.set('auth', await initAuth(apiClient))
- c.set('systemSettings', await initSystemSettings(apiClient))
- await next()
- })
- // 使用数据库中间件
- api.use('/', withDatabase)
- // 查询仪表盘数据
- api.get('/dashboard', withAuth, async (c) => {
- try {
- const user = c.get('user')!
- const apiClient = c.get('apiClient')
- const lastLogin = await apiClient.database.table('login_history')
- .where('user_id', user.id)
- .orderBy('login_time', 'desc')
- .limit(1)
- .first()
-
- // 获取登录总次数
- const loginCount = await apiClient.database.table('login_history')
- .where('user_id', user.id)
- .count()
-
- // 获取系统数据统计
- const fileCount = await apiClient.database.table('file_library')
- .where('is_deleted', 0)
- .count()
-
- const userCount = await apiClient.database.table('users')
- .where('is_deleted', 0)
- .count()
-
- // 返回仪表盘数据
- const dashboardData: DashboardData = {
- lastLogin: lastLogin ? lastLogin.login_time : new Date().toISOString(),
- loginCount: loginCount,
- fileCount: Number(fileCount),
- userCount: Number(userCount),
- systemInfo: {
- version: '1.0.0',
- lastUpdate: new Date().toISOString()
- }
- }
-
- return c.json(dashboardData)
- } catch (error) {
- log.api('获取仪表盘数据失败:', error)
- return c.json({ error: '获取仪表盘数据失败' }, 500)
- }
- })
- // 注册基础路由
- api.route('/auth', createAuthRoutes(withAuth))
- api.route('/users', createUserRoutes(withAuth))
- api.route('/know-info', createKnowInfoRoutes(withAuth))
- api.route('/upload', createFileUploadRoutes(withAuth)) // 添加文件上传路由
- api.route('/file-categories', createFileCategoryRoutes(withAuth)) // 添加文件分类管理路由
- api.route('/theme', createThemeRoutes(withAuth)) // 添加主题设置路由
- api.route('/charts', createChartRoutes(withAuth)) // 添加图表数据路由
- api.route('/map', createMapRoutes(withAuth)) // 添加地图数据路由
- api.route('/settings', createSystemSettingsRoutes(withAuth)) // 添加系统设置路由
- // 注册API路由
- honoApp.route('/api', api)
-
- // 首页路由 - SSR
- honoApp.get('/', async (c: HonoContext) => {
- const systemName = GLOBAL_CONFIG.APP_NAME
- return c.html(
- <html>
- <head>
- <title>{systemName}</title>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <script src="https://cdn.tailwindcss.com"></script>
- </head>
- <body>
- <div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
- <div className="max-w-md w-full space-y-8">
- {/* 系统介绍区域 */}
- <div className="text-center">
- <h1 className="text-4xl font-bold text-gray-900 mb-4">
- {systemName}
- </h1>
- <p className="text-lg text-gray-600 mb-8">
- 全功能应用Starter
- </p>
- <p className="text-base text-gray-500 mb-8">
- 这是一个基于Hono和React的应用Starter,提供了用户认证、文件管理、图表分析、地图集成和主题切换等常用功能。
- </p>
- </div>
- {/* 管理入口按钮 */}
- <div>
- <a
- href="/admin"
- className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-lg font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
- >
- 进入管理后台
- </a>
- </div>
- </div>
- </div>
- </body>
- </html>
- )
- })
-
- // 创建一个函数,用于生成包含全局配置的HTML页面
- const createHtmlWithConfig = (scriptConfig: EsmScriptConfig, title = '应用Starter') => {
- return (c: HonoContext) => {
- const isProd = GLOBAL_CONFIG.ENV === 'production';
-
- return c.html(
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>{title}</title>
-
- {isProd ? (
- <script type="module" src={scriptConfig.prodSrc || `/asset_dist/${scriptConfig.prodPath}`}></script>
- ) : (
- <script src={scriptConfig.src} href={scriptConfig.href} deno-json={scriptConfig.denoJson} refresh={scriptConfig.refresh}></script>
- )}
-
- {isProd ? (<script src="/tailwindcss@3.4.16/index.js"></script>) : (<script src="https://cdn.tailwindcss.com"></script>)}
- <script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
-
- {!isProd && (
- <>
- <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
- <script dangerouslySetInnerHTML={{ __html: `
- const urlParams = new URLSearchParams(window.location.search);
- if (urlParams.has('vconsole')) {
- var vConsole = new VConsole({
- theme: urlParams.get('vconsole_theme') || 'light',
- onReady: function() {
- console.log('vConsole is ready');
- }
- });
- }
- `}} />
- </>
- )}
- </head>
- <body className="bg-gray-50">
- <div id="root"></div>
- </body>
- </html>
- )
- }
- }
- // 后台管理路由
- honoApp.get('/admin', createHtmlWithConfig({
- src: "https://esm.d8d.fun/xb",
- href: "/asset/admin/web_app.tsx",
- denoJson: "/asset/admin/deno.json",
- refresh: true,
- prodPath: "admin/web_app.js"
- }, GLOBAL_CONFIG.APP_NAME))
-
- honoApp.get('/admin/*', createHtmlWithConfig({
- src: "https://esm.d8d.fun/xb",
- href: "/asset/admin/web_app.tsx",
- denoJson: "/asset/admin/deno.json",
- refresh: true,
- prodPath: "admin/web_app.js"
- }, GLOBAL_CONFIG.APP_NAME))
- const staticRoutes = serveStatic({
- root: moduleDir,
- onFound: async (path: string, c: HonoContext) => {
- const fileExt = path.split('.').pop()?.toLowerCase()
- 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}`)
- }
- const fileInfo = await Deno.stat(path)
- c.header('Last-Modified', fileInfo.mtime?.toUTCString() ?? new Date().toUTCString())
- },
- })
-
- // 静态资源路由
- honoApp.get('/asset/*', staticRoutes)
- honoApp.get('/amap/*', staticRoutes)
- honoApp.get('/tailwindcss@3.4.16/*', staticRoutes)
- honoApp.get('/asset_dist/*', staticRoutes)
-
- return honoApp
- }
|