app.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. /** @jsxImportSource https://esm.d8d.fun/hono@4.7.4/jsx */
  2. import { Hono } from 'hono'
  3. import { Auth } from '@d8d-appcontainer/auth'
  4. import type { User as AuthUser } from '@d8d-appcontainer/auth'
  5. import React from 'hono/jsx'
  6. import type { FC } from 'hono/jsx'
  7. import { cors } from 'hono/cors'
  8. import type { Context as HonoContext } from 'hono'
  9. import { serveStatic } from 'hono/deno'
  10. import { APIClient } from '@d8d-appcontainer/api'
  11. import debug from "debug"
  12. import dayjs from 'dayjs';
  13. import utc from 'dayjs/plugin/utc';
  14. import type { SystemSettingRecord, GlobalConfig } from '../client/share/types.ts';
  15. import { SystemSettingKey, OssType, MapMode } from '../client/share/types.ts';
  16. import {
  17. createKnowInfoRoutes,
  18. createFileCategoryRoutes,
  19. createFileUploadRoutes,
  20. createThemeRoutes,
  21. createSystemSettingsRoutes,
  22. } from "./routes_sys.ts";
  23. import {
  24. createMapRoutes,
  25. } from "./routes_maps.ts";
  26. import {
  27. createChartRoutes,
  28. } from "./routes_charts.ts";
  29. import { migrations } from './migrations.ts';
  30. // 导入基础路由
  31. import { createAuthRoutes } from "./routes_auth.ts";
  32. import { createUserRoutes } from "./routes_users.ts";
  33. dayjs.extend(utc)
  34. // 初始化debug实例
  35. const log = {
  36. app: debug('app:server'),
  37. auth: debug('auth:server'),
  38. api: debug('api:server'),
  39. debug: debug('debug:server')
  40. }
  41. const GLOBAL_CONFIG: GlobalConfig = {
  42. OSS_BASE_URL: Deno.env.get('OSS_BASE_URL') || 'https://d8d-appcontainer-user.oss-cn-beijing.aliyuncs.com',
  43. OSS_TYPE: Deno.env.get('OSS_TYPE') === OssType.MINIO ? OssType.MINIO : OssType.ALIYUN,
  44. API_BASE_URL: '/api',
  45. APP_NAME: Deno.env.get('APP_NAME') || '应用Starter',
  46. ENV: Deno.env.get('ENV') || 'development',
  47. DEFAULT_THEME: 'light', // 默认主题
  48. MAP_CONFIG: {
  49. KEY: Deno.env.get('AMAP_KEY') || '您的地图API密钥',
  50. VERSION: '2.0',
  51. PLUGINS: ['AMap.ToolBar', 'AMap.Scale', 'AMap.HawkEye', 'AMap.MapType', 'AMap.Geolocation'],
  52. MAP_MODE: Deno.env.get('MAP_MODE') === MapMode.OFFLINE ? MapMode.OFFLINE : MapMode.ONLINE,
  53. },
  54. CHART_THEME: 'default', // 图表主题
  55. ENABLE_THEME_CONFIG: false, // 主题配置开关
  56. THEME: null
  57. };
  58. log.app.enabled = true
  59. log.auth.enabled = true
  60. log.api.enabled = true
  61. log.debug.enabled = true
  62. // 定义自定义上下文类型
  63. export interface Variables {
  64. auth: Auth
  65. user?: AuthUser
  66. apiClient: APIClient
  67. moduleDir: string
  68. systemSettings?: SystemSettingRecord
  69. }
  70. // 定义登录历史类型
  71. interface LoginHistory {
  72. id: number
  73. user_id: number
  74. login_time: string
  75. ip_address?: string
  76. user_agent?: string
  77. }
  78. // 定义仪表盘数据类型
  79. interface DashboardData {
  80. lastLogin: string
  81. loginCount: number
  82. fileCount: number
  83. userCount: number
  84. systemInfo: {
  85. version: string
  86. lastUpdate: string
  87. }
  88. }
  89. interface EsmScriptConfig {
  90. src: string
  91. href: string
  92. denoJson: string
  93. refresh: boolean
  94. prodPath?: string
  95. prodSrc?: string
  96. }
  97. // Auth实例
  98. let authInstance: Auth | null = null
  99. // 初始化Auth实例
  100. const initAuth = async (apiClient: APIClient) => {
  101. try {
  102. if (authInstance) {
  103. return authInstance
  104. }
  105. log.auth('正在初始化Auth实例')
  106. authInstance = new Auth(apiClient as any, {
  107. jwtSecret: Deno.env.get("JWT_SECRET") || 'your-jwt-secret-key',
  108. initialUsers: [],
  109. storagePrefix: '',
  110. userTable: 'users',
  111. fieldNames: {
  112. id: 'id',
  113. username: 'username',
  114. password: 'password',
  115. phone: 'phone',
  116. email: 'email',
  117. is_disabled: 'is_disabled',
  118. is_deleted: 'is_deleted'
  119. },
  120. tokenExpiry: 24 * 60 * 60,
  121. refreshTokenExpiry: 7 * 24 * 60 * 60
  122. })
  123. log.auth('Auth实例初始化完成')
  124. return authInstance
  125. } catch (error) {
  126. log.auth('Auth初始化失败:', error)
  127. throw error
  128. }
  129. }
  130. // 初始化系统设置
  131. const initSystemSettings = async (apiClient: APIClient) => {
  132. try {
  133. const systemSettings = await apiClient.database.table('system_settings')
  134. .select()
  135. // 将系统设置转换为键值对形式
  136. const settings = systemSettings.reduce((acc: Record<string, any>, setting: any) => {
  137. acc[setting.key] = setting.value
  138. return acc
  139. }, {}) as SystemSettingRecord
  140. // 更新全局配置
  141. if (settings[SystemSettingKey.SITE_NAME]) {
  142. GLOBAL_CONFIG.APP_NAME = String(settings[SystemSettingKey.SITE_NAME])
  143. }
  144. // 设置其他全局配置项
  145. if (settings[SystemSettingKey.SITE_FAVICON]) {
  146. GLOBAL_CONFIG.DEFAULT_THEME = String(settings[SystemSettingKey.SITE_FAVICON])
  147. }
  148. if (settings[SystemSettingKey.SITE_LOGO]) {
  149. GLOBAL_CONFIG.MAP_CONFIG.KEY = String(settings[SystemSettingKey.SITE_LOGO])
  150. }
  151. if (settings[SystemSettingKey.SITE_DESCRIPTION]) {
  152. GLOBAL_CONFIG.CHART_THEME = String(settings[SystemSettingKey.SITE_DESCRIPTION])
  153. }
  154. // 设置主题配置开关
  155. if (settings[SystemSettingKey.ENABLE_THEME_CONFIG]) {
  156. GLOBAL_CONFIG.ENABLE_THEME_CONFIG = settings[SystemSettingKey.ENABLE_THEME_CONFIG] === 'true'
  157. }
  158. // 查询ID1管理员的主题配置
  159. const adminTheme = await apiClient.database.table('theme_settings')
  160. .where('user_id', 1)
  161. .first()
  162. if (adminTheme) {
  163. GLOBAL_CONFIG.THEME = adminTheme.settings
  164. }
  165. return settings
  166. } catch (error) {
  167. log.app('获取系统设置失败:', error)
  168. return {} as SystemSettingRecord
  169. }
  170. }
  171. // 初始化数据库
  172. const initDatabase = async (apiClient: APIClient) => {
  173. try {
  174. log.app('正在执行数据库迁移...')
  175. const migrationsResult = await apiClient.database.executeLiveMigrations(migrations)
  176. // log.app('数据库迁移完成 %O',migrationsResult)
  177. log.app('数据库迁移完成')
  178. } catch (error) {
  179. log.app('数据库迁移失败:', error)
  180. }
  181. }
  182. // 中间件:数据库初始化
  183. const withDatabase = async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
  184. try {
  185. const apiClient = c.get('apiClient')
  186. await initDatabase(apiClient)
  187. await next()
  188. } catch (error) {
  189. log.api('数据库操作失败:', error)
  190. return c.json({ error: '数据库操作失败' }, 500)
  191. }
  192. }
  193. // 中间件:验证认证
  194. const withAuth = async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
  195. try {
  196. const auth = c.get('auth')
  197. const token = c.req.header('Authorization')?.replace('Bearer ', '')
  198. if (token) {
  199. const userData = await auth.verifyToken(token)
  200. if (userData) {
  201. c.set('user', userData)
  202. await next()
  203. return
  204. }
  205. }
  206. return c.json({ error: '未授权' }, 401)
  207. } catch (error) {
  208. log.auth('认证失败:', error)
  209. return c.json({ error: '无效凭证' }, 401)
  210. }
  211. }
  212. // 导出withAuth类型定义
  213. export type WithAuth = typeof withAuth;
  214. // 定义模块参数接口
  215. interface ModuleParams {
  216. apiClient: APIClient
  217. app: Hono<{ Variables: Variables }>
  218. moduleDir: string
  219. }
  220. export default function({ apiClient, app, moduleDir }: ModuleParams) {
  221. const honoApp = app
  222. // 添加CORS中间件
  223. honoApp.use('/*', cors())
  224. // 创建API路由
  225. const api = new Hono<{ Variables: Variables }>()
  226. // 设置环境变量
  227. api.use('*', async (c, next) => {
  228. c.set('apiClient', apiClient)
  229. c.set('moduleDir', moduleDir)
  230. c.set('auth', await initAuth(apiClient))
  231. c.set('systemSettings', await initSystemSettings(apiClient))
  232. await next()
  233. })
  234. // 使用数据库中间件
  235. api.use('/', withDatabase)
  236. // 查询仪表盘数据
  237. api.get('/dashboard', withAuth, async (c) => {
  238. try {
  239. const user = c.get('user')!
  240. const apiClient = c.get('apiClient')
  241. const lastLogin = await apiClient.database.table('login_history')
  242. .where('user_id', user.id)
  243. .orderBy('login_time', 'desc')
  244. .limit(1)
  245. .first()
  246. // 获取登录总次数
  247. const loginCount = await apiClient.database.table('login_history')
  248. .where('user_id', user.id)
  249. .count()
  250. // 获取系统数据统计
  251. const fileCount = await apiClient.database.table('file_library')
  252. .where('is_deleted', 0)
  253. .count()
  254. const userCount = await apiClient.database.table('users')
  255. .where('is_deleted', 0)
  256. .count()
  257. // 返回仪表盘数据
  258. const dashboardData: DashboardData = {
  259. lastLogin: lastLogin ? lastLogin.login_time : new Date().toISOString(),
  260. loginCount: loginCount,
  261. fileCount: Number(fileCount),
  262. userCount: Number(userCount),
  263. systemInfo: {
  264. version: '1.0.0',
  265. lastUpdate: new Date().toISOString()
  266. }
  267. }
  268. return c.json(dashboardData)
  269. } catch (error) {
  270. log.api('获取仪表盘数据失败:', error)
  271. return c.json({ error: '获取仪表盘数据失败' }, 500)
  272. }
  273. })
  274. // 注册基础路由
  275. api.route('/auth', createAuthRoutes(withAuth))
  276. api.route('/users', createUserRoutes(withAuth))
  277. api.route('/know-info', createKnowInfoRoutes(withAuth))
  278. api.route('/upload', createFileUploadRoutes(withAuth)) // 添加文件上传路由
  279. api.route('/file-categories', createFileCategoryRoutes(withAuth)) // 添加文件分类管理路由
  280. api.route('/theme', createThemeRoutes(withAuth)) // 添加主题设置路由
  281. api.route('/charts', createChartRoutes(withAuth)) // 添加图表数据路由
  282. api.route('/map', createMapRoutes(withAuth)) // 添加地图数据路由
  283. api.route('/settings', createSystemSettingsRoutes(withAuth)) // 添加系统设置路由
  284. // 注册API路由
  285. honoApp.route('/api', api)
  286. // 首页路由 - SSR
  287. honoApp.get('/', async (c: HonoContext) => {
  288. const systemName = GLOBAL_CONFIG.APP_NAME
  289. return c.html(
  290. <html>
  291. <head>
  292. <title>{systemName}</title>
  293. <meta charset="UTF-8" />
  294. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  295. <script src="https://cdn.tailwindcss.com"></script>
  296. </head>
  297. <body>
  298. <div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
  299. <div className="max-w-md w-full space-y-8">
  300. {/* 系统介绍区域 */}
  301. <div className="text-center">
  302. <h1 className="text-4xl font-bold text-gray-900 mb-4">
  303. {systemName}
  304. </h1>
  305. <p className="text-lg text-gray-600 mb-8">
  306. 全功能应用Starter
  307. </p>
  308. <p className="text-base text-gray-500 mb-8">
  309. 这是一个基于Hono和React的应用Starter,提供了用户认证、文件管理、图表分析、地图集成和主题切换等常用功能。
  310. </p>
  311. </div>
  312. {/* 管理入口按钮 */}
  313. <div>
  314. <a
  315. href="/admin"
  316. 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"
  317. >
  318. 进入管理后台
  319. </a>
  320. </div>
  321. </div>
  322. </div>
  323. </body>
  324. </html>
  325. )
  326. })
  327. // 创建一个函数,用于生成包含全局配置的HTML页面
  328. const createHtmlWithConfig = (scriptConfig: EsmScriptConfig, title = '应用Starter') => {
  329. return (c: HonoContext) => {
  330. const isProd = GLOBAL_CONFIG.ENV === 'production';
  331. return c.html(
  332. <html lang="zh-CN">
  333. <head>
  334. <meta charset="UTF-8" />
  335. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  336. <title>{title}</title>
  337. {isProd ? (
  338. <script type="module" src={scriptConfig.prodSrc || `/client_dist/${scriptConfig.prodPath}`}></script>
  339. ) : (
  340. <script src={scriptConfig.src} href={scriptConfig.href} deno-json={scriptConfig.denoJson} refresh={scriptConfig.refresh}></script>
  341. )}
  342. {isProd ? (<script src="/tailwindcss@3.4.16/index.js"></script>) : (<script src="https://cdn.tailwindcss.com"></script>)}
  343. <script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
  344. {!isProd && (
  345. <>
  346. <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
  347. <script dangerouslySetInnerHTML={{ __html: `
  348. const urlParams = new URLSearchParams(window.location.search);
  349. if (urlParams.has('vconsole')) {
  350. var vConsole = new VConsole({
  351. theme: urlParams.get('vconsole_theme') || 'light',
  352. onReady: function() {
  353. console.log('vConsole is ready');
  354. }
  355. });
  356. }
  357. `}} />
  358. </>
  359. )}
  360. </head>
  361. <body className="bg-gray-50">
  362. <div id="root"></div>
  363. </body>
  364. </html>
  365. )
  366. }
  367. }
  368. // 后台管理路由
  369. honoApp.get('/admin', createHtmlWithConfig({
  370. src: "https://esm.d8d.fun/xb",
  371. href: "/client/admin/web_app.tsx",
  372. denoJson: "/client/admin/deno.json",
  373. refresh: true,
  374. prodPath: "admin/web_app.js"
  375. }, GLOBAL_CONFIG.APP_NAME))
  376. honoApp.get('/admin/*', createHtmlWithConfig({
  377. src: "https://esm.d8d.fun/xb",
  378. href: "/client/admin/web_app.tsx",
  379. denoJson: "/client/admin/deno.json",
  380. refresh: true,
  381. prodPath: "admin/web_app.js"
  382. }, GLOBAL_CONFIG.APP_NAME))
  383. const staticRoutes = serveStatic({
  384. root: moduleDir,
  385. onFound: async (path: string, c: HonoContext) => {
  386. const fileExt = path.split('.').pop()?.toLowerCase()
  387. if (fileExt === 'tsx' || fileExt === 'ts') {
  388. c.header('Content-Type', 'text/typescript; charset=utf-8')
  389. } else if (fileExt === 'js' || fileExt === 'mjs') {
  390. c.header('Content-Type', 'application/javascript; charset=utf-8')
  391. } else if (fileExt === 'json') {
  392. c.header('Content-Type', 'application/json; charset=utf-8')
  393. } else if (fileExt === 'html') {
  394. c.header('Content-Type', 'text/html; charset=utf-8')
  395. } else if (fileExt === 'css') {
  396. c.header('Content-Type', 'text/css; charset=utf-8')
  397. } else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExt || '')) {
  398. c.header('Content-Type', `image/${fileExt}`)
  399. }
  400. const fileInfo = await Deno.stat(path)
  401. c.header('Last-Modified', fileInfo.mtime?.toUTCString() ?? new Date().toUTCString())
  402. },
  403. })
  404. // 静态资源路由
  405. honoApp.get('/client/*', staticRoutes)
  406. honoApp.get('/amap/*', staticRoutes)
  407. honoApp.get('/tailwindcss@3.4.16/*', staticRoutes)
  408. honoApp.get('/client_dist/*', staticRoutes)
  409. return honoApp
  410. }