create-bubbles 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +5 -0
- package/index.js +1 -1
- package/package.json +25 -24
- package/template-taro-react-oxc/.env.development +1 -1
- package/template-taro-react-oxc/.env.production +1 -1
- package/template-taro-react-oxc/project.config.json +1 -1
- package/template-taro-vue-eslint/project.config.json +2 -2
- package/{template-vp-monorepo-react-nestjs → template-vp-monorepo-react-hono}/.spaces/server//347/231/273/345/275/225/JWT/344/270/216RefreshToken/350/275/256/346/215/242/351/211/264/346/235/203/346/236/266/346/236/204/345/256/236/347/216/260/346/214/207/345/215/227.md +588 -1133
- package/template-vp-monorepo-react-hono/.vite-hooks/commit-msg +2 -0
- package/template-vp-monorepo-react-hono/.vite-hooks/pre-commit +2 -0
- package/template-vp-monorepo-react-hono/.vscode/extensions.json +3 -0
- package/template-vp-monorepo-react-hono/.vscode/settings.json +23 -0
- package/template-vp-monorepo-react-hono/AGENTS.md +16 -0
- package/template-vp-monorepo-react-hono/README.md +54 -0
- package/template-vp-monorepo-react-hono/apps/server/.env.example +13 -0
- package/template-vp-monorepo-react-hono/apps/server/README.md +75 -0
- package/template-vp-monorepo-react-hono/apps/server/drizzle/0000_flaky_madame_masque.sql +18 -0
- package/template-vp-monorepo-react-hono/apps/server/drizzle/meta/0000_snapshot.json +153 -0
- package/template-vp-monorepo-react-hono/apps/server/drizzle/meta/_journal.json +13 -0
- package/template-vp-monorepo-react-hono/apps/server/drizzle.config.ts +19 -0
- package/template-vp-monorepo-react-hono/apps/server/package.json +46 -0
- package/template-vp-monorepo-react-hono/apps/server/src/app.test.ts +57 -0
- package/template-vp-monorepo-react-hono/apps/server/src/app.ts +68 -0
- package/template-vp-monorepo-react-hono/apps/server/src/common/errors/app-error.ts +23 -0
- package/template-vp-monorepo-react-hono/apps/server/src/common/http/response.ts +21 -0
- package/template-vp-monorepo-react-hono/apps/server/src/common/logger.ts +30 -0
- package/template-vp-monorepo-react-hono/apps/server/src/config/env.ts +54 -0
- package/template-vp-monorepo-react-hono/apps/server/src/index.bun.ts +60 -0
- package/template-vp-monorepo-react-hono/apps/server/src/index.ts +73 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/database/client.ts +46 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/database/schema/index.ts +1 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/database/schema/job-runs.ts +42 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/queue/connection.ts +6 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/redis/client.ts +39 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/redis/options.ts +28 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/health/health.routes.ts +17 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/health/health.service.ts +59 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.queue.ts +46 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.repository.ts +101 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.routes.ts +46 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.schemas.ts +9 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.service.ts +57 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.types.ts +11 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.worker.ts +108 -0
- package/template-vp-monorepo-react-hono/apps/server/src/runtime/shutdown.ts +38 -0
- package/template-vp-monorepo-react-hono/apps/server/src/worker.ts +48 -0
- package/template-vp-monorepo-react-hono/apps/server/tsconfig.json +15 -0
- package/template-vp-monorepo-react-hono/apps/server/vite.config.ts +15 -0
- package/template-vp-monorepo-react-hono/apps/web/.env +2 -0
- package/template-vp-monorepo-react-hono/apps/web/.env.dev +1 -0
- package/template-vp-monorepo-react-hono/apps/web/.env.prod +1 -0
- package/template-vp-monorepo-react-hono/apps/web/index.html +13 -0
- package/template-vp-monorepo-react-hono/apps/web/package.json +35 -0
- package/template-vp-monorepo-react-hono/apps/web/public/favicon.svg +1 -0
- package/template-vp-monorepo-react-hono/apps/web/public/icons.svg +24 -0
- package/template-vp-monorepo-react-hono/apps/web/src/App.tsx +25 -0
- package/template-vp-monorepo-react-hono/apps/web/src/assets/hero.png +0 -0
- package/template-vp-monorepo-react-hono/apps/web/src/assets/react.svg +1 -0
- package/template-vp-monorepo-react-hono/apps/web/src/assets/vite.svg +1 -0
- package/template-vp-monorepo-react-hono/apps/web/src/components/Loading/PageLoading.tsx +11 -0
- package/template-vp-monorepo-react-hono/apps/web/src/locales/en_US.json +4 -0
- package/template-vp-monorepo-react-hono/apps/web/src/locales/zh_CN.json +4 -0
- package/template-vp-monorepo-react-hono/apps/web/src/main.tsx +5 -0
- package/template-vp-monorepo-react-hono/apps/web/src/pages/home/index.tsx +47 -0
- package/template-vp-monorepo-react-hono/apps/web/src/router/index.tsx +25 -0
- package/template-vp-monorepo-react-hono/apps/web/src/router/modules/.gitkeep +0 -0
- package/template-vp-monorepo-react-hono/apps/web/src/styles/index.css +7 -0
- package/template-vp-monorepo-react-hono/apps/web/src/types/auto-imports.d.ts +58 -0
- package/template-vp-monorepo-react-hono/apps/web/src/utils/env/index.ts +6 -0
- package/template-vp-monorepo-react-hono/apps/web/src/utils/request/core/index.ts +420 -0
- package/template-vp-monorepo-react-hono/apps/web/src/utils/request/core/utils.ts +39 -0
- package/template-vp-monorepo-react-hono/apps/web/src/utils/request/index.ts +61 -0
- package/template-vp-monorepo-react-hono/apps/web/tsconfig.json +18 -0
- package/template-vp-monorepo-react-hono/apps/web/vite.config.ts +45 -0
- package/template-vp-monorepo-react-hono/commitlint.config.js +1 -0
- package/template-vp-monorepo-react-hono/docker-compose.yml +41 -0
- package/template-vp-monorepo-react-hono/mise.toml +4 -0
- package/template-vp-monorepo-react-hono/package.json +37 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/package.json +20 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/format.ts +9 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/i18n.ts +73 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/index.ts +24 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/storage.ts +47 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/store.ts +48 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/types.ts +36 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/tsconfig.json +4 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/vite.config.ts +13 -0
- package/template-vp-monorepo-react-hono/packages/i18n/react/package.json +28 -0
- package/template-vp-monorepo-react-hono/packages/i18n/react/src/index.tsx +26 -0
- package/template-vp-monorepo-react-hono/packages/i18n/react/src/use-store.ts +14 -0
- package/template-vp-monorepo-react-hono/packages/i18n/react/tsconfig.json +9 -0
- package/template-vp-monorepo-react-hono/packages/i18n/react/vite.config.ts +16 -0
- package/template-vp-monorepo-react-hono/packages/i18n/vue/package.json +27 -0
- package/template-vp-monorepo-react-hono/packages/i18n/vue/src/index.ts +52 -0
- package/template-vp-monorepo-react-hono/packages/i18n/vue/src/use-store.ts +19 -0
- package/template-vp-monorepo-react-hono/packages/i18n/vue/tsconfig.json +8 -0
- package/template-vp-monorepo-react-hono/packages/i18n/vue/vite.config.ts +16 -0
- package/template-vp-monorepo-react-hono/pnpm-workspace.yaml +57 -0
- package/template-vp-monorepo-react-hono/scripts/clean.mjs +48 -0
- package/template-vp-monorepo-react-hono/tsconfig.base.json +10 -0
- package/template-vp-monorepo-react-hono/tsconfig.bundler.json +12 -0
- package/template-vp-monorepo-react-hono/tsconfig.json +11 -0
- package/template-vp-monorepo-react-hono/tsconfig.lib.json +10 -0
- package/template-vp-monorepo-react-hono/vite.config.ts +20 -0
- package/template-vp-monorepo-react-nestjs/.spaces/server/todo.md +991 -0
- package/template-vp-monorepo-react-nestjs/.vscode/launch.json +16 -0
- package/template-vp-monorepo-react-nestjs/.vscode/settings.json +21 -17
- package/template-vp-monorepo-react-nestjs/apps/server/.env.development +3 -39
- package/template-vp-monorepo-react-nestjs/apps/server/.swcrc +8 -0
- package/template-vp-monorepo-react-nestjs/apps/server/drizzle.config.ts +8 -3
- package/template-vp-monorepo-react-nestjs/apps/server/nest-cli.json +8 -1
- package/template-vp-monorepo-react-nestjs/apps/server/package.json +11 -6
- package/template-vp-monorepo-react-nestjs/apps/server/src/app.controller.ts +2 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/app.module.ts +20 -15
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/adapters/fastify.adapter.ts +16 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/auth.ts +2 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/response.ts +0 -4
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/session.constants.ts +28 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/current-auth.decorator.ts +16 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/public.decorator.ts +4 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/dto/api-failure.dto.ts +25 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/error/common.error.ts +40 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/exceptions/app.exception.ts +55 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/filters/global-exception.filter.ts +384 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/openapi/response-headers.ts +9 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/config/index.ts +1 -1
- package/template-vp-monorepo-react-nestjs/apps/server/src/config/session.config.ts +36 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/database/schema.ts +7 -8
- package/template-vp-monorepo-react-nestjs/apps/server/src/main.ts +29 -8
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.controller.ts +55 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.errors.ts +42 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.module.ts +26 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.repository.ts +40 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.service.ts +129 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/auth-repsponse.dto.ts +31 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/login.dto.ts +11 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/register.dto.ts +17 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/guards/session-auth.guard.ts +50 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/password.service.ts +24 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session-store.service.ts +198 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session-token.service.ts +29 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session.script.ts +221 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session.types.ts +24 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/db/db.controller.ts +1 -1
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/db/dto/create-user.dto.ts +3 -5
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/redis/redis.service.ts +1 -1
- package/template-vp-monorepo-react-nestjs/apps/server/src/utils/env-arr.ts +3 -0
- package/template-vp-monorepo-react-nestjs/apps/server/test/error-catalog.spec.ts +30 -0
- package/template-vp-monorepo-react-nestjs/apps/server/tsconfig.build.json +11 -1
- package/template-vp-monorepo-react-nestjs/apps/server/tsconfig.json +12 -20
- package/template-vp-monorepo-react-nestjs/apps/server/vite.config.ts +15 -0
- package/template-vp-monorepo-react-nestjs/apps/web/package.json +1 -0
- package/template-vp-monorepo-react-nestjs/apps/web/tsconfig.json +5 -15
- package/template-vp-monorepo-react-nestjs/apps/web-vue/commitlint.config.js +1 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/lefthook.yml +1 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/package.json +1 -3
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/api/index.ts +5 -5
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/components/Icon/svg-icon.vue +3 -3
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/hooks/chart/lib.ts +6 -6
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/hooks/chart/useEcharts.ts +34 -29
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/i18n/instance.ts +2 -0
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/layout/default/index.vue +17 -32
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/locales/en_US.json +4 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/locales/zh_CN.json +4 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/guard/index.ts +3 -3
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/guard/permissionGuard.ts +7 -7
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/index.ts +7 -7
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/interface.ts +6 -6
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/store/index.ts +6 -6
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/store/modules/user.ts +9 -9
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/styles/index.scss +3 -3
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/types/antdv-next.d.ts +1 -0
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/env.ts +1 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/core/index.ts +63 -45
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/core/utils.ts +12 -6
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/index.ts +1 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/example/echart/config.ts +687 -687
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/example/echart/index.vue +7 -7
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/example/tree-chart.vue +2 -7
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/home/index.vue +13 -3
- package/template-vp-monorepo-react-nestjs/apps/web-vue/tsconfig.json +5 -17
- package/template-vp-monorepo-react-nestjs/apps/web-vue/uno.config.ts +5 -5
- package/template-vp-monorepo-react-nestjs/i18n.config.ts +22 -0
- package/template-vp-monorepo-react-nestjs/package.json +12 -1
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/README.md +113 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/docs/excel-sync-plan.md +618 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/package.json +31 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/cli.ts +5 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/command.ts +512 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/config.ts +15 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/files.ts +67 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/index.ts +19 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/load-config.ts +228 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/report.ts +57 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/scanner.test.ts +81 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/scanner.ts +290 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/sync.ts +274 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/command.test.ts +209 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/config.test.ts +19 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/load-config.test.ts +194 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/report.test.ts +63 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/sync.test.ts +203 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/temporary-directory.ts +25 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/tsconfig.json +9 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/vite.config.ts +18 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/core/tsconfig.json +1 -12
- package/template-vp-monorepo-react-nestjs/packages/i18n/react/tsconfig.json +3 -10
- package/template-vp-monorepo-react-nestjs/packages/i18n/vue/tsconfig.json +3 -10
- package/template-vp-monorepo-react-nestjs/packages/shared/package.json +40 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/index.ts +13 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/types/auth.ts +35 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/types/common.ts +11 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/types/index.ts +11 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/account.ts +5 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/index.ts +2 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/session-terminal.ts +11 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/tsconfig.json +4 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/vite.config.ts +17 -0
- package/template-vp-monorepo-react-nestjs/pnpm-workspace.yaml +6 -6
- package/template-vp-monorepo-react-nestjs/scripts/clean.mjs +48 -0
- package/template-vp-monorepo-react-nestjs/tsconfig.base.json +10 -0
- package/template-vp-monorepo-react-nestjs/tsconfig.bundler.json +12 -0
- package/template-vp-monorepo-react-nestjs/tsconfig.json +8 -6
- package/template-vp-monorepo-react-nestjs/tsconfig.lib.json +10 -0
- package/template-vp-monorepo-react-nestjs/vite.config.ts +2 -1
- package/template-vp-vue-eslint-vapor/.env +6 -0
- package/template-vp-vue-eslint-vapor/.env.development +2 -0
- package/template-vp-vue-eslint-vapor/.env.production +2 -0
- package/template-vp-vue-eslint-vapor/.vite-hooks/commit-msg +1 -0
- package/template-vp-vue-eslint-vapor/.vite-hooks/pre-commit +1 -0
- package/template-vp-vue-eslint-vapor/.vscode/extensions.json +3 -0
- package/template-vp-vue-eslint-vapor/AGENTS.md +16 -0
- package/template-vp-vue-eslint-vapor/README.md +93 -0
- package/template-vp-vue-eslint-vapor/commitlint.config.js +1 -0
- package/template-vp-vue-eslint-vapor/eslint.config.js +13 -0
- package/template-vp-vue-eslint-vapor/index.html +13 -0
- package/template-vp-vue-eslint-vapor/package.json +64 -0
- package/template-vp-vue-eslint-vapor/pnpm-workspace.yaml +2 -0
- package/template-vp-vue-eslint-vapor/public/vite.svg +1 -0
- package/template-vp-vue-eslint-vapor/src/App.vue +7 -0
- package/template-vp-vue-eslint-vapor/src/api/index.ts +12 -0
- package/template-vp-vue-eslint-vapor/src/assets/icon/home.svg +3 -0
- package/template-vp-vue-eslint-vapor/src/assets/icon/logo.svg +1 -0
- package/template-vp-vue-eslint-vapor/src/assets/image/.gitkeep +0 -0
- package/template-vp-vue-eslint-vapor/src/components/Icon/svg-icon.vue +29 -0
- package/template-vp-vue-eslint-vapor/src/hooks/chart/lib.ts +57 -0
- package/template-vp-vue-eslint-vapor/src/hooks/chart/useEcharts.ts +65 -0
- package/template-vp-vue-eslint-vapor/src/layout/default/header/index.vue +57 -0
- package/template-vp-vue-eslint-vapor/src/layout/default/index.vue +138 -0
- package/template-vp-vue-eslint-vapor/src/main.ts +19 -0
- package/template-vp-vue-eslint-vapor/src/router/guard/index.ts +7 -0
- package/template-vp-vue-eslint-vapor/src/router/guard/permissionGuard.ts +13 -0
- package/template-vp-vue-eslint-vapor/src/router/index.ts +18 -0
- package/template-vp-vue-eslint-vapor/src/router/interface.ts +9 -0
- package/template-vp-vue-eslint-vapor/src/router/modules/index.ts +33 -0
- package/template-vp-vue-eslint-vapor/src/store/index.ts +11 -0
- package/template-vp-vue-eslint-vapor/src/store/modules/user.ts +17 -0
- package/template-vp-vue-eslint-vapor/src/styles/font.scss +0 -0
- package/template-vp-vue-eslint-vapor/src/styles/index.scss +29 -0
- package/template-vp-vue-eslint-vapor/src/styles/variables.scss +3 -0
- package/template-vp-vue-eslint-vapor/src/types/vite-env.d.ts +15 -0
- package/template-vp-vue-eslint-vapor/src/utils/env.ts +11 -0
- package/template-vp-vue-eslint-vapor/src/utils/request/core/index.ts +425 -0
- package/template-vp-vue-eslint-vapor/src/utils/request/core/utils.ts +39 -0
- package/template-vp-vue-eslint-vapor/src/utils/request/index.ts +44 -0
- package/template-vp-vue-eslint-vapor/src/views/example/echart/config.ts +1794 -0
- package/template-vp-vue-eslint-vapor/src/views/example/echart/index.vue +20 -0
- package/template-vp-vue-eslint-vapor/src/views/example/tree-chart.vue +16 -0
- package/template-vp-vue-eslint-vapor/src/views/home/index.vue +16 -0
- package/template-vp-vue-eslint-vapor/src/views/login/index.vue +9 -0
- package/template-vp-vue-eslint-vapor/tsconfig.json +32 -0
- package/template-vp-vue-eslint-vapor/tsconfig.tsbuildinfo +1 -0
- package/template-vp-vue-eslint-vapor/uno.config.ts +17 -0
- package/template-vp-vue-eslint-vapor/vite.config.ts +66 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/decorator.ts +0 -2
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/bypass.ts +0 -12
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/interceptors/transform.ts +0 -34
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/model/response.ts +0 -23
- package/template-vp-monorepo-react-nestjs/apps/server/src/config/auth.config.ts +0 -64
- package/template-vp-monorepo-react-nestjs/apps/web-vue/pnpm-workspace.yaml +0 -16
- /package/{template-vp-monorepo-react-nestjs/apps/server/src/config/security.config.ts → template-vp-monorepo-react-hono/.agents/auth.config.ts} +0 -0
package/template-vp-monorepo-react-nestjs/apps/server/src/common/filters/global-exception.filter.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArgumentsHost,
|
|
3
|
+
Catch,
|
|
4
|
+
ExceptionFilter,
|
|
5
|
+
HttpException,
|
|
6
|
+
HttpStatus,
|
|
7
|
+
Logger,
|
|
8
|
+
} from '@nestjs/common'
|
|
9
|
+
import { HttpAdapterHost } from '@nestjs/core'
|
|
10
|
+
import { FastifyReply, FastifyRequest } from 'fastify'
|
|
11
|
+
import { ZodSerializationException, ZodValidationException } from 'nestjs-zod'
|
|
12
|
+
import { ApiErrorDetail, ApiFailure } from 'shared/types'
|
|
13
|
+
import { ZodError } from 'zod'
|
|
14
|
+
import { COMMON_ERRORS } from '../error/common.error'
|
|
15
|
+
import { AppException } from '../exceptions/app.exception'
|
|
16
|
+
|
|
17
|
+
interface PublicHttpError {
|
|
18
|
+
readonly code: string
|
|
19
|
+
readonly message: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MAX_DETAIL_COUNT = 20
|
|
23
|
+
const MAX_DETAIL_PATH_LENGTH = 200
|
|
24
|
+
/**
|
|
25
|
+
* 错误响应体的 details[number].code 的最长长度限制
|
|
26
|
+
*/
|
|
27
|
+
const MAX_DETAIL_CODE_LENGTH = 80
|
|
28
|
+
const MAX_PUBLIC_MESSAGE_LENGTH = 300
|
|
29
|
+
const MAX_LOG_MESSAGE_LENGTH = 1000
|
|
30
|
+
const MAX_LOG_FRAME_LENGTH = 600
|
|
31
|
+
const MAX_LOG_STACK_FRAMES = 12
|
|
32
|
+
|
|
33
|
+
interface NormalizedFailure {
|
|
34
|
+
readonly status: number
|
|
35
|
+
readonly body: ApiFailure
|
|
36
|
+
readonly cause?: unknown
|
|
37
|
+
readonly bearerChallenge?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PUBLIC_HTTP_ERRORS: Readonly<Record<number, PublicHttpError>> = {
|
|
41
|
+
[HttpStatus.BAD_REQUEST]: {
|
|
42
|
+
code: 'COMMON.BAD_REQUEST',
|
|
43
|
+
message: '请求内容无效',
|
|
44
|
+
},
|
|
45
|
+
[HttpStatus.UNAUTHORIZED]: {
|
|
46
|
+
code: 'COMMON.UNAUTHORIZED',
|
|
47
|
+
message: '身份认证失败',
|
|
48
|
+
},
|
|
49
|
+
[HttpStatus.FORBIDDEN]: {
|
|
50
|
+
code: 'COMMON.FORBIDDEN',
|
|
51
|
+
message: '没有权限执行此操作',
|
|
52
|
+
},
|
|
53
|
+
[HttpStatus.NOT_FOUND]: {
|
|
54
|
+
code: 'COMMON.NOT_FOUND',
|
|
55
|
+
message: '请求的资源不存在',
|
|
56
|
+
},
|
|
57
|
+
[HttpStatus.METHOD_NOT_ALLOWED]: {
|
|
58
|
+
code: 'COMMON.METHOD_NOT_ALLOWED',
|
|
59
|
+
message: '请求方法不受支持',
|
|
60
|
+
},
|
|
61
|
+
[HttpStatus.CONFLICT]: {
|
|
62
|
+
code: 'COMMON.CONFLICT',
|
|
63
|
+
message: '当前资源状态存在冲突',
|
|
64
|
+
},
|
|
65
|
+
[HttpStatus.PAYLOAD_TOO_LARGE]: {
|
|
66
|
+
code: 'COMMON.PAYLOAD_TOO_LARGE',
|
|
67
|
+
message: '请求内容过大',
|
|
68
|
+
},
|
|
69
|
+
[HttpStatus.UNSUPPORTED_MEDIA_TYPE]: {
|
|
70
|
+
code: 'COMMON.UNSUPPORTED_MEDIA_TYPE',
|
|
71
|
+
message: '请求内容类型不受支持',
|
|
72
|
+
},
|
|
73
|
+
[HttpStatus.UNPROCESSABLE_ENTITY]: {
|
|
74
|
+
code: 'COMMON.UNPROCESSABLE_CONTENT',
|
|
75
|
+
message: '请求内容无法处理',
|
|
76
|
+
},
|
|
77
|
+
[HttpStatus.TOO_MANY_REQUESTS]: {
|
|
78
|
+
code: 'COMMON.TOO_MANY_REQUESTS',
|
|
79
|
+
message: '请求过于频繁,请稍后重试',
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cleanPublicText(value: unknown, maxLength: number): string {
|
|
84
|
+
if (typeof value !== 'string') {
|
|
85
|
+
return ''
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 这段代码是在把字符串中的“不可见控制字符和换行符”全部替换成空格:
|
|
89
|
+
// \u0000-\u001F:ASCII 控制字符,包括换行 \n、回车 \r、Tab 等。
|
|
90
|
+
// \u007F-\u009F:其他不可见控制字符。
|
|
91
|
+
// \u2028:Unicode 行分隔符。
|
|
92
|
+
// \u2029:Unicode 段落分隔符。
|
|
93
|
+
return (
|
|
94
|
+
value
|
|
95
|
+
// oxlint-disable-next-line no-control-regex
|
|
96
|
+
.replace(/[\u0000-\u001F\u007F-\u009F\u2028\u2029]/g, ' ')
|
|
97
|
+
.trim()
|
|
98
|
+
.slice(0, maxLength)
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cleanDetailCode(value: unknown): string {
|
|
103
|
+
const code = cleanPublicText(value, MAX_DETAIL_CODE_LENGTH)
|
|
104
|
+
if (/^[A-Za-z][A-Za-z0-9._-]*$/.test(code)) {
|
|
105
|
+
return code
|
|
106
|
+
}
|
|
107
|
+
return 'invalid'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeDetails(
|
|
111
|
+
details: readonly ApiErrorDetail[] | undefined,
|
|
112
|
+
): ApiErrorDetail[] | undefined {
|
|
113
|
+
const normalized = details?.slice(0, MAX_DETAIL_COUNT).map((detail) => {
|
|
114
|
+
const path = cleanPublicText(detail.path, MAX_DETAIL_PATH_LENGTH)
|
|
115
|
+
const message = cleanPublicText(detail.message, MAX_PUBLIC_MESSAGE_LENGTH) || '请求内容无效'
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...(path ? { path } : {}),
|
|
119
|
+
code: cleanDetailCode(detail.code),
|
|
120
|
+
message,
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
return normalized?.length ? normalized : undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeStatus(value: unknown): number {
|
|
127
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
128
|
+
return HttpStatus.INTERNAL_SERVER_ERROR
|
|
129
|
+
}
|
|
130
|
+
if (value < 400 || value > 599) {
|
|
131
|
+
return HttpStatus.INTERNAL_SERVER_ERROR
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return value
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 对日志文本进行脱敏处理
|
|
139
|
+
* @param value 日志文本
|
|
140
|
+
* @param maxLength 最大长度
|
|
141
|
+
* @returns 脱敏后的日志文本
|
|
142
|
+
*/
|
|
143
|
+
function sanitizeLogText(value: string, maxLength: number): string {
|
|
144
|
+
return value
|
|
145
|
+
.replace(/[\u0000-\u001F\u007F\u2028\u2029]/g, ' ')
|
|
146
|
+
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)([^@\s/]+)@/gi, '$1[REDACTED]@')
|
|
147
|
+
.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, '$1 [REDACTED]')
|
|
148
|
+
.replace(
|
|
149
|
+
/([?&](?:password|passwd|pwd|token|access[_-]?token|refresh[_-]?token|api[_-]?key|secret|client[_-]?secret)=)[^&#\s]*/gi,
|
|
150
|
+
'$1[REDACTED]',
|
|
151
|
+
)
|
|
152
|
+
.replace(
|
|
153
|
+
/(["']?)(authorization|proxy-authorization|cookie|set-cookie)\1\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^,}\]]+)/gi,
|
|
154
|
+
'$1$2$1=[REDACTED]',
|
|
155
|
+
)
|
|
156
|
+
.replace(
|
|
157
|
+
/(["']?)(password|passwd|pwd|token|access[_-]?token|refresh[_-]?token|api[_-]?key|x-api-key|secret|client[_-]?secret|database[_-]?url|redis[_-]?url|connection[_-]?string|dsn)\1\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;}\]]+)/gi,
|
|
158
|
+
'$1$2$1=[REDACTED]',
|
|
159
|
+
)
|
|
160
|
+
.replace(/\s+/g, ' ')
|
|
161
|
+
.trim()
|
|
162
|
+
.slice(0, maxLength)
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* /u2028 行分隔符
|
|
166
|
+
* /u2029 段落分隔符
|
|
167
|
+
* @param error
|
|
168
|
+
* @returns
|
|
169
|
+
*/
|
|
170
|
+
function sanitizeStackFrame(error: Error): string[] | undefined {
|
|
171
|
+
if (!error.stack || /[\r\n\u2028\u2029]/.test(error.message)) {
|
|
172
|
+
return undefined
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const frames = error.stack
|
|
176
|
+
.split(/\r?\n/)
|
|
177
|
+
.slice(1)
|
|
178
|
+
.map((line) => line.trim())
|
|
179
|
+
.filter((line) => line.startsWith('at '))
|
|
180
|
+
.slice(0, MAX_LOG_STACK_FRAMES)
|
|
181
|
+
.map((line) => sanitizeLogText(line, MAX_LOG_FRAME_LENGTH))
|
|
182
|
+
.filter(Boolean)
|
|
183
|
+
return frames.length ? frames : undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function serializeError(value: unknown, depth = 0): Record<string, unknown> {
|
|
187
|
+
if (!(value instanceof Error)) {
|
|
188
|
+
return {
|
|
189
|
+
type: typeof value,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const stackFrames = sanitizeStackFrame(value)
|
|
194
|
+
const result: Record<string, unknown> = {
|
|
195
|
+
name: sanitizeLogText(value.name, 120) || 'Error',
|
|
196
|
+
message: sanitizeLogText(value.message, MAX_LOG_MESSAGE_LENGTH),
|
|
197
|
+
...(stackFrames ? { stackFrames } : {}),
|
|
198
|
+
}
|
|
199
|
+
if (depth < 2 && value.cause !== undefined) {
|
|
200
|
+
result.cause = serializeError(value.cause, depth + 1)
|
|
201
|
+
}
|
|
202
|
+
return result
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Catch()
|
|
206
|
+
export class GlobalExceptionFilter implements ExceptionFilter {
|
|
207
|
+
private readonly logger = new Logger(GlobalExceptionFilter.name)
|
|
208
|
+
|
|
209
|
+
constructor(private readonly adapterHost: HttpAdapterHost) {}
|
|
210
|
+
|
|
211
|
+
private getZodDetails(exception: ZodValidationException): ApiErrorDetail[] | undefined {
|
|
212
|
+
const zodError = exception.getZodError()
|
|
213
|
+
|
|
214
|
+
if (!(zodError instanceof ZodError)) {
|
|
215
|
+
return undefined
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const details = zodError.issues.slice(0, MAX_DETAIL_COUNT).map((issue) => {
|
|
219
|
+
const path = cleanPublicText(issue.path.map(String).join('.'), MAX_DETAIL_PATH_LENGTH)
|
|
220
|
+
const message = cleanPublicText(issue.message, MAX_PUBLIC_MESSAGE_LENGTH) || '请求内容无效'
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
...(path ? { path } : {}),
|
|
224
|
+
code: cleanDetailCode(issue.code),
|
|
225
|
+
message,
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
return details.length ? details : undefined
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private normalizeLegacyHttpException(exception: HttpException): NormalizedFailure {
|
|
232
|
+
const status = normalizeStatus(exception.getStatus())
|
|
233
|
+
|
|
234
|
+
if (status >= 500) {
|
|
235
|
+
const serviceUnavailabel =
|
|
236
|
+
status === HttpStatus.BAD_GATEWAY ||
|
|
237
|
+
status === HttpStatus.SERVICE_UNAVAILABLE ||
|
|
238
|
+
status === HttpStatus.GATEWAY_TIMEOUT
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
status,
|
|
242
|
+
body: serviceUnavailabel
|
|
243
|
+
? {
|
|
244
|
+
code: COMMON_ERRORS.SERVICE_UNAVAILABLE.code,
|
|
245
|
+
message: COMMON_ERRORS.SERVICE_UNAVAILABLE.publicMessage,
|
|
246
|
+
}
|
|
247
|
+
: {
|
|
248
|
+
code: COMMON_ERRORS.INTERNAL_SERVER_ERROR.code,
|
|
249
|
+
message: COMMON_ERRORS.INTERNAL_SERVER_ERROR.publicMessage,
|
|
250
|
+
},
|
|
251
|
+
cause: exception,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const publicError = PUBLIC_HTTP_ERRORS[status] ?? {
|
|
256
|
+
code: 'COMMON.HTTP_' + status,
|
|
257
|
+
message: '请求处理失败',
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
status,
|
|
261
|
+
body: publicError,
|
|
262
|
+
bearerChallenge: status === HttpStatus.UNAUTHORIZED,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private normalize(exception: unknown): NormalizedFailure {
|
|
267
|
+
if (exception instanceof ZodSerializationException) {
|
|
268
|
+
return {
|
|
269
|
+
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
270
|
+
body: {
|
|
271
|
+
code: COMMON_ERRORS.INTERNAL_SERVER_ERROR.code,
|
|
272
|
+
message: COMMON_ERRORS.INTERNAL_SERVER_ERROR.publicMessage,
|
|
273
|
+
},
|
|
274
|
+
cause: exception.getZodError() ?? exception,
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (exception instanceof ZodValidationException) {
|
|
279
|
+
const details = this.getZodDetails(exception)
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
status: COMMON_ERRORS.VALIDATION_FAILED.status,
|
|
283
|
+
body: {
|
|
284
|
+
code: COMMON_ERRORS.VALIDATION_FAILED.code,
|
|
285
|
+
message: COMMON_ERRORS.VALIDATION_FAILED.publicMessage,
|
|
286
|
+
...(details?.length ? { details } : {}),
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (exception instanceof AppException) {
|
|
292
|
+
const status = normalizeStatus(exception.getStatus())
|
|
293
|
+
const details = normalizeDetails(exception.details)
|
|
294
|
+
const message =
|
|
295
|
+
cleanPublicText(exception.definition.publicMessage, MAX_PUBLIC_MESSAGE_LENGTH) ||
|
|
296
|
+
'请求处理失败'
|
|
297
|
+
return {
|
|
298
|
+
status,
|
|
299
|
+
body: {
|
|
300
|
+
code: exception.definition.code,
|
|
301
|
+
message,
|
|
302
|
+
...(details?.length ? { details } : {}),
|
|
303
|
+
},
|
|
304
|
+
cause: status >= 500 ? (exception.cause ?? exception) : undefined,
|
|
305
|
+
bearerChallenge: exception.definition.bearerChallenge,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (exception instanceof HttpException) {
|
|
310
|
+
return this.normalizeLegacyHttpException(exception)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
315
|
+
body: {
|
|
316
|
+
code: COMMON_ERRORS.INTERNAL_SERVER_ERROR.code,
|
|
317
|
+
message: COMMON_ERRORS.INTERNAL_SERVER_ERROR.publicMessage,
|
|
318
|
+
},
|
|
319
|
+
cause: exception,
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private logServerFailure(
|
|
324
|
+
exception: unknown,
|
|
325
|
+
normalized: NormalizedFailure,
|
|
326
|
+
request: FastifyRequest,
|
|
327
|
+
requestId: string,
|
|
328
|
+
): void {
|
|
329
|
+
const route = request.routeOptions?.url ?? 'unmatched'
|
|
330
|
+
const cause = normalized.cause ?? exception
|
|
331
|
+
|
|
332
|
+
this.logger.error({
|
|
333
|
+
event: 'http_request_failed',
|
|
334
|
+
requestId,
|
|
335
|
+
method: request.method,
|
|
336
|
+
route,
|
|
337
|
+
status: normalized.status,
|
|
338
|
+
code: normalized.body.code,
|
|
339
|
+
error: serializeError(cause),
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
catch(exception: unknown, host: ArgumentsHost): void {
|
|
344
|
+
const http = host.switchToHttp()
|
|
345
|
+
const request = http.getRequest<FastifyRequest>()
|
|
346
|
+
const response = http.getResponse<FastifyReply>()
|
|
347
|
+
const requestId = String(request.id)
|
|
348
|
+
const normalized = this.normalize(exception)
|
|
349
|
+
const { httpAdapter } = this.adapterHost
|
|
350
|
+
|
|
351
|
+
// 只记录服务器错误 业务错误不记录
|
|
352
|
+
if (normalized.status >= 500) {
|
|
353
|
+
this.logServerFailure(exception, normalized, request, requestId)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 如果响应头已经被发送了 则直接结束响应 不再进行后面发的处理
|
|
357
|
+
if (httpAdapter.isHeadersSent(response)) {
|
|
358
|
+
httpAdapter.end(response)
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
httpAdapter.setHeader(response, 'x-request-id', requestId)
|
|
363
|
+
|
|
364
|
+
/** 如果是未授权错误 则添加WWW-Authenticate 头 相当于告诉客户 请使用Bearer 认证
|
|
365
|
+
* 为什么使用 normalized.bearerChallenge 而不是 normalized.status === HttpStatus.UNAUTHORIZED
|
|
366
|
+
* 因为 normalized.status === HttpStatus.UNAUTHORIZED 只是告诉客户 授权有问题
|
|
367
|
+
* 以下情况 bearerChallenge 为 false
|
|
368
|
+
* 账号被禁用/锁定、权限不足、API Key 认证失败 、Basic Auth 失败、IP / 网络层拦截、Session/Cookie 失效
|
|
369
|
+
* 为true 的场景
|
|
370
|
+
* 应该设为 true 的场景
|
|
371
|
+
异常类型 原因 为什么需要 Bearer 质询
|
|
372
|
+
Token 缺失 请求头没有 Authorization 客户端需要知道“这里要传 Bearer Token”
|
|
373
|
+
Token 格式错误 不是合法的 JWT / 签名不对 客户端可能传错了 Token 类型或损坏了
|
|
374
|
+
Token 过期 exp 时间已过 客户端需要用 refresh_token 换取新 Token
|
|
375
|
+
Token 签发者/受众不匹配 iss / aud 校验失败 客户端用了错误环境的 Token
|
|
376
|
+
Token 被吊销 在黑名单 / 撤销列表中 客户端需要重新获取 Token
|
|
377
|
+
*/
|
|
378
|
+
if (normalized.bearerChallenge) {
|
|
379
|
+
httpAdapter.setHeader(response, 'www-authenticate', 'Bearer realm="api"')
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
httpAdapter.reply(response, normalized.body, normalized.status)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -2,4 +2,4 @@ export { default as appConfig } from './app.config'
|
|
|
2
2
|
export { default as databaseConfig } from './database.config'
|
|
3
3
|
export { default as llmConfig } from './llm.config'
|
|
4
4
|
export { default as redisConfig } from './redis.config'
|
|
5
|
-
export { default as
|
|
5
|
+
export { default as sessionConfig } from './session.config'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { registerAs } from '@nestjs/config'
|
|
2
|
+
|
|
3
|
+
function readPositiveInteger(name: string, fallback: number) {
|
|
4
|
+
const value = Number.parseInt(process.env[name] ?? String(fallback), 10)
|
|
5
|
+
|
|
6
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
7
|
+
throw new Error(`${name} must be a positive integer`)
|
|
8
|
+
}
|
|
9
|
+
return value
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default registerAs('session', () => {
|
|
13
|
+
const tokenPepper = process.env.SESSION_TOKEN_PEPPER
|
|
14
|
+
/**
|
|
15
|
+
* 没有置换的情况下的过期时间 2个小时
|
|
16
|
+
*/
|
|
17
|
+
const idleTtlSeconds = readPositiveInteger('SESSION_IDLE_TTL_SECONDS', 7200)
|
|
18
|
+
/**
|
|
19
|
+
* 绝对过期时间7天
|
|
20
|
+
*/
|
|
21
|
+
const absoluteTtlSeconds = readPositiveInteger('SESSION_ABSOLUTE_TTL_SECONDS', 604800)
|
|
22
|
+
|
|
23
|
+
if (!tokenPepper || tokenPepper.length < 32) {
|
|
24
|
+
throw new Error('SESSION_TOKEN_PEPPER must contain at least 32 characters')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (absoluteTtlSeconds <= idleTtlSeconds) {
|
|
28
|
+
throw new Error('SESSION_ABSOLUTE_TTL_SECONDS must be greater than idle TTL')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
tokenPepper,
|
|
33
|
+
idleTtlMs: +idleTtlSeconds * 1000,
|
|
34
|
+
absoluteTtlMs: +absoluteTtlSeconds * 1000,
|
|
35
|
+
}
|
|
36
|
+
})
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { pgEnum } from 'drizzle-orm/pg-core'
|
|
2
|
-
import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core'
|
|
1
|
+
import { pgEnum, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
|
|
3
2
|
|
|
4
|
-
export const
|
|
5
|
-
|
|
6
|
-
export const authSessionStatusEnum = pgEnum('auth_session_status', ['active', 'revoked'])
|
|
3
|
+
export const userStatusEnum = pgEnum('user_status', ['active', 'locked', 'disabled'])
|
|
7
4
|
|
|
8
5
|
export const users = pgTable('users', {
|
|
9
|
-
id:
|
|
6
|
+
id: uuid('id').defaultRandom().primaryKey(),
|
|
10
7
|
name: varchar('name', { length: 100 }).notNull(),
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
account: varchar('account', { length: 32 }).notNull().unique(),
|
|
9
|
+
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
|
10
|
+
status: userStatusEnum('status').default('active').notNull(),
|
|
13
11
|
createdAt: timestamp('create_at').defaultNow().notNull(),
|
|
12
|
+
updatedAt: timestamp('update_at', { withTimezone: true }).defaultNow().notNull(),
|
|
14
13
|
})
|
|
@@ -1,21 +1,38 @@
|
|
|
1
|
-
import { NestFactory } from '@nestjs/core'
|
|
2
1
|
import { AppModule } from '@/app.module'
|
|
3
2
|
import { logNetworkUrls } from '@/utils/server-address'
|
|
4
|
-
import {
|
|
3
|
+
import { ConsoleLogger, Logger } from '@nestjs/common'
|
|
4
|
+
import { NestFactory } from '@nestjs/core'
|
|
5
|
+
import { type NestFastifyApplication } from '@nestjs/platform-fastify'
|
|
5
6
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
|
|
6
7
|
import { cleanupOpenApiDoc } from 'nestjs-zod'
|
|
7
|
-
import
|
|
8
|
+
import { createFastifyAdapter } from './common/adapters/fastify.adapter'
|
|
8
9
|
|
|
9
10
|
async function bootstrap() {
|
|
10
|
-
const app = await NestFactory.create<NestFastifyApplication>(AppModule,
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const app = await NestFactory.create<NestFastifyApplication>(AppModule, createFastifyAdapter(), {
|
|
12
|
+
logger: new ConsoleLogger({
|
|
13
|
+
json: process.env.NODE_ENV === 'production',
|
|
14
|
+
colors: process.env.NODE_ENV === 'development',
|
|
15
|
+
}),
|
|
16
|
+
})
|
|
17
|
+
app.enableCors({
|
|
18
|
+
origin: ['*'],
|
|
19
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-Company-Id'],
|
|
20
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
21
|
+
exposedHeaders: ['X-Request-Id', 'WWW-Authenticate', 'Retry-After'],
|
|
22
|
+
})
|
|
13
23
|
|
|
14
24
|
const config = new DocumentBuilder()
|
|
15
25
|
.setTitle('前后端模板 API')
|
|
16
26
|
.setDescription('接口文档')
|
|
17
27
|
.setVersion('1.0')
|
|
18
|
-
.addBearerAuth(
|
|
28
|
+
.addBearerAuth(
|
|
29
|
+
{
|
|
30
|
+
type: 'http',
|
|
31
|
+
scheme: 'bearer',
|
|
32
|
+
bearerFormat: 'Opaque Session Token',
|
|
33
|
+
},
|
|
34
|
+
'session',
|
|
35
|
+
) // 用 JWT 就留着,不用可以删
|
|
19
36
|
.build()
|
|
20
37
|
|
|
21
38
|
// 生成openAPI 文档
|
|
@@ -31,4 +48,8 @@ async function bootstrap() {
|
|
|
31
48
|
logNetworkUrls(port)
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
void bootstrap()
|
|
51
|
+
void bootstrap().catch((cause: unknown) => {
|
|
52
|
+
const logger = new Logger('Bootstrap')
|
|
53
|
+
logger.error('Server boostrap failed', cause instanceof Error ? cause.stack : undefined)
|
|
54
|
+
process.exitCode = 1
|
|
55
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CurrentAuth } from '@/common/decorators/current-auth.decorator'
|
|
2
|
+
import { Public } from '@/common/decorators/public.decorator'
|
|
3
|
+
import { Body, Controller, Get, Headers, HttpCode, HttpStatus, Post, Req } from '@nestjs/common'
|
|
4
|
+
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'
|
|
5
|
+
import type { FastifyRequest } from 'fastify'
|
|
6
|
+
import { CurrentUser } from 'shared/types'
|
|
7
|
+
import { AuthService } from './auth.service'
|
|
8
|
+
import { LoginDto } from './dto/login.dto'
|
|
9
|
+
import { RegisterDto } from './dto/register.dto'
|
|
10
|
+
import type { CurrentAuthType } from './session/session.types'
|
|
11
|
+
|
|
12
|
+
@ApiTags('认证')
|
|
13
|
+
@Controller('auth')
|
|
14
|
+
export class AuthController {
|
|
15
|
+
constructor(private readonly authService: AuthService) {}
|
|
16
|
+
|
|
17
|
+
@Public()
|
|
18
|
+
@ApiOperation({ summary: '注册' })
|
|
19
|
+
@Post('register')
|
|
20
|
+
register(@Body() body: RegisterDto) {
|
|
21
|
+
return this.authService.register(body)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Public()
|
|
25
|
+
@ApiOperation({ summary: '登录并创建 Redis Session' })
|
|
26
|
+
@HttpCode(HttpStatus.OK)
|
|
27
|
+
@Post('login')
|
|
28
|
+
login(@Body() body: LoginDto, @Req() request: FastifyRequest) {
|
|
29
|
+
return this.authService.login(body, {
|
|
30
|
+
ip: request.ip,
|
|
31
|
+
userAgent: request.headers['user-agent'] ?? '',
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@ApiBearerAuth('session')
|
|
36
|
+
@ApiOperation({ summary: '退出当前端' })
|
|
37
|
+
@Public()
|
|
38
|
+
@HttpCode(HttpStatus.OK)
|
|
39
|
+
@Post('logout')
|
|
40
|
+
logout(@Headers('authorization') authorization: string | undefined) {
|
|
41
|
+
return this.authService.logout(authorization)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@ApiBearerAuth('session')
|
|
45
|
+
@ApiOperation({ summary: '使用 Session Token 获取当前用户资料' })
|
|
46
|
+
@Get('me')
|
|
47
|
+
async me(@CurrentAuth() auth: CurrentAuthType): Promise<CurrentUser> {
|
|
48
|
+
const user = await this.authService.getCurrentUser(auth.userId)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...user,
|
|
52
|
+
terminal: auth.terminal,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AppErrorDefinition } from '@/common/exceptions/app.exception'
|
|
2
|
+
import { HttpStatus } from '@nestjs/common'
|
|
3
|
+
|
|
4
|
+
export const AUTH_ERRORS = {
|
|
5
|
+
SESSION_TOKEN_MISSING: {
|
|
6
|
+
code: 'AUTH.SESSION_TOKEN_MISSING',
|
|
7
|
+
publicMessage: '缺少有效的登录 Token',
|
|
8
|
+
status: HttpStatus.UNAUTHORIZED,
|
|
9
|
+
bearerChallenge: true,
|
|
10
|
+
},
|
|
11
|
+
SESSION_INVALID: {
|
|
12
|
+
code: 'AUTH.SESSION_INVALID',
|
|
13
|
+
publicMessage: '登录已失效,请重新登录',
|
|
14
|
+
status: HttpStatus.UNAUTHORIZED,
|
|
15
|
+
bearerChallenge: true,
|
|
16
|
+
},
|
|
17
|
+
CONTEXT_MISSING: {
|
|
18
|
+
code: 'AUTH.CONTEXT_MISSING',
|
|
19
|
+
publicMessage: '服务器内部错误',
|
|
20
|
+
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
21
|
+
},
|
|
22
|
+
INVALID_CREDENTIALS: {
|
|
23
|
+
code: 'AUTH.INVALID_CREDENTIALS',
|
|
24
|
+
publicMessage: '账号或密码错误',
|
|
25
|
+
status: HttpStatus.UNPROCESSABLE_ENTITY,
|
|
26
|
+
},
|
|
27
|
+
ACCOUNT_ALREADY_EXISTS: {
|
|
28
|
+
code: 'AUTH.ACCOUNT_ALREADY_EXISTS',
|
|
29
|
+
publicMessage: '账号已存在',
|
|
30
|
+
status: HttpStatus.CONFLICT,
|
|
31
|
+
},
|
|
32
|
+
FORBIDDEN: {
|
|
33
|
+
code: 'AUTH.FORBIDDEN',
|
|
34
|
+
publicMessage: '没有权限执行此操作',
|
|
35
|
+
status: HttpStatus.FORBIDDEN,
|
|
36
|
+
},
|
|
37
|
+
SERVICE_UNAVAILABLE: {
|
|
38
|
+
code: 'AUTH.SERVICE_UNAVAILABLE',
|
|
39
|
+
publicMessage: '登录服务暂时不可用,请稍后重试',
|
|
40
|
+
status: HttpStatus.SERVICE_UNAVAILABLE,
|
|
41
|
+
},
|
|
42
|
+
} as const satisfies Record<string, AppErrorDefinition>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common'
|
|
2
|
+
import { APP_GUARD } from '@nestjs/core'
|
|
3
|
+
import { AuthController } from './auth.controller'
|
|
4
|
+
import { AuthRepository } from './auth.repository'
|
|
5
|
+
import { AuthService } from './auth.service'
|
|
6
|
+
import { SessionAuthGuard } from './guards/session-auth.guard'
|
|
7
|
+
import { PasswordService } from './password.service'
|
|
8
|
+
import { SessionStoreService } from './session/session-store.service'
|
|
9
|
+
import { SessionTokenService } from './session/session-token.service'
|
|
10
|
+
|
|
11
|
+
@Module({
|
|
12
|
+
controllers: [AuthController],
|
|
13
|
+
providers: [
|
|
14
|
+
AuthRepository,
|
|
15
|
+
AuthService,
|
|
16
|
+
PasswordService,
|
|
17
|
+
SessionTokenService,
|
|
18
|
+
SessionStoreService,
|
|
19
|
+
{
|
|
20
|
+
provide: APP_GUARD,
|
|
21
|
+
useClass: SessionAuthGuard,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
exports: [SessionStoreService],
|
|
25
|
+
})
|
|
26
|
+
export class AuthModule {}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DRIZZLE, type DrizzleDB } from '@/database/db.module'
|
|
2
|
+
import { users } from '@/database/schema'
|
|
3
|
+
import { Inject, Injectable } from '@nestjs/common'
|
|
4
|
+
import { eq } from 'drizzle-orm'
|
|
5
|
+
|
|
6
|
+
type CreateUserInput = Pick<typeof users.$inferInsert, 'name' | 'account' | 'passwordHash'>
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class AuthRepository {
|
|
10
|
+
constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB) {}
|
|
11
|
+
|
|
12
|
+
async findByAccount(account: string) {
|
|
13
|
+
const [user] = await this.db.select().from(users).where(eq(users.account, account)).limit(1)
|
|
14
|
+
return user ?? null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async findPublicById(userId: string) {
|
|
18
|
+
const [user] = await this.db
|
|
19
|
+
.select({
|
|
20
|
+
id: users.id,
|
|
21
|
+
account: users.account,
|
|
22
|
+
name: users.name,
|
|
23
|
+
status: users.status,
|
|
24
|
+
})
|
|
25
|
+
.from(users)
|
|
26
|
+
.where(eq(users.id, userId))
|
|
27
|
+
.limit(1)
|
|
28
|
+
return user ?? null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async createUser(input: CreateUserInput) {
|
|
32
|
+
const [user] = await this.db
|
|
33
|
+
.insert(users)
|
|
34
|
+
.values(input)
|
|
35
|
+
// 处理账号重复情况 ,不插入重复账号
|
|
36
|
+
.onConflictDoNothing({ target: users.account })
|
|
37
|
+
.returning()
|
|
38
|
+
return user ?? null
|
|
39
|
+
}
|
|
40
|
+
}
|