├── .editorconfig ├── .eslintignore ├── .github └── workflows │ ├── cluster-setting.yaml │ ├── deployment_pipline.yaml │ └── release_pipline.yaml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── README.MD ├── READMECN.MD ├── apps ├── backend │ ├── gateway │ │ ├── .npmrc │ │ ├── nest-cli.json │ │ ├── package.json │ │ ├── src │ │ │ ├── app.module.ts │ │ │ ├── main.ts │ │ │ └── modules │ │ │ │ ├── Auth │ │ │ │ ├── auth.controller.ts │ │ │ │ ├── auth.module.ts │ │ │ │ ├── auth.service.ts │ │ │ │ ├── decorators │ │ │ │ │ ├── noSIgn.decorator.ts │ │ │ │ │ └── noToken.decorator.ts │ │ │ │ └── guards │ │ │ │ │ ├── JWT.guard.ts │ │ │ │ │ └── Sign.guard.ts │ │ │ │ ├── Document │ │ │ │ ├── document.controller.ts │ │ │ │ ├── document.module.ts │ │ │ │ └── document.service.ts │ │ │ │ └── HealthCheck │ │ │ │ ├── health.controller.ts │ │ │ │ ├── health.module.ts │ │ │ │ └── health.service.ts │ │ └── tsconfig.json │ ├── ms-auth │ │ ├── .npmrc │ │ ├── nest-cli.json │ │ ├── package.json │ │ ├── src │ │ │ ├── app.module.ts │ │ │ ├── main.ts │ │ │ ├── models │ │ │ │ ├── auth.DBcollection.ts │ │ │ │ ├── common.schema.ts │ │ │ │ ├── permission.schema.ts │ │ │ │ ├── resource.schema.ts │ │ │ │ ├── role.schema.ts │ │ │ │ └── user.schema.ts │ │ │ └── modules │ │ │ │ ├── ACL │ │ │ │ ├── ACLs.controller.ts │ │ │ │ ├── ACLs.module.ts │ │ │ │ ├── ACLs.service.ts │ │ │ │ ├── dto │ │ │ │ │ ├── permission.dto.ts │ │ │ │ │ ├── resource.dto.ts │ │ │ │ │ ├── role-update.ts │ │ │ │ │ └── role.dto.ts │ │ │ │ └── middlewares │ │ │ │ │ ├── ACL.decorator.ts │ │ │ │ │ └── ACL.guard.ts │ │ │ │ ├── Auth │ │ │ │ ├── auth.controller.ts │ │ │ │ ├── auth.module.ts │ │ │ │ └── auth.service.ts │ │ │ │ └── User │ │ │ │ ├── dto │ │ │ │ ├── create-user.dto.ts │ │ │ │ ├── interface.ts │ │ │ │ └── userInfo.dto.ts │ │ │ │ ├── user.controller.ts │ │ │ │ ├── user.module.ts │ │ │ │ └── user.service.ts │ │ └── tsconfig.json │ ├── ms-document │ │ ├── nest-cli.json │ │ ├── package.json │ │ ├── src │ │ │ ├── app.module.ts │ │ │ ├── i18n │ │ │ │ ├── en.json │ │ │ │ ├── i18n.config.ts │ │ │ │ └── zh.json │ │ │ ├── main.ts │ │ │ ├── modules │ │ │ │ ├── Email │ │ │ │ │ ├── email.controller.ts │ │ │ │ │ ├── email.module.ts │ │ │ │ │ ├── email.service.ts │ │ │ │ │ └── templates │ │ │ │ │ │ └── trip_submit_failed.tsx │ │ │ │ ├── File │ │ │ │ │ ├── file.controller.ts │ │ │ │ │ ├── file.module.ts │ │ │ │ │ └── file.service.ts │ │ │ │ └── PDF │ │ │ │ │ ├── PDF.controller.ts │ │ │ │ │ ├── PDF.module.ts │ │ │ │ │ ├── PDF.service.ts │ │ │ │ │ └── templates │ │ │ │ │ ├── common_footer.tsx │ │ │ │ │ ├── common_header.tsx │ │ │ │ │ └── trip_submit_success.tsx │ │ │ └── shared │ │ └── tsconfig.json │ └── ms-pawhaven │ │ ├── nest-cli.json │ │ ├── package.json │ │ ├── src │ │ ├── app.module.ts │ │ ├── main.ts │ │ ├── models │ │ │ ├── common.schema.ts │ │ │ ├── trip.DBcollection.ts │ │ │ └── tripInfo.schema.ts │ │ └── modules │ │ │ └── Trip │ │ │ ├── trip.controller.ts │ │ │ ├── trip.module.ts │ │ │ └── trip.service.ts │ │ └── tsconfig.json └── frontend │ ├── admin │ ├── tsconfig.json │ └── tsconfig.node.json │ └── user │ ├── .cssrem │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── index.html │ ├── manifest.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── images │ │ ├── 404.png │ │ ├── 500.png │ │ ├── PawHaven-Desktop.svg │ │ ├── PawHaven-Mobile.svg │ │ ├── UI-design.png │ │ ├── authBanner.png │ │ ├── hero.png │ │ ├── hero1.png │ │ └── logo.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── app │ │ ├── App.tsx │ │ ├── AppProvider.tsx │ │ ├── GlobalInitializationAPI.tsx │ │ └── GlobalInitializer.tsx │ ├── components │ │ ├── Brand │ │ │ └── index.tsx │ │ ├── ErrorFallback │ │ │ └── index.tsx │ │ ├── ImageUploader │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── LangSwitcher │ │ │ └── index.tsx │ │ ├── NotFund │ │ │ └── index.tsx │ │ └── SystemError │ │ │ └── index.tsx │ ├── config │ │ └── index.ts │ ├── constants │ │ ├── RescueStatus.ts │ │ └── StorageKeys.ts │ ├── features │ │ ├── Auth │ │ │ ├── Login │ │ │ │ └── index.tsx │ │ │ ├── Register │ │ │ │ └── index.tsx │ │ │ ├── apis │ │ │ │ ├── queries.ts │ │ │ │ └── requests.ts │ │ │ ├── authLayout.tsx │ │ │ └── types.ts │ │ ├── Home │ │ │ ├── apis │ │ │ │ ├── queries.ts │ │ │ │ └── requests.ts │ │ │ ├── components │ │ │ │ ├── Hero.tsx │ │ │ │ ├── LatestRescue.tsx │ │ │ │ └── RecentStory.tsx │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── LoveStories │ │ │ └── index.tsx │ │ ├── ReportStray │ │ │ ├── apis │ │ │ │ ├── queries.ts │ │ │ │ └── requests.ts │ │ │ ├── components │ │ │ │ └── ReportForm.tsx │ │ │ ├── constants.ts │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── RescueDetail │ │ │ ├── apis │ │ │ │ ├── queries.ts │ │ │ │ └── request.ts │ │ │ ├── components │ │ │ │ ├── AnimalBasicInfo.tsx │ │ │ │ ├── RescueInteraction.tsx │ │ │ │ └── RescueTimeline.tsx │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ └── RescueGuide │ │ │ ├── components │ │ │ └── StepCard.tsx │ │ │ └── index.tsx │ ├── hooks │ │ ├── reduxHooks.ts │ │ └── useIsProd.ts │ ├── layout │ │ ├── RootLayoutFooter.tsx │ │ ├── RootLayoutMenu.tsx │ │ ├── RootLayoutMenuRender.tsx │ │ ├── RootLayoutSidebar.tsx │ │ └── index.tsx │ ├── main.tsx │ ├── route │ │ ├── AppRouterProvider.tsx │ │ ├── routePaths.ts │ │ └── routerElementMapping.tsx │ ├── store │ │ ├── globalReducer.ts │ │ ├── reducerConfig.ts │ │ ├── reducerNames.ts │ │ └── reduxStore.ts │ ├── types │ │ ├── AnimalType.ts │ │ └── LayoutType.ts │ └── utils │ │ ├── apiClient.ts │ │ └── getStatusColorByPrefix.ts │ ├── tsconfig.json │ └── vite.config.ts ├── commitlint.config.cjs ├── libs └── configs │ ├── eslint-config │ ├── README.MD │ ├── base.js │ ├── index.js │ ├── node.js │ ├── package.json │ └── web.js │ └── tsconfig │ ├── README.MD │ ├── base.json │ ├── index.js │ ├── node.json │ ├── package.json │ └── web.json ├── package.json ├── packages ├── i18n │ ├── README.MD │ ├── de-DE.json │ ├── en-US.json │ ├── index.js │ ├── package.json │ └── zh-CN.json ├── shared-backend │ ├── .eslintrc.cjs │ ├── DTO │ │ ├── Auth │ │ │ ├── create-user.dto.ts │ │ │ ├── interface.ts │ │ │ └── userInfo.dto.ts │ │ ├── Document │ │ │ ├── create-PDF.DTO.ts │ │ │ ├── email-options.DTO.ts │ │ │ └── send-email.DTO.ts │ │ └── Trip │ │ │ └── tripInfo.DTO.ts │ ├── assets │ │ └── HMAC.png │ ├── constants │ │ ├── MSMessagePatterns │ │ │ ├── auth.messagePattern.ts │ │ │ └── trip.messagePattern.ts │ │ ├── constant.ts │ │ └── enum.ts │ ├── core │ │ ├── configModule │ │ │ └── configs.module.ts │ │ ├── dataBase │ │ │ └── db.module.ts │ │ ├── httpClient │ │ │ ├── HttpClient.service.ts │ │ │ ├── httpClient.module.ts │ │ │ ├── httpExceptionFilter.ts │ │ │ ├── httpInterceptor.ts │ │ │ ├── interface.ts │ │ │ └── rpcExceptionFillter.ts │ │ ├── microServiceClient │ │ │ ├── msClient.module.ts │ │ │ └── msClient.servicea.ts │ │ └── swagger │ │ │ └── index.ts │ ├── middlewares │ │ ├── httpSetting.middleware.ts │ │ └── index.module.ts │ ├── package.json │ ├── shared.module.ts │ ├── tsconfig.json │ └── utils │ │ ├── convertImagToBase64.ts │ │ ├── getConfigValues.ts │ │ ├── overWriteHeader.ts │ │ └── trim.ts ├── shared-frontend │ ├── .eslintrc.cjs │ ├── constants │ │ ├── localeKey.ts │ │ └── myPerson.ts │ ├── cores │ │ ├── http │ │ │ ├── encrypt.ts │ │ │ ├── errorHandle.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── react-query │ │ │ └── index.ts │ ├── hooks │ │ ├── useDoubleClick.ts │ │ ├── useIsMobile.ts │ │ └── useRouterInfo.ts │ ├── package.json │ ├── resources │ │ ├── featurePlan.md │ │ ├── ideas.md │ │ ├── 主页.png │ │ ├── 动物详情.png │ │ └── 爱心故事.png │ ├── tsconfig.json │ └── utils │ │ ├── SRTime.ts │ │ ├── convertToColonFormat.ts │ │ ├── getCountryCode.ts │ │ ├── getCurrencyCode.ts │ │ ├── getIconByName.ts │ │ ├── getLocale.ts │ │ └── storage.ts ├── theme │ ├── MUI-theme.js │ ├── README.MD │ ├── common.css │ ├── designTokens.js │ ├── globalTailwind.css │ └── package.json └── ui │ ├── .eslintrc.cjs │ ├── AvatarMenu │ └── index.tsx │ ├── FileUpload │ └── index.tsx │ ├── Form │ ├── FormCheckBox │ │ └── index.tsx │ ├── FormDateRanger │ │ └── index.tsx │ ├── FormInput │ │ └── index.tsx │ ├── FormRadio │ │ └── index.tsx │ ├── FormSelect │ │ └── index.tsx │ ├── FormTextArea │ │ └── index.tsx │ └── formBase.type.ts │ ├── IconComponent │ └── index.tsx │ ├── Loading │ └── index.tsx │ ├── Phase │ └── index.tsx │ ├── README.MD │ ├── SuspenseWrapper │ └── index.tsx │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*.md] 6 | trim_trailing_whitespace = false 7 | 8 | [*.js] 9 | trim_trailing_whitespace = true 10 | 11 | [*] 12 | indent_style = space 13 | indent_size = 2 14 | end_of_line = lf 15 | charset = utf-8 16 | insert_final_newline = true 17 | max_line_length = 100 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore build outputs and dependencies 2 | node_modules/ 3 | dist/ 4 | build/ 5 | coverage/ 6 | .next/ 7 | out/ 8 | 9 | # Ignore config and generated files 10 | *.config.js 11 | *.config.cjs 12 | *.config.mjs 13 | *.min.js 14 | *.d.ts 15 | 16 | # Ignore environment and tooling folders 17 | .husky/ 18 | .vscode/ 19 | .github/ 20 | scripts/ 21 | logs/ 22 | 23 | # Ignore specific monorepo folders that don't require linting 24 | libs/configs 25 | -------------------------------------------------------------------------------- /.github/workflows/cluster-setting.yaml: -------------------------------------------------------------------------------- 1 | name: AKS settings Applyment 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | deployedEnvironment: 6 | description: 'The environment to deploy' 7 | required: true 8 | type: choice 9 | options: [uat, test] 10 | default: uat 11 | 12 | permissions: 13 | security-events: write 14 | actions: read 15 | contents: read 16 | issues: write 17 | id-token: write 18 | 19 | jobs: 20 | Deployment-pipiline: 21 | uses: aoda-zhang/shared-devops/.github/workflows/shared_aks_applyment.yaml@master 22 | with: 23 | currentEnvironment: ${{inputs.deployedEnvironment}} 24 | target_config_path: k8sSettings/${{inputs.deployedEnvironment }} 25 | secrets: 26 | PAT: ${{ secrets.PAT }} 27 | AKS_NAMESPACE: ${{ secrets[format('AKS_NAMESPACE_{0}', inputs.deployedEnvironment)] }} 28 | AZURE_CREDS: ${{ secrets.AZURE_CREDS_DEV }} 29 | AZURE_RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP_DEV }} 30 | AKS_CLUSTER_NAME: ${{ secrets.AKS_CLUSTER_NAME_DEV }} 31 | -------------------------------------------------------------------------------- /.github/workflows/deployment_pipline.yaml: -------------------------------------------------------------------------------- 1 | name: Deployment-pipiline (Non Prod) 2 | run-name: Deploying ${{ inputs.deployedApp }} to ${{ inputs.deployedEnvironment }} 3 | on: 4 | # Only auto deploy when push to develope branch 5 | # push: 6 | # branches: develope 7 | # paths-ignore: 8 | # - "**/.github/**" 9 | # - "**/*.txt" 10 | # - "**/*.MD" 11 | # - "**/*.md" 12 | workflow_dispatch: 13 | inputs: 14 | deployedEnvironment: 15 | description: 'The environment to deploy' 16 | required: true 17 | type: choice 18 | options: [uat, test] 19 | default: uat 20 | deployedApp: 21 | description: 'The app name to deploy' 22 | required: true 23 | type: choice 24 | options: [apps/pawHaven, apps/auth] 25 | 26 | permissions: 27 | security-events: write 28 | actions: read 29 | contents: read 30 | issues: write 31 | id-token: write 32 | 33 | jobs: 34 | Deployment-pipiline: 35 | uses: aoda-zhang/shared-devops/.github/workflows/shared_delivery_pipline.yaml@master 36 | with: 37 | app_repository: ${{ github.repository }} 38 | app_branch: ${{ github.ref }} 39 | deployed_app: ${{ inputs.deployedApp }} 40 | deploye_type: WEBAPP 41 | config_path: ${{inputs.deployedApp}} 42 | currentEnvironment: ${{inputs.deployedEnvironment}} 43 | dockerfile_path: k8s/Dockerfile 44 | secrets: 45 | PAT: ${{ secrets.PAT }} 46 | # Dynamic get secrets by environment example,like DOCKER_SERVER_UAT,DOCKER_SERVER_TEST 47 | # DOCKER_SERVER: ${{ secrets[format('DOCKER_SERVER_{0}', inputs.deployedEnvironment)] }} 48 | AKS_NAMESPACE: ${{ secrets[format('AKS_NAMESPACE_{0}', inputs.deployedEnvironment)] }} 49 | DOCKER_SERVER: ${{ secrets.DOCKER_SERVER_DEV }} 50 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_DEV }} 51 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_DEV }} 52 | AZURE_RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP_DEV }} 53 | AKS_CLUSTER_NAME: ${{ secrets.AKS_CLUSTER_NAME_DEV }} 54 | AZURE_CREDS: ${{ secrets.AZURE_CREDS_DEV }} 55 | -------------------------------------------------------------------------------- /.github/workflows/release_pipline.yaml: -------------------------------------------------------------------------------- 1 | name: Release-pipiline (Prod) 2 | on: 3 | # For Prod,Only Manuly deploy 4 | workflow_dispatch: 5 | inputs: 6 | deployedApp: 7 | description: 'The app name to deploy' 8 | required: true 9 | type: choice 10 | options: [apps/auth, apps/security] 11 | 12 | permissions: 13 | security-events: write 14 | actions: read 15 | contents: read 16 | issues: write 17 | 18 | jobs: 19 | Deployment-pipiline: 20 | uses: aoda-zhang/shared-devops/.github/workflows/shared_delivery_pipline.yaml@master 21 | with: 22 | app_repository: ${{ github.repository }} 23 | app_branch: ${{ github.ref }} 24 | deployed_app: ${{ inputs.deployedApp }} 25 | deploye_type: WEBAPP 26 | config_path: ${{inputs.deployedApp}} 27 | currentEnvironment: prod 28 | secrets: 29 | PAT: ${{ secrets.PAT }} 30 | DOCKER_SERVER: ${{ secrets.DOCKER_SERVER_PROD }} 31 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_PROD }} 32 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_PROD }} 33 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS_PROD }} 34 | AZURE_RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP_PROD }} 35 | AKS_CLUSTER_NAME: ${{ secrets.AKS_CLUSTER_NAME_PROD }} 36 | AKS_NAMESPACE: ${{ secrets.AKS_NAMESPACE_PROD }} 37 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npx --no -- commitlint --edit "$1" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run ESLint + Prettier via lint-staged 4 | echo "🧹 Checking code lints...." 5 | pnpm exec lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | coverage 5 | *.log 6 | *.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "useTabs": false 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", // ESLint integration 4 | "esbenp.prettier-vscode", // Prettier formatter 5 | "bradlc.vscode-tailwindcss", // Tailwind CSS IntelliSense 6 | "christian-kohler.path-intellisense", // Local file path completion 7 | "ionutvmi.path-autocomplete", // Alias path completion 8 | "yoavbls.pretty-ts-errors", // Better TypeScript errors 9 | "ecmel.vscode-html-css", // HTML + CSS IntelliSense 10 | "formulahendry.auto-rename-tag", // Auto rename paired HTML/JSX tags 11 | "abusaidm.html-snippets", // HTML snippets and tag suggestions 12 | "dsznajder.es7-react-js-snippets", // React/Redux/TS snippets 13 | "burkeholland.simple-react-snippets", // Clean React hooks/component snippets 14 | "xabikos.react-native-snippets" // Optional: more JSX/React patterns 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // --- 🧹 Basic formatting & save rules --- 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll": "explicit", 7 | "source.fixAll.eslint": "explicit" 8 | }, 9 | 10 | // --- 🧠 TypeScript settings --- 11 | "typescript.tsdk": "node_modules/typescript/lib", 12 | "typescript.enablePromptUseWorkspaceTsdk": true, 13 | 14 | // --- 💄 ESLint + Prettier integration --- 15 | "eslint.validate": [ 16 | "javascript", 17 | "typescript", 18 | "javascriptreact", 19 | "typescriptreact" 20 | ], 21 | "prettier.requireConfig": true, 22 | 23 | // --- 🎨 Tailwind CSS support --- 24 | "css.lint.unknownAtRules": "ignore", 25 | "scss.lint.unknownAtRules": "ignore", 26 | "less.lint.unknownAtRules": "ignore", 27 | "files.associations": { 28 | "*.css": "tailwindcss" 29 | }, 30 | "tailwindCSS.emmetCompletions": true, 31 | "tailwindCSS.experimental.classRegex": [ 32 | ["className\\s*=\\s*\"([^\"]*)", 1], 33 | ["className\\s*=\\s*{`([^`]*)", 1], 34 | ["class\\s*=\\s*\"([^\"]*)", 1] 35 | ], 36 | "tailwindCSS.experimental.configFile": "packages/theme/globalTailwind.css", 37 | "tailwindCSS.includeLanguages": { 38 | "javascript": "javascript", 39 | "javascriptreact": "javascript", 40 | "typescript": "typescript", 41 | "typescriptreact": "typescript" 42 | }, 43 | 44 | // --- 💬 Quick suggestions --- 45 | "editor.quickSuggestions": { 46 | "strings": true 47 | }, 48 | 49 | // --- 🧭 Path Autocomplete: alias path completions --- 50 | "path-autocomplete.pathMappings": { 51 | "@pawhaven/ui/*": "${workspaceFolder}/packages/ui/*", 52 | "@pawhaven/shared-frontend/*": "${workspaceFolder}/packages/shared-frontend/*", 53 | "@pawhaven/shared-backend/*": "${workspaceFolder}/packages/shared-backend/*" 54 | }, 55 | "path-autocomplete.extensionOnImport": true 56 | } 57 | -------------------------------------------------------------------------------- /READMECN.MD: -------------------------------------------------------------------------------- 1 | ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white) 2 | ![React](https://img.shields.io/badge/React-20232a?logo=react&logoColor=61dafb) 3 | ![Node.js](https://img.shields.io/badge/Node.js-43853D?logo=node.js&logoColor=white) 4 | ![NestJS](https://img.shields.io/badge/NestJS-E0234E?logo=nestjs&logoColor=white) 5 | ![pnpm](https://img.shields.io/badge/Package-pnpm-F69220?logo=pnpm&logoColor=white) 6 | ![License](https://img.shields.io/github/license/aoda-zhang/fullStack-frontEnd) 7 | 8 | [![logo.png](https://i.postimg.cc/XvtGZv64/logo.png)](https://postimg.cc/f3jTpDbr) 9 | 10 | ## 🌟 这是什么? 11 | 12 | PawHaven 是一个基于 React 和 Node.js(NestJS) 的全栈项目。通过这个项目,你不仅可以学习和实践 React 与 NestJS 的生态开发与最佳实践,还能全面掌握基于 JavaScript 的全栈开发和 DevOps 能力。与此同时,PawHaven 也是一个致力于流浪动物救助的公益项目,我希望用技术让更多人能够参与到救助行动中。目前项目由我独立维护,欢迎志同道合的朋友加入,共同让 PawHaven 真正帮助到流浪动物。 13 | 14 | --- 15 | 16 | ## 🚀 核心功能 17 | 18 | - **记录与分享:** 快速记录身边流浪动物的发现,上传照片和定位信息。 19 | - **社区救助:** 浏览附近的救助机会,主动报名参与行动。 20 | - **用户注册与个人主页:** 注册成为 PawHaven 成员,跟踪自己的救助记录。 21 | - **短链分享:** 通过短链接或邮件轻松分享救助故事和动物信息。 22 | - **响应式设计:** 移动端与桌面端均可流畅使用。 23 | 24 | --- 25 | 26 | ## 🛠️ 技术栈 27 | 28 | - **前端:** React、Redux、TypeScript、Tailwind CSS、React Hook Form 29 | - **后端:** NestJS、Node.js、REST & RPC API 30 | - **数据库:** MongoDB 31 | - **认证:** 基于 JWT 的身份验证与角色管理 32 | - **部署:** Kubernetes、Docker、GitHub Actions CI/CD 33 | 34 | 参与贡献,你将获得与前沿技术栈的亲身实践,学习如何构建可扩展、易维护的应用。 35 | 36 | --- 37 | 38 | ## 🤝 加入关爱社区 39 | 40 | 你热爱动物并对技术充满热情吗?欢迎加入我们!PawHaven 向所有开发者、设计师、动物爱好者及相信科技有温度的人敞开大门。 41 | 42 | 无论是代码、设计、测试,还是宣传推广,你的每一份贡献都让项目更强大,让世界更温暖。 43 | 44 | 让我们一起,为每一只需要帮助的流浪动物打造安全港湾。 45 | 46 | --- 47 | 48 | ## 📩 如何参与贡献 49 | 50 | 1. **Fork 本仓库** 并创建你的功能分支。 51 | 2. **提交 Pull Request**,请详细描述你的改进内容。 52 | 3. **通过 GitHub Issues** 报告问题或提出新功能建议。 53 | 4. 多多宣传 PawHaven,邀请更多朋友参与! 54 | 55 | --- 56 | 57 | ## 🔗 相关链接 58 | 59 | - GitHub 仓库:https://github.com/aoda-zhang/PawHaven 60 | - 联系方式:请在 GitHub 上提交 issue 或 pull request。 61 | 62 | --- 63 | -------------------------------------------------------------------------------- /apps/backend/gateway/.npmrc: -------------------------------------------------------------------------------- 1 | enable-scripts=true -------------------------------------------------------------------------------- /apps/backend/gateway/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "assets": [ 7 | { 8 | "include": "config/**", 9 | "exclude": "config/index.ts", 10 | "watchAssets": true 11 | } 12 | ] 13 | }, 14 | "generateOptions": { 15 | "spec": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import { AuthModule } from '@modules/Auth/auth.module'; 4 | import { Module } from '@nestjs/common'; 5 | import { APP_GUARD } from '@nestjs/core'; 6 | import SharedModule from '@shared/shared.module'; 7 | import { DocumentModule } from '@modules/Document/document.module'; 8 | import { EnvConstant } from '@shared/constants/constant'; 9 | import { JwtModule } from '@nestjs/jwt'; 10 | import { SignGuard } from '@modules/Auth/guards/Sign.guard'; 11 | import { JWTGuard } from '@modules/Auth/guards/JWT.guard'; 12 | // import ACLGuard from '@modules/ACL/middlewares/ACL.guard' 13 | const currentEnv = process.env.NODE_ENV ?? 'uat'; 14 | const configFilePath = path.resolve( 15 | __dirname, 16 | `./config/${EnvConstant[currentEnv]}/env/index.yaml`, 17 | ); 18 | 19 | @Module({ 20 | imports: [ 21 | SharedModule.forRoot({ 22 | configFilePath, 23 | isIntergrateHttpExceptionFilter: true, 24 | isIntergrateHttpInterceptor: true, 25 | }), 26 | JwtModule, 27 | DocumentModule, 28 | AuthModule, 29 | ], 30 | controllers: [], 31 | providers: [ 32 | { 33 | provide: APP_GUARD, 34 | useClass: SignGuard, 35 | }, 36 | { 37 | provide: APP_GUARD, 38 | useClass: JWTGuard, 39 | }, 40 | // { 41 | // provide: APP_GUARD, 42 | // useClass: ACLGuard 43 | // } 44 | ], 45 | }) 46 | export class AppModule {} 47 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/main.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, ValidationPipe, VersioningType } from '@nestjs/common' 2 | import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface' 3 | import { ConfigService } from '@nestjs/config' 4 | import { NestFactory } from '@nestjs/core' 5 | import { NestExpressApplication } from '@nestjs/platform-express' 6 | import helmet from 'helmet' 7 | // import { Logger } from 'nestjs-pino' 8 | 9 | import initSwagger from '@shared/core/swagger' 10 | import { EnvConstant } from '@shared/constants/constant' 11 | import { AppModule } from './app.module' 12 | 13 | const currentENV = process.env.NODE_ENV 14 | async function bootstrap() { 15 | const app = await NestFactory.create(AppModule, { 16 | bufferLogs: true 17 | }) 18 | const corsOptions: CorsOptions = app.get(ConfigService).get('cors') 19 | app.enableCors(corsOptions) 20 | // app.useLogger(app.get(Logger)) 21 | const prefix = app.get(ConfigService).get('http.prefix') ?? '' 22 | app.setGlobalPrefix(prefix) 23 | // Version control like v1 v2 24 | app.enableVersioning({ 25 | type: VersioningType.URI 26 | }) 27 | app.useGlobalPipes( 28 | new ValidationPipe({ 29 | transform: true, 30 | whitelist: true, 31 | forbidNonWhitelisted: true, 32 | exceptionFactory: (errors) => { 33 | return new BadRequestException(errors) 34 | } 35 | }) 36 | ) 37 | await initSwagger(app) 38 | app.use(helmet()) 39 | const port = app.get(ConfigService).get('http.port') ?? 8080 40 | await app 41 | .listen(port, '0.0.0.0', () => { 42 | ;[EnvConstant.dev, EnvConstant.uat]?.includes(currentENV?.toUpperCase()) && 43 | console.log(`Successfully runing on local http://localhost:${port}`) 44 | }) 45 | .catch((error) => { 46 | console.error(`Running failed on local with error : ${error}`) 47 | }) 48 | } 49 | bootstrap() 50 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/modules/Auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import CreateUserDTO from '@shared/DTO/Auth/create-user.dto'; 3 | import { Schema } from 'mongoose'; 4 | 5 | import { AuthService } from './auth.service'; 6 | import NoToken from './decorators/noToken.decorator'; 7 | 8 | @Controller('auth') 9 | export class AuthController { 10 | constructor(private readonly authService: AuthService) {} 11 | 12 | @NoToken() 13 | @Post('v1/register') 14 | register(@Body() userInfo: CreateUserDTO) { 15 | return this.authService.register(userInfo); 16 | } 17 | 18 | @NoToken() 19 | @Post('v1/login') 20 | async login(@Body() userInfo: { userName: string; password: string }) { 21 | return this.authService.login(userInfo?.userName, userInfo?.password); 22 | } 23 | 24 | @NoToken() 25 | @Post('v1/refresh') 26 | async refresh( 27 | @Body() 28 | body: { 29 | refreshToken: { userName: string; userID: Schema.Types.ObjectId }; 30 | }, 31 | ) { 32 | return this.authService.refresh(body?.refreshToken); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/modules/Auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AuthController } from './auth.controller'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Module({ 7 | imports: [], 8 | controllers: [AuthController], 9 | providers: [AuthService], 10 | exports: [AuthService], 11 | }) 12 | export class AuthModule {} 13 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/modules/Auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Inject, Injectable } from '@nestjs/common'; 2 | import { ClientProxy } from '@nestjs/microservices'; 3 | import { MSClientNames } from '@shared/constants/constant'; 4 | import AuthMessagePattern from '@shared/constants/MSMessagePatterns/auth.messagePattern'; 5 | import CreateUserDTO from '@shared/DTO/Auth/create-user.dto'; 6 | import { Schema } from 'mongoose'; 7 | 8 | @Injectable() 9 | export class AuthService { 10 | constructor( 11 | @Inject(MSClientNames.MS_AUTH) 12 | private readonly authClient: ClientProxy, 13 | ) {} 14 | 15 | register = async (userInfo: CreateUserDTO) => { 16 | try { 17 | return await this.authClient.send(AuthMessagePattern.REGISTER, userInfo); 18 | } catch (error) { 19 | throw new BadRequestException(`register failed :${error}`); 20 | } 21 | }; 22 | 23 | login = async (userName: string, password: string) => { 24 | try { 25 | return await this.authClient.send(AuthMessagePattern.LOGIN, { 26 | userName, 27 | password, 28 | }); 29 | } catch (error) { 30 | throw new BadRequestException(`Login failed:${error}`); 31 | } 32 | }; 33 | 34 | refresh = async (refreshToken: { 35 | userName: string; 36 | userID: Schema.Types.ObjectId; 37 | }) => { 38 | try { 39 | return await this.authClient.send(AuthMessagePattern.LOGIN, refreshToken); 40 | } catch (error) { 41 | throw new BadRequestException(`generate refresh token failed :${error}`); 42 | } 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/modules/Auth/decorators/noSIgn.decorator.ts: -------------------------------------------------------------------------------- 1 | // No need Sign validation 2 | import { CustomDecorator, SetMetadata } from '@nestjs/common'; 3 | import { Decorators } from '@shared/constants/enum'; 4 | 5 | const NoSign = (): CustomDecorator => 6 | SetMetadata(Decorators.noSign, true); 7 | export default NoSign; 8 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/modules/Auth/decorators/noToken.decorator.ts: -------------------------------------------------------------------------------- 1 | // No token verify route 2 | import { SetMetadata } from '@nestjs/common'; 3 | import { Decorators } from '@shared/constants/enum'; 4 | 5 | const NoToken: () => MethodDecorator = () => 6 | SetMetadata(Decorators.noToken, true); 7 | export default NoToken; 8 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/modules/Document/document.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { MicroServiceNames } from '@shared/constants/constant'; 3 | import NoToken from '@modules/Auth/decorators/noToken.decorator'; 4 | import NoSign from '@modules/Auth/decorators/noSIgn.decorator'; 5 | 6 | import { DocumentService } from './document.service'; 7 | 8 | @Controller(MicroServiceNames.DOCUMENT) 9 | export class DocumentController { 10 | constructor(private readonly documentService: DocumentService) {} 11 | 12 | @NoToken() 13 | @NoSign() 14 | @Get('/v1/default-trip-views') 15 | async getDefaultTripViews(): Promise { 16 | return this.documentService.getDefaultTripViews(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/modules/Document/document.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DocumentController } from './document.controller'; 4 | import { DocumentService } from './document.service'; 5 | 6 | @Module({ 7 | imports: [], 8 | controllers: [DocumentController], 9 | providers: [DocumentService], 10 | exports: [DocumentService], 11 | }) 12 | export class DocumentModule {} 13 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/modules/Document/document.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import HttpClientService from '@shared/core/httpClient/HttpClient.service'; 4 | 5 | @Injectable() 6 | export class DocumentService { 7 | private readonly httpClient; 8 | 9 | constructor( 10 | private readonly http: HttpClientService, 11 | private readonly configService: ConfigService, 12 | ) { 13 | this.httpClient = this.http.create( 14 | this.configService.get('documentService.baseURL') || '', 15 | ); 16 | } 17 | 18 | getDefaultTripViews = async () => { 19 | try { 20 | return await this.httpClient.get('/file/v1/default-trip-views'); 21 | } catch (error) { 22 | throw new Error(`Error fetching default trip views: ${error}`); 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/modules/HealthCheck/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { HealthCheck } from '@nestjs/terminus'; 4 | 5 | import NoToken from '../Auth/decorators/noToken.decorator'; 6 | 7 | import { HealthService } from './health.service'; 8 | 9 | @Controller() 10 | @ApiTags('System health check') 11 | export class HealthController { 12 | constructor(private readonly health: HealthService) {} 13 | 14 | @NoToken() 15 | @Get('/health') 16 | @HealthCheck() 17 | healthChecker() { 18 | return this.health.healthChecker(); 19 | } 20 | 21 | @NoToken() 22 | @Get('/ping') 23 | ping() { 24 | return this.health.ping(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/backend/gateway/src/modules/HealthCheck/health.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TerminusModule } from '@nestjs/terminus'; 3 | 4 | import { HealthController } from './health.controller'; 5 | import { HealthService } from './health.service'; 6 | 7 | @Module({ 8 | imports: [TerminusModule], 9 | controllers: [HealthController], 10 | providers: [HealthService], 11 | exports: [HealthService], 12 | }) 13 | export class HealthCheckModule {} 14 | -------------------------------------------------------------------------------- /apps/backend/gateway/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": "", 5 | "outDir": "build", 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "paths": { 9 | "@shared/*": ["../../../packages/shared-backend/*"], 10 | "@modules/*": ["src/modules/*"] 11 | } 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/.npmrc: -------------------------------------------------------------------------------- 1 | enable-scripts=true -------------------------------------------------------------------------------- /apps/backend/ms-auth/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "assets": [ 7 | { 8 | "include": "config/**", 9 | "exclude": "config/index.ts", 10 | "watchAssets": true 11 | } 12 | ] 13 | }, 14 | "generateOptions": { 15 | "spec": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/package", 3 | "private": true, 4 | "name": "@pawhaven/auth", 5 | "main": "build/main.js", 6 | "version": "1.0.0", 7 | "engines": { 8 | "node": ">=22", 9 | "pnpm": ">=10" 10 | }, 11 | "scripts": { 12 | "start:dev": "cross-env NODE_ENV=dev nest start --watch", 13 | "uat": "cross-env NODE_ENV=uat nest start --watch", 14 | "build": "nest build", 15 | "start": "NODE_ENV=uat node build/main.js" 16 | }, 17 | "dependencies": { 18 | "@nestjs/axios": "^4.0.1", 19 | "@nestjs/common": "^11.1.6", 20 | "@nestjs/config": "^4.0.2", 21 | "@nestjs/core": "^11.1.6", 22 | "@nestjs/jwt": "^11.0.1", 23 | "@nestjs/microservices": "^11.1.6", 24 | "@nestjs/mongoose": "^11.0.3", 25 | "@nestjs/passport": "^11.0.5", 26 | "@nestjs/platform-express": "^11.1.6", 27 | "@nestjs/swagger": "^11.2.0", 28 | "@nestjs/terminus": "^11.0.0", 29 | "@nestjs/throttler": "^6.4.0", 30 | "@types/bcrypt": "^6.0.0", 31 | "@types/express": "^5.0.3", 32 | "@types/passport-jwt": "^4.0.1", 33 | "add": "^2.0.6", 34 | "axios": "^1.12.2", 35 | "bcrypt": "^6.0.0", 36 | "class-transformer": "^0.5.1", 37 | "class-validator": "^0.14.2", 38 | "cookie-parser": "^1.4.7", 39 | "cross-env": "^10.1.0", 40 | "crypto-js": "^4.2.0", 41 | "csurf": "^1.11.0", 42 | "dayjs": "^1.11.18", 43 | "dotenv": "^17.2.3", 44 | "helmet": "^8.1.0", 45 | "js-yaml": "^4.1.0", 46 | "mongoose": "^8.19.1", 47 | "passport": "^0.7.0", 48 | "passport-jwt": "^4.0.1", 49 | "passport-local": "^1.0.0", 50 | "puppeteer": "^24.24.0", 51 | "reflect-metadata": "^0.2.2", 52 | "rxjs": "^7.8.2", 53 | "swagger-ui-express": "^5.0.1", 54 | "uuid": "^13.0.0" 55 | }, 56 | "devDependencies": { 57 | "@nestjs/cli": "^11.0.10", 58 | "@nestjs/schematics": "^11.0.9", 59 | "@nestjs/testing": "^11.1.6", 60 | "@types/crypto-js": "^4.2.2", 61 | "@types/jest": "^30.0.0", 62 | "@types/js-yaml": "^4.0.9", 63 | "@types/node": "^24.7.1", 64 | "@types/passport-local": "^1.0.38", 65 | "@types/supertest": "^6.0.3", 66 | "source-map-support": "^0.5.21", 67 | "supertest": "^7.1.4", 68 | "ts-loader": "^9.5.4", 69 | "ts-node": "^10.9.2", 70 | "tsconfig-paths": "^4.2.0", 71 | "typescript": "^5.9.3" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import ACLModule from '@modules/ACL/ACLs.module' 3 | import AuthModule from '@modules/Auth/auth.module' 4 | import { Module } from '@nestjs/common' 5 | import SharedModule from '@shared/shared.module' 6 | import { EnvConstant } from '@shared/constants/constant' 7 | import UserModule from '@modules/User/user.module' 8 | const currentEnv = process.env.NODE_ENV ?? 'uat' 9 | const configFilePath = path.resolve(__dirname, `./config/${EnvConstant[currentEnv]}/env/index.yaml`) 10 | 11 | @Module({ 12 | imports: [ 13 | SharedModule.forRoot({ 14 | configFilePath 15 | }), 16 | UserModule, 17 | AuthModule, 18 | ACLModule 19 | ] 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import { AppModule } from './app.module' 3 | import { MicroserviceOptions, Transport } from '@nestjs/microservices' 4 | import { ConfigService } from '@nestjs/config' 5 | import { AllRpcExceptionsFilter } from '@shared/core/httpClient/rpcExceptionFillter' 6 | 7 | async function bootstrap() { 8 | const appContext = await NestFactory.createApplicationContext(AppModule) 9 | const configService = appContext.get(ConfigService) 10 | 11 | const port = configService.get('http.port', 8083) 12 | const host = configService.get('http.host', '0.0.0.0') 13 | 14 | const app = await NestFactory.createMicroservice(AppModule, { 15 | transport: Transport.TCP, 16 | options: { 17 | host, 18 | port 19 | } 20 | }) 21 | app.useGlobalFilters(new AllRpcExceptionsFilter()) 22 | 23 | await app.listen() 24 | } 25 | bootstrap() 26 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/models/auth.DBcollection.ts: -------------------------------------------------------------------------------- 1 | export const AuthDBCollections = { 2 | PERMISSION: 'Permissions', 3 | USER: 'Users', 4 | ROLE: 'Roles', 5 | RESOURCE: 'Resources', 6 | ROLE_PERMISSON: 'RolePermission', 7 | }; 8 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/models/common.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop } from '@nestjs/mongoose'; 2 | import { Document, type Schema } from 'mongoose'; 3 | 4 | export default class CommonSchema extends Document { 5 | // 显示声明 6 | declare _id: Schema.Types.ObjectId; 7 | 8 | // 无需返回给前端响应的字段 9 | @Prop({ 10 | select: false, 11 | }) 12 | declare __v: number; 13 | 14 | @Prop({ 15 | select: false, 16 | }) 17 | createdAt: Date; 18 | 19 | @Prop({ 20 | select: false, 21 | }) 22 | updatedAt: Date; 23 | 24 | @Prop({ 25 | required: false, 26 | select: false, 27 | default: false, 28 | }) 29 | isRemove: boolean; 30 | } 31 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/models/permission.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop } from '@nestjs/mongoose'; 2 | 3 | export enum ResourceType { 4 | MENU = 'MENU', 5 | PAGE = 'PAGE', 6 | BUTTON = 'BUTTON', 7 | } 8 | export class Permission { 9 | @Prop({ 10 | required: true, 11 | unique: true, 12 | type: String, 13 | }) 14 | name: string; 15 | 16 | @Prop({ 17 | required: true, 18 | type: String, 19 | }) 20 | desc: string; 21 | 22 | @Prop({ 23 | required: true, 24 | enum: Object.values(ResourceType), 25 | }) 26 | type: string; 27 | } 28 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/models/resource.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | 3 | import AuthDBCollections from './auth.DBcollection'; 4 | import CommonSchema from './common.schema'; 5 | import { Permission } from './permission.schema'; 6 | 7 | @Schema({ collection: AuthDBCollections.RESOURCE, timestamps: true }) 8 | export class Resource extends CommonSchema { 9 | @Prop({ 10 | required: true, 11 | unique: true, 12 | type: String, 13 | }) 14 | name: string; 15 | 16 | @Prop({ 17 | required: true, 18 | type: String, 19 | }) 20 | desc: string; 21 | 22 | @Prop({ 23 | required: true, 24 | type: Permission, 25 | }) 26 | permissions: Permission[]; 27 | } 28 | export const ResourceSchema = SchemaFactory.createForClass(Resource); 29 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/models/role.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | 3 | import AuthDBCollections from './auth.DBcollection'; 4 | import CommonSchema from './common.schema'; 5 | 6 | @Schema({ collection: AuthDBCollections.ROLE, timestamps: true }) 7 | export class Role extends CommonSchema { 8 | @Prop({ 9 | required: true, 10 | unique: true, 11 | type: String, 12 | }) 13 | name: string; 14 | 15 | @Prop({ 16 | required: true, 17 | type: String, 18 | }) 19 | desc: string; 20 | 21 | @Prop({ 22 | required: true, 23 | type: [String], 24 | }) 25 | permissions: string[]; 26 | } 27 | export const RoleSchema = SchemaFactory.createForClass(Role); 28 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/models/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | 3 | import AuthDBCollections from './auth.DBcollection'; 4 | import CommonSchema from './common.schema'; 5 | 6 | @Schema({ collection: AuthDBCollections.USER, timestamps: true }) 7 | export class User extends CommonSchema { 8 | @Prop({ 9 | required: true, 10 | unique: true, 11 | type: String, 12 | trim: true, 13 | minlength: 2, 14 | }) 15 | userName: string; 16 | 17 | @Prop({ 18 | required: true, 19 | trim: true, 20 | minlength: 2, 21 | }) 22 | password: string; 23 | 24 | @Prop({ 25 | required: true, 26 | unique: true, 27 | type: String, 28 | }) 29 | salt: string; 30 | 31 | @Prop({ 32 | required: true, 33 | // type: String, 34 | default: ['GUEST'], 35 | }) 36 | roles: string[]; 37 | } 38 | export const UserSchema = SchemaFactory.createForClass(User); 39 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/ACL/ACLs.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Put, Req } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | 4 | import ACLService from './ACLs.service'; 5 | import ResourceDTO from './dto/resource.dto'; 6 | import RoleUpdateDTO from './dto/role-update'; 7 | import RoleDTO from './dto/role.dto'; 8 | import ACLPermissions from './middlewares/ACL.decorator'; 9 | 10 | @ApiTags('ACL module') 11 | @Controller('ACL') 12 | export class ACLController { 13 | constructor(private ACL: ACLService) {} 14 | 15 | @Get('/role/permissions') 16 | getRolePermissions(@Req() req: { user: { roles: string[] } }) { 17 | return this.ACL.getRolePermissions(req?.user?.roles); 18 | } 19 | 20 | @ACLPermissions(['ROLE_ADD']) 21 | @Post('/role/add') 22 | addRole(@Body() role: RoleDTO) { 23 | return this.ACL.addRoles(role); 24 | } 25 | 26 | @ACLPermissions(['ROLE_UPDATE']) 27 | @Put('/role/update') 28 | updateRolePermission(@Body() roles: RoleUpdateDTO) { 29 | return this.ACL.updateRolePermission(roles); 30 | } 31 | 32 | @ACLPermissions(['RES_ADD']) 33 | @Post('/resource/add') 34 | addResource(@Body() resource: ResourceDTO | ResourceDTO[]) { 35 | return this.ACL.addResource(resource); 36 | } 37 | 38 | @ACLPermissions(['RES_UPDATE']) 39 | @Put('/resource/update') 40 | updateResourcePermission(@Body() resource: ResourceDTO) { 41 | return this.ACL.updateResourcePermission(resource); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/ACL/ACLs.module.ts: -------------------------------------------------------------------------------- 1 | import { ResourceSchema } from '@models/resource.schema'; 2 | import { RoleSchema } from '@models/role.schema'; 3 | import { UserSchema } from '@models/user.schema'; 4 | import { Module } from '@nestjs/common'; 5 | import { MongooseModule } from '@nestjs/mongoose'; 6 | import GatewayDBCollections from 'src/models/auth.DBcollection'; 7 | 8 | import ACLController from './ACLs.controller'; 9 | import ACLService from './ACLs.service'; 10 | 11 | @Module({ 12 | imports: [ 13 | MongooseModule.forFeature([ 14 | { name: GatewayDBCollections.ROLE, schema: RoleSchema }, 15 | { name: GatewayDBCollections.RESOURCE, schema: ResourceSchema }, 16 | { name: GatewayDBCollections.USER, schema: UserSchema }, 17 | ]), 18 | ], 19 | controllers: [ACLController], 20 | providers: [ACLService], 21 | exports: [ACLService], 22 | }) 23 | export class ACLModule {} 24 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/ACL/dto/permission.dto.ts: -------------------------------------------------------------------------------- 1 | import { ResourceType } from '@modules/permission.schema'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | import { IsEnum, IsNotEmpty, IsString, MaxLength } from 'class-validator'; 5 | 6 | export class PermissionDTO { 7 | @ApiProperty({ description: '权限分类', required: true }) 8 | @IsNotEmpty() 9 | @IsEnum(ResourceType, { message: '无效的权限分类' }) 10 | type!: ResourceType; 11 | 12 | @ApiProperty({ description: '权限名称', required: true }) 13 | @IsString() 14 | @IsNotEmpty() 15 | @Type(() => String) 16 | @MaxLength(50, { message: '权限名称最多为50' }) 17 | name!: string; 18 | 19 | @ApiProperty({ description: '权限描述', required: true }) 20 | @IsNotEmpty() 21 | @IsString() 22 | @Type(() => String) 23 | @MaxLength(200, { message: '权限描述最多为200' }) 24 | desc: string; 25 | } 26 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/ACL/dto/resource.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Type } from 'class-transformer'; 3 | import { 4 | IsArray, 5 | IsDefined, 6 | IsNotEmpty, 7 | IsOptional, 8 | IsString, 9 | MaxLength, 10 | ValidateNested, 11 | } from 'class-validator'; 12 | 13 | import { PermissionDTO } from './permission.dto'; 14 | 15 | export class ResourceDTO { 16 | @ApiProperty({ description: '资源名称', required: true }) 17 | @IsNotEmpty() 18 | @IsString() 19 | @Type(() => String) 20 | @MaxLength(50, { message: '资源名称最多为50' }) 21 | name: string | undefined; 22 | 23 | @ApiProperty({ description: '资源描述' }) 24 | @IsOptional() 25 | @MaxLength(500, { message: '资源描述最多为500' }) 26 | desc?: string; 27 | 28 | @ApiProperty({ description: '权限列表', required: true }) 29 | @ValidateNested({ each: true }) 30 | @IsArray() 31 | @IsDefined() 32 | @Type(() => PermissionDTO) 33 | permissions: PermissionDTO[] | undefined; 34 | } 35 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/ACL/dto/role-update.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Type } from 'class-transformer'; 3 | import { ArrayNotEmpty, IsArray, IsString } from 'class-validator'; 4 | 5 | export class RoleUpdateDTO { 6 | @ApiProperty({ description: '角色名称' }) 7 | @IsString() 8 | @Type(() => String) 9 | name: string; 10 | 11 | @ApiProperty({ description: '角色权限列表' }) 12 | @IsArray() 13 | @ArrayNotEmpty() 14 | @Type(() => String) 15 | permissions: string[]; 16 | } 17 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/ACL/dto/role.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Type } from 'class-transformer'; 3 | import { 4 | ArrayNotEmpty, 5 | IsArray, 6 | IsNotEmpty, 7 | IsString, 8 | MaxLength, 9 | } from 'class-validator'; 10 | 11 | export class RoleDTO { 12 | @ApiProperty({ description: '角色名称' }) 13 | @IsString() 14 | @Type(() => String) 15 | @MaxLength(50, { message: '角色名称最多为50' }) 16 | name: string; 17 | 18 | @ApiProperty({ description: '角色描述' }) 19 | @IsNotEmpty({ message: '目的地名称为必填项' }) 20 | @IsString() 21 | @Type(() => String) 22 | @MaxLength(200, { message: '角色描述最多为200' }) 23 | desc: string; 24 | 25 | @ApiProperty({ description: '角色权限列表' }) 26 | @IsArray() 27 | @ArrayNotEmpty() 28 | @Type(() => String) 29 | permissions: string[]; 30 | } 31 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/ACL/middlewares/ACL.decorator.ts: -------------------------------------------------------------------------------- 1 | // ACL permission verifcation 2 | import { SetMetadata } from '@nestjs/common'; 3 | import { Decorators } from '@shared/constants/enum'; 4 | 5 | const ACLPermissions = (permissions: string[]) => 6 | SetMetadata(Decorators.ACLPermissions, permissions); 7 | export default ACLPermissions; 8 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/ACL/middlewares/ACL.guard.ts: -------------------------------------------------------------------------------- 1 | import ACLService from '@modules/ACL/ACLs.service'; 2 | import { 3 | BadRequestException, 4 | type CanActivate, 5 | type ExecutionContext, 6 | Injectable, 7 | } from '@nestjs/common'; 8 | import { ConfigService } from '@nestjs/config'; 9 | import { Reflector } from '@nestjs/core'; 10 | import { Decorators } from '@shared/constants/enum'; 11 | import { 12 | HttpBusinessCode, 13 | HttpBusinessMappingCode, 14 | } from '@shared/core/httpClient/interface'; 15 | import trime from '@shared/utils/trime'; 16 | 17 | @Injectable() 18 | export class ACLGuard implements CanActivate { 19 | constructor( 20 | private reflector: Reflector, 21 | private ACL: ACLService, 22 | private configService: ConfigService, 23 | ) {} 24 | 25 | async canActivate(context: ExecutionContext): Promise { 26 | const request = context.switchToHttp().getRequest(); 27 | const response = context.switchToHttp().getResponse(); 28 | const aclPermissions = this.reflector.getAllAndOverride( 29 | Decorators.ACLPermissions, 30 | [context.getHandler(), context.getClass()], 31 | ); 32 | try { 33 | // WARN: The root user have the highst poewer, pls opreat carefully!! 34 | if ( 35 | !aclPermissions || 36 | request?.user?.roles?.includes(this.configService.get('auth.rootUser')) 37 | ) { 38 | return true; 39 | } 40 | const currentUserPermissions = await this.ACL.getRolePermissions( 41 | request?.user?.roles, 42 | ); 43 | return aclPermissions?.every((item) => 44 | currentUserPermissions?.includes(item), 45 | ); 46 | } catch (error) { 47 | switch (trime(error?.message)) { 48 | case HttpBusinessCode.jwtexpired || HttpBusinessCode.invalidToken: 49 | response.data = HttpBusinessMappingCode.jwtexpired; 50 | break; 51 | default: 52 | break; 53 | } 54 | throw new BadRequestException(`error:${error}`); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/Auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common' 2 | import CreateUserDTO from '@modules/User/dto/create-user.dto' 3 | import AuthService from './auth.service' 4 | import { MessagePattern, Payload } from '@nestjs/microservices' 5 | import AuthMessagePattern from '@shared/constants/MSMessagePatterns/auth.messagePattern' 6 | 7 | @Controller() 8 | export class AuthController { 9 | constructor(private readonly authService: AuthService) {} 10 | 11 | @MessagePattern(AuthMessagePattern.REGISTER) 12 | register(@Payload() userInfo: CreateUserDTO) { 13 | return this.authService.register(userInfo) 14 | } 15 | 16 | @MessagePattern(AuthMessagePattern.LOGIN) 17 | async login(@Payload() userInfo: { userName: string; password: string }) { 18 | return this.authService.login(userInfo?.userName, userInfo?.password) 19 | } 20 | 21 | @MessagePattern(AuthMessagePattern.REFRESH) 22 | async refresh(@Payload() refreshToken: string) { 23 | const tokenInfo = await this.authService.verifyRefreshToken(refreshToken) 24 | return this.authService.refresh(tokenInfo) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/Auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import AuthDBCollections from '@models/auth.DBcollection'; 2 | import { UserSchema } from '@models/user.schema'; 3 | import ACLModule from '@modules/ACL/ACLs.module'; 4 | import UserModule from '@modules/User/user.module'; 5 | import { Module } from '@nestjs/common'; 6 | import { JwtModule } from '@nestjs/jwt'; 7 | import { MongooseModule } from '@nestjs/mongoose'; 8 | 9 | import { AuthController } from './auth.controller'; 10 | import AuthService from './auth.service'; 11 | 12 | @Module({ 13 | imports: [ 14 | MongooseModule.forFeature([ 15 | { name: AuthDBCollections.USER, schema: UserSchema }, 16 | ]), 17 | JwtModule, 18 | ACLModule, 19 | UserModule, 20 | ], 21 | controllers: [AuthController], 22 | providers: [AuthService], 23 | exports: [AuthService], 24 | }) 25 | export class AuthModule {} 26 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/User/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Type } from 'class-transformer'; 3 | import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; 4 | 5 | export class CreateUserDTO { 6 | // 必选项描述 7 | @ApiProperty({ description: '用户姓名', default: '二狗' }) // 默认值设置 8 | @IsNotEmpty({ message: '姓名为必填项' }) 9 | @IsString() 10 | @Type(() => String) 11 | @MinLength(2, { message: '姓名最小长度为2' }) 12 | @MaxLength(10, { message: '姓名最大长度为10' }) 13 | readonly userName: string; 14 | 15 | @ApiProperty({ description: '密码' }) 16 | @IsNotEmpty({ message: '密码为必填项' }) 17 | @IsString() 18 | @Type(() => String) 19 | readonly password: string; 20 | } 21 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/User/dto/interface.ts: -------------------------------------------------------------------------------- 1 | import type { Schema } from 'mongoose' 2 | 3 | // token中需要记录的用户信息类型 4 | export interface UserAccessInfo { 5 | userName: string | undefined 6 | userID: Schema.Types.ObjectId | undefined 7 | roles: string[] | undefined 8 | } 9 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/User/dto/userInfo.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Type } from 'class-transformer'; 3 | import { 4 | IsArray, 5 | IsNotEmpty, 6 | IsString, 7 | MaxLength, 8 | MinLength, 9 | } from 'class-validator'; 10 | import type { Schema } from 'mongoose'; 11 | 12 | export class UserInfoDTO { 13 | // 必选项描述 14 | @ApiProperty({ description: '用户姓名', default: '二狗' }) // 默认值设置 15 | @IsNotEmpty({ message: '姓名为必填项' }) 16 | @IsString() 17 | @Type(() => String) 18 | @MinLength(2, { message: '姓名最小长度为2' }) 19 | @MaxLength(10, { message: '姓名最大长度为10' }) 20 | readonly userName: string; 21 | 22 | @ApiProperty({ description: '用户ID' }) 23 | userID: Schema.Types.ObjectId; 24 | 25 | @ApiProperty({ description: '用户权限组' }) 26 | @IsArray() 27 | readonly roles?: string[]; 28 | } 29 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/User/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Put } from '@nestjs/common' 2 | 3 | import ACLPermissions from '@modules/ACL/middlewares/ACL.decorator' 4 | import UserService from './user.service' 5 | 6 | @Controller('user') 7 | export class UserController { 8 | constructor(private userService: UserService) {} 9 | 10 | @ACLPermissions(['USER_ROLE_EDIT']) 11 | @Put('/role/update') 12 | addUserRoles(@Body() body: { userID: string; roles: string[] }) { 13 | return this.userService.addUserRoles(body?.userID, body?.roles) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/User/user.module.ts: -------------------------------------------------------------------------------- 1 | import AuthDBCollections from '@models/auth.DBcollection'; 2 | import { UserSchema } from '@models/user.schema'; 3 | import ACLModule from '@modules/ACL/ACLs.module'; 4 | import { Module } from '@nestjs/common'; 5 | import { MongooseModule } from '@nestjs/mongoose'; 6 | 7 | import { UserController } from './user.controller'; 8 | import { UserService } from './user.service'; 9 | 10 | @Module({ 11 | imports: [ 12 | MongooseModule.forFeature([ 13 | { name: AuthDBCollections.USER, schema: UserSchema }, 14 | ]), 15 | ACLModule, 16 | ], 17 | controllers: [UserController], 18 | providers: [UserService], 19 | exports: [UserService], 20 | }) 21 | export class UserModule {} 22 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/src/modules/User/user.service.ts: -------------------------------------------------------------------------------- 1 | import { AuthDBCollections } from '@models/auth.DBcollection'; 2 | import { ACLService } from '@modules/ACL/ACLs.service'; 3 | import { 4 | BadRequestException, 5 | Injectable, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { InjectModel } from '@nestjs/mongoose'; 9 | import { Model } from 'mongoose'; 10 | 11 | import { User } from '../../models/user.schema'; 12 | 13 | @Injectable() 14 | export class UserService { 15 | constructor( 16 | @InjectModel(AuthDBCollections.USER) private userModel: Model, 17 | private ACL: ACLService, 18 | ) {} 19 | 20 | async getUserInfo(userName: string) { 21 | try { 22 | const userInfo = await this.userModel.findOne({ userName }); 23 | if (!userInfo) { 24 | throw new UnauthorizedException('用户不存在'); 25 | } 26 | return userInfo; 27 | } catch (error) { 28 | throw new UnauthorizedException(`${error?.message}`); 29 | } 30 | } 31 | 32 | addUserRoles = async (userID: string, roles: string[]) => { 33 | try { 34 | const isRoleExisting = await this.ACL.isRoleExisting(roles); 35 | if (isRoleExisting) { 36 | await this.userModel.findByIdAndUpdate(userID, { roles }); 37 | return true; 38 | } 39 | throw new BadRequestException('role NOT exising!!'); 40 | } catch (error) { 41 | throw new BadRequestException(error); 42 | } 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /apps/backend/ms-auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": "", 5 | "outDir": "build", 6 | "paths": { 7 | "@shared/*": ["../../../packages/shared-backend/*"], 8 | "@modules/*": ["./src/modules/*"], 9 | "@models/*": ["./src/models//*"] 10 | } 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /apps/backend/ms-document/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "assets": [ 7 | { 8 | "include": "config", 9 | "watchAssets": true 10 | }, 11 | { "include": "i18n/*", "watchAssets": true } 12 | ] 13 | }, 14 | "generateOptions": { 15 | "spec": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { Module } from '@nestjs/common' 3 | import SharedModule from '@shared/shared.module' 4 | import EmailModule from './modules/Email/email.module' 5 | import PDFModule from './modules/PDF/PDF.module' 6 | import { EnvConstant } from '@shared/constants/constant' 7 | import FileModule from '@modules/File/file.module' 8 | const currentEnv = process.env.NODE_ENV ?? 'uat' 9 | const configFilePath = path.resolve(__dirname, `./config/${EnvConstant[currentEnv]}/env/index.yaml`) 10 | @Module({ 11 | imports: [ 12 | SharedModule.forRoot({ 13 | configFilePath 14 | }), 15 | EmailModule, 16 | PDFModule, 17 | FileModule 18 | ], 19 | providers: [] 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "common.brand": "Trip", 3 | "common.slogan": "Explore the world of possibilities", 4 | "trip_submit_failed.title": "Trip Submit Failed" 5 | } 6 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/i18n/i18n.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | 3 | import i18n from 'i18n'; 4 | 5 | i18n.configure({ 6 | locales: ['en', 'zh'], // List of supported languages 7 | directory: join(__dirname, '.'), // Path to translation files 8 | defaultLocale: 'en', // Default language 9 | autoReload: true, // Automatically reload translation files on changes 10 | updateFiles: false, // Prevents automatic creation of missing translation files 11 | syncFiles: false, // Disables synchronization of locale files 12 | }); 13 | 14 | export default i18n; 15 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/i18n/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "common.brand": "Trip", 3 | "common.slogan": "探索无限可能", 4 | "trip_submit_failed.title": "行程提交失败" 5 | } 6 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe, VersioningType } from '@nestjs/common' 2 | import { ConfigService } from '@nestjs/config' 3 | import { NestFactory } from '@nestjs/core' 4 | import { NestExpressApplication } from '@nestjs/platform-express' 5 | import helmet from 'helmet' 6 | import { AppModule } from './app.module' 7 | import { EnvConstant } from '@shared/constants/constant' 8 | import i18n from 'i18n' 9 | const currentENV = process.env.NODE_ENV 10 | async function bootstrap() { 11 | // Hybrid application 12 | // can be used as a microservice or a web service both 13 | const app = await NestFactory.create(AppModule, { 14 | bufferLogs: true 15 | }) 16 | 17 | // Initialize i18n 18 | app.use(i18n.init) 19 | 20 | // global service prefix 21 | const prefix = app.get(ConfigService).get('http.prefix') ?? '' 22 | app.setGlobalPrefix(prefix) 23 | 24 | // Version control like v1 v2 25 | app.enableVersioning({ 26 | type: VersioningType.URI 27 | }) 28 | 29 | // DTO pipe settings 30 | app.useGlobalPipes( 31 | new ValidationPipe({ 32 | transform: true, 33 | whitelist: true 34 | }) 35 | ) 36 | 37 | // avoid attack 38 | app.use(helmet()) 39 | 40 | const port = app.get(ConfigService).get('http.port') ?? 8082 41 | // As a web service 42 | await app 43 | .listen(port, '0.0.0.0', () => { 44 | ;[EnvConstant.dev, EnvConstant.uat].includes(currentENV?.toUpperCase()) && 45 | console.log(`MS_Document Running on local: http://localhost:${port}`) 46 | }) 47 | .catch((error) => { 48 | console.error(`MS_Document start error:${error}`) 49 | }) 50 | } 51 | bootstrap() 52 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/Email/email.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post, Req } from '@nestjs/common' 2 | import { EmailService } from './email.service' 3 | import EmailPayloadDTO from '@shared/DTO/Document/send-email.DTO' 4 | 5 | @Controller('email') 6 | export class EmailController { 7 | constructor(private readonly emailService: EmailService) {} 8 | 9 | @Post('/send') 10 | sendEmail(@Req() req, @Body() emailInfo: EmailPayloadDTO) { 11 | const userID = req?.user?.userID 12 | return this.emailService.sendMail(userID, emailInfo) 13 | } 14 | 15 | @Post('/preview') 16 | previewEmail(@Body() emailInfo: EmailPayloadDTO) { 17 | return this.emailService.getEmailHtml({ 18 | template: emailInfo?.template, 19 | payload: emailInfo?.payload, 20 | locale: emailInfo?.locale ?? 'en' 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/Email/email.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { MailerModule } from '@nestjs-modules/mailer'; 4 | 5 | import { EmailController } from './email.controller'; 6 | import { EmailService } from './email.service'; 7 | 8 | @Module({ 9 | imports: [ 10 | // MongooseModule.forFeature([{ name: DBCollection.HISTORY, schema: HistorySchema }]), 11 | MailerModule.forRootAsync({ 12 | imports: [ConfigModule], 13 | useFactory: async (configs: ConfigService) => ({ 14 | transport: { 15 | host: configs.get('email')?.host ?? '', 16 | port: configs.get('email')?.port ?? '', 17 | secureConnection: false, 18 | auth: { 19 | user: configs.get('email')?.user ?? '', 20 | pass: configs.get('email')?.password ?? '', 21 | }, 22 | tls: { 23 | ciphers: configs.get('email')?.tls?.ciphers ?? '', 24 | }, 25 | }, 26 | }), 27 | inject: [ConfigService], 28 | }), 29 | ], 30 | controllers: [EmailController], 31 | providers: [EmailService], 32 | exports: [EmailService], 33 | }) 34 | export class EmailModule {} 35 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/Email/email.service.ts: -------------------------------------------------------------------------------- 1 | import { ISendMailOptions, MailerService } from '@nestjs-modules/mailer' 2 | import { Injectable } from '@nestjs/common' 3 | import { ConfigService } from '@nestjs/config' 4 | import { render } from '@react-email/components' 5 | import EmailPayloadDTO from '@shared/DTO/Document/send-email.DTO' 6 | import i18n from '@i18n/i18n.config' 7 | 8 | @Injectable() 9 | export class EmailService { 10 | constructor( 11 | private readonly mailService: MailerService, 12 | private readonly configs: ConfigService 13 | ) {} 14 | async getEmailHtml({ 15 | template, 16 | payload, 17 | locale 18 | }: { template: string; payload: Record; locale: string }) { 19 | try { 20 | i18n.setLocale(locale) 21 | const { default: EmailTemplate } = require(`./templates/${template}`) 22 | return render(EmailTemplate(payload)) 23 | } catch (error) { 24 | console.log(error) 25 | throw new Error(`Failed to get email html with error: ${error}`) 26 | } 27 | } 28 | async sendMail(_userID: string, emailProps: EmailPayloadDTO) { 29 | try { 30 | const emailHtml = await this.getEmailHtml({ 31 | template: emailProps?.template, 32 | payload: emailProps?.payload, 33 | locale: emailProps?.locale ?? 'en' 34 | }) 35 | const options: ISendMailOptions = { 36 | ...(emailProps?.options ?? {}), 37 | from: this.configs.get('email')?.from, 38 | html: emailHtml 39 | } 40 | await this.mailService.sendMail(options) 41 | } catch (error) { 42 | console.log(error) 43 | throw new Error(`Failed to send email with error: ${error}`) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/Email/templates/trip_submit_failed.tsx: -------------------------------------------------------------------------------- 1 | import i18n from '@i18n/i18n.config'; 2 | import React from 'react'; 3 | 4 | interface TripSubmitFailedProps { 5 | username: string; 6 | } 7 | 8 | const TripSubmitFailed = ({ username }: TripSubmitFailedProps) => ( 9 |
10 | 11 |

12 | {i18n.__('trip_submit_failed.title')} 13 |

14 |

15 | Dear {username}, 16 |

17 |

18 | We're sorry to inform you that your trip registration was not successful. 19 | Please try again or contact our support team for assistance. 20 |

21 |

22 | If you have any questions, feel free to reach out to us. 23 |

24 |
25 | ); 26 | 27 | export default TripSubmitFailed; 28 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/File/file.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param } from '@nestjs/common' 2 | import { FileService } from './file.service' 3 | 4 | @Controller('file') 5 | export class FileController { 6 | constructor(private readonly fileService: FileService) {} 7 | 8 | @Get('/v1/default-trip-views') 9 | getTrip() { 10 | return this.fileService.getTripDefaultViews() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/File/file.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { FileController } from './file.controller'; 4 | import { FileService } from './file.service'; 5 | 6 | @Module({ 7 | // imports: [MongooseModule.forFeature([{ name: DBCollection.HISTORY, schema: HistorySchema }])], 8 | controllers: [FileController], 9 | providers: [FileService], 10 | exports: [FileService], 11 | }) 12 | export class FileModule {} 13 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/PDF/PDF.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Body, 4 | Controller, 5 | Post, 6 | Res, 7 | } from '@nestjs/common'; 8 | import { ConfigService } from '@nestjs/config'; 9 | import { Payload } from '@nestjs/microservices'; 10 | import { EnvConstant } from '@shared/constants/constant'; 11 | import CreatePDFDTO from '@shared/DTO/Document/create-PDF.DTO'; 12 | import { Response } from 'express'; 13 | 14 | import { PDFService } from './PDF.service'; 15 | 16 | @Controller('pdf') 17 | export class PDFController { 18 | constructor( 19 | private readonly pdfService: PDFService, 20 | private readonly configService: ConfigService, 21 | ) {} 22 | 23 | // @MessagePattern(documentMessagePattern.GET_DOCUMENT_BY_ID) 24 | async generatePdf(@Payload() payload: CreatePDFDTO) { 25 | return await this.pdfService.generatePDF(payload); 26 | } 27 | 28 | @Post('v1/preview') 29 | async generatePDFPreview( 30 | @Body() payload: CreatePDFDTO, 31 | @Res() res: Response, 32 | ) { 33 | // Only for develop test 34 | if (this.configService.get('http.env') === EnvConstant.prod) { 35 | throw new BadRequestException('Forbidden request!'); 36 | } 37 | const PDFData = await this.pdfService.generatePDF(payload); 38 | res.set({ 39 | 'Content-Type': 'application/pdf', 40 | 'Content-Disposition': `attachment; filename=${PDFData?.fileName}`, 41 | }); 42 | res.end(PDFData?.data); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/PDF/PDF.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import PDFController from './PDF.controller'; 4 | import { PDFService } from './PDF.service'; 5 | 6 | @Module({ 7 | controllers: [PDFController], 8 | providers: [PDFService], 9 | exports: [PDFService], 10 | }) 11 | export class PDFModule {} 12 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/PDF/templates/common_footer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | const Container = styled.div` 3 | font-size: 14px; 4 | text-align: right; 5 | width: 100%; 6 | margin: 20px 40px; 7 | padding: 10px 0; 8 | border-top: 1px solid #c0c0c0; 9 | .page { 10 | margin-right: 10px; 11 | } 12 | `; 13 | const CommonFooter = () => { 14 | return ( 15 | 16 | Page 17 | / 18 | 19 | ); 20 | }; 21 | 22 | export CommonFooter; 23 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/PDF/templates/common_header.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import i18n from "@i18n/i18n.config"; 3 | 4 | const Container = styled.div` 5 | display: flex; 6 | aling-items: center; 7 | justify-content: space-between; 8 | .left { 9 | display: flex; 10 | align-item: center; 11 | font-size: 20px; 12 | .logo { 13 | width: 20px; 14 | height: 20px; 15 | } 16 | } 17 | .right { 18 | font-size: 20px; 19 | } 20 | `; 21 | 22 | interface CommonHeaderProps { 23 | logoUrl: string; 24 | title?: string; 25 | slogan?: string; 26 | } 27 | 28 | const CommonHeader = ({ logoUrl, title, slogan }: CommonHeaderProps) => { 29 | return ( 30 | 31 |
32 | header 33 | {title ?? i18n.__("common.brand")} 34 | {slogan ?? i18n.__("common.slogan")} 35 |
36 |
Test
37 |
38 | ); 39 | }; 40 | 41 | export CommonHeader; 42 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/modules/PDF/templates/trip_submit_success.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Title = styled.div` 5 | font-size: 40px; 6 | font-weight: bold; 7 | text-align: center; 8 | `; 9 | 10 | interface Props { 11 | title: string; 12 | content: { name: string; value: string }[]; 13 | } 14 | const TripSubmitSuccess: React.FC = ({ title, content }) => { 15 | return ( 16 | <> 17 | {title} 18 | {content?.map((item, index) => ( 19 |
20 | {item?.name} 21 | {item.value} 22 |
23 | ))} 24 | 25 | ); 26 | }; 27 | 28 | export default TripSubmitSuccess; 29 | -------------------------------------------------------------------------------- /apps/backend/ms-document/src/shared: -------------------------------------------------------------------------------- 1 | ../../../Libs -------------------------------------------------------------------------------- /apps/backend/ms-document/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "baseUrl": "", 6 | "outDir": "build", 7 | "paths": { 8 | "@shared/*": ["../../../packages/shared-backend/*"], 9 | "@modules/*": ["src/modules/*"] 10 | } 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "assets": [{ "include": "config/**", "exclude": "config/index.ts", "watchAssets": true }] 7 | }, 8 | "generateOptions": { 9 | "spec": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": "true", 3 | "name": "@pawhaven/honey-service", 4 | "main": "build/main.js", 5 | "version": "1.0.0", 6 | "engines": { 7 | "node": ">=22", 8 | "pnpm": ">=10" 9 | }, 10 | "scripts": { 11 | "start:debug": "cross-env NODE_ENV=dev nest start --debug --watch", 12 | "dev": "cross-env NODE_ENV=dev nest start --watch", 13 | "uat": "cross-env NODE_ENV=uat nest start --watch", 14 | "test": "cross-env NODE_ENV=test nest start --watch", 15 | "build": "rm -rf build && nest build" 16 | }, 17 | "dependencies": { 18 | "@nestjs/axios": "^4.0.1", 19 | "@nestjs/common": "^11.1.6", 20 | "@nestjs/config": "^4.0.2", 21 | "@nestjs/core": "^11.1.6", 22 | "@nestjs/jwt": "^11.0.1", 23 | "@nestjs/microservices": "^11.1.6", 24 | "@nestjs/mongoose": "^11.0.3", 25 | "@nestjs/passport": "^11.0.5", 26 | "@nestjs/platform-express": "^11.1.6", 27 | "@nestjs/swagger": "^11.2.0", 28 | "@nestjs/terminus": "^11.0.0", 29 | "@nestjs/throttler": "^6.4.0", 30 | "@types/express": "^5.0.3", 31 | "add": "^2.0.6", 32 | "axios": "^1.12.2", 33 | "bcrypt": "^6.0.0", 34 | "class-transformer": "^0.5.1", 35 | "class-validator": "^0.14.2", 36 | "cookie-parser": "^1.4.7", 37 | "cross-env": "^10.1.0", 38 | "crypto-js": "^4.2.0", 39 | "csurf": "^1.11.0", 40 | "dayjs": "^1.11.18", 41 | "dotenv": "^17.2.3", 42 | "helmet": "^8.1.0", 43 | "js-yaml": "^4.1.0", 44 | "mongoose": "^8.19.1", 45 | "puppeteer": "^24.24.0", 46 | "reflect-metadata": "^0.2.2", 47 | "rxjs": "^7.8.2", 48 | "swagger-ui-express": "^5.0.1", 49 | "uuid": "^13.0.0" 50 | }, 51 | "devDependencies": { 52 | "@nestjs/cli": "^11.0.10", 53 | "@nestjs/schematics": "^11.0.9", 54 | "@nestjs/testing": "^11.1.6", 55 | "@types/crypto-js": "^4.2.2", 56 | "@types/jest": "^30.0.0", 57 | "@types/js-yaml": "^4.0.9", 58 | "@types/node": "^24.7.1", 59 | "@types/supertest": "^6.0.3", 60 | "source-map-support": "^0.5.21", 61 | "supertest": "^7.1.4", 62 | "ts-loader": "^9.5.4", 63 | "ts-node": "^10.9.2", 64 | "tsconfig-paths": "^4.2.0", 65 | "typescript": "^5.9.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import { TripModule } from '@modules/Trip/trip.module'; 4 | import { Module } from '@nestjs/common'; 5 | import { EnvConstant } from '@shared/constants/constant'; 6 | import SharedModule from '@shared/shared.module'; 7 | 8 | const currentEnv = process.env.NODE_ENV ?? 'uat'; 9 | const configFilePath = path.resolve( 10 | __dirname, 11 | `./config/${EnvConstant[currentEnv]}/env/index.yaml`, 12 | ); 13 | @Module({ 14 | imports: [ 15 | SharedModule.forRoot({ 16 | configFilePath, 17 | }), 18 | TripModule, 19 | ], 20 | providers: [], 21 | }) 22 | export class AppModule {} 23 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { MicroserviceOptions, Transport } from '@nestjs/microservices'; 4 | import { AllRpcExceptionsFilter } from '@shared/core/httpClient/rpcExceptionFillter'; 5 | 6 | import { AppModule } from './app.module'; 7 | 8 | async function bootstrap() { 9 | const appContext = await NestFactory.createApplicationContext(AppModule); 10 | const configService = appContext.get(ConfigService); 11 | 12 | const port = configService.get('http.port', 8081); 13 | const host = configService.get('http.host', '0.0.0.0'); 14 | 15 | const app = await NestFactory.createMicroservice( 16 | AppModule, 17 | { 18 | transport: Transport.TCP, 19 | options: { 20 | host, 21 | port, 22 | }, 23 | }, 24 | ); 25 | app.useGlobalFilters(new AllRpcExceptionsFilter()); 26 | 27 | await app.listen(); 28 | } 29 | bootstrap(); 30 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/src/models/common.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop } from '@nestjs/mongoose'; 2 | import { Document, type Schema } from 'mongoose'; 3 | 4 | export class CommonSchema extends Document { 5 | // 显示声明 6 | declare _id: Schema.Types.ObjectId; 7 | 8 | // 无需返回给前端响应的字段 9 | @Prop({ 10 | select: false, 11 | }) 12 | declare __v: number; 13 | 14 | @Prop({ 15 | select: false, 16 | }) 17 | createdAt: Date; 18 | 19 | @Prop({ 20 | select: false, 21 | }) 22 | updatedAt: Date; 23 | 24 | @Prop({ 25 | required: false, 26 | select: false, 27 | default: false, 28 | }) 29 | isRemove: boolean; 30 | } 31 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/src/models/trip.DBcollection.ts: -------------------------------------------------------------------------------- 1 | const TripDBCollection = { 2 | DESTINATION: 'Destinations', 3 | HISTORY: 'History', 4 | }; 5 | 6 | export default TripDBCollection; 7 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/src/models/tripInfo.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | 3 | import TripDBCollection from './trip.DBcollection'; 4 | import { CommonSchema } from './common.schema'; 5 | 6 | @Schema({ collection: TripDBCollection.HISTORY, timestamps: true }) 7 | export class TripHistory extends CommonSchema { 8 | @Prop({ 9 | required: true, 10 | type: String, 11 | }) 12 | destination: string; 13 | 14 | @Prop({ 15 | required: true, 16 | type: String, 17 | }) 18 | date: string; 19 | 20 | @Prop({ 21 | required: true, 22 | type: String, 23 | }) 24 | note: string; 25 | } 26 | export const TripHistorySchema = SchemaFactory.createForClass(TripHistory); 27 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/src/modules/Trip/trip.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { MessagePattern, Payload } from '@nestjs/microservices'; 3 | import { MicroServiceNames } from '@shared/constants/constant'; 4 | import TripMessagePattern from '@shared/constants/MSMessagePatterns/trip.messagePattern'; 5 | import TripInfoDTO from '@shared/DTO/Trip/tripInfo.DTO'; 6 | 7 | import { TripService } from './trip.service'; 8 | 9 | @Controller(MicroServiceNames.TRIP) 10 | export class TripController { 11 | constructor(private readonly tripService: TripService) {} 12 | 13 | /** 14 | * @description Create a new trip 15 | * @param payload - The payload containing trip information 16 | * @returns The created trip information 17 | */ 18 | @MessagePattern(TripMessagePattern.ADD_TRIP) 19 | addTrip(@Payload() payload: TripInfoDTO) { 20 | return this.tripService.addTrip(payload); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/src/modules/Trip/trip.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import TripDBCollection from 'src/models/trip.DBcollection'; 3 | import { TripHistorySchema } from 'src/models/tripInfo.schema'; 4 | import { MongooseModule } from '@nestjs/mongoose'; 5 | 6 | import { TripService } from './trip.service'; 7 | import { TripController } from './trip.controller'; 8 | 9 | @Module({ 10 | imports: [ 11 | MongooseModule.forFeature([ 12 | { name: TripDBCollection.HISTORY, schema: TripHistorySchema }, 13 | ]), 14 | ], 15 | controllers: [TripController], 16 | providers: [TripService], 17 | exports: [TripService], 18 | }) 19 | export class TripModule {} 20 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/src/modules/Trip/trip.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Model } from 'mongoose'; 4 | import TripDBCollection from 'src/models/trip.DBcollection'; 5 | import { TripHistory } from 'src/models/tripInfo.schema'; 6 | 7 | @Injectable() 8 | export class TripService { 9 | constructor( 10 | @InjectModel(TripDBCollection.HISTORY) 11 | private tripHistoryModel: Model, 12 | ) {} 13 | 14 | async addTrip(payload: Record) { 15 | const tripRecords = await this.tripHistoryModel.estimatedDocumentCount(); 16 | if (tripRecords < 10) { 17 | await this.tripHistoryModel.create(payload); 18 | return 'Trip record added successfully'; 19 | } 20 | throw new Error('Trip record limit reached'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/backend/ms-pawhaven/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": "", 5 | "outDir": "build", 6 | "paths": { 7 | "@shared/*": ["../../../packages/shared-backend/*"], 8 | "@modules/*": ["src/modules/*"] 9 | } 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/frontend/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"], 8 | "@shared/*": ["../../packages/*"] 9 | }, 10 | "outDir": "build", 11 | "types": ["node", "vite/client", "@modyfi/vite-plugin-yaml/modules"] 12 | }, 13 | "include": ["src"], 14 | "exclude": ["node_modules", "src/**/*.spec.ts", "tailwind.config.js"], 15 | "references": [{ "path": "./tsconfig.node.json" }] 16 | } 17 | -------------------------------------------------------------------------------- /apps/frontend/admin/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/frontend/user/.cssrem: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/cipchk/vscode-cssrem/master/schema.json", 3 | "rootFontSize": 16, 4 | "fixedDigits": 3 5 | } 6 | -------------------------------------------------------------------------------- /apps/frontend/user/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@pawhaven/eslint-config/web'], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/frontend/user/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @pawhaven/user 2 | 3 | ## 1.0.2 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies 8 | - @pawhaven/ui@3.0.0 9 | 10 | ## 1.0.1 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies 15 | - @pawhaven/ui@2.0.0 16 | -------------------------------------------------------------------------------- /apps/frontend/user/README.md: -------------------------------------------------------------------------------- 1 | # To be a JavaScript FullStack (FrontEnd) 2 | 3 | Complete the project by best practice, then share to the social media (Dev, Medium) 4 | 5 | ## TODO list 6 | 7 | - tailwind best practice 8 | - dynamic token refresh 9 | - useQuery best practice 10 | - dark model 11 | -------------------------------------------------------------------------------- /apps/frontend/user/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | pawHaven 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/frontend/user/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /apps/frontend/user/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pawhaven/user", 3 | "version": "1.0.2", 4 | "main": "main.js", 5 | "type": "module", 6 | "private": true, 7 | "engines": { 8 | "node": "22.x", 9 | "pnpm": ">=10" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.14.0", 13 | "@emotion/styled": "^11.14.1", 14 | "@hookform/resolvers": "^5.2.2", 15 | "@modyfi/vite-plugin-yaml": "^1.1.1", 16 | "@mui/material": "^7.3.4", 17 | "@pawhaven/eslint-config": "workspace:*", 18 | "@pawhaven/i18n": "workspace:*", 19 | "@pawhaven/shared-frontend": "workspace:*", 20 | "@pawhaven/theme": "workspace:*", 21 | "@pawhaven/tsconfig": "workspace:*", 22 | "@pawhaven/ui": "workspace:*", 23 | "@reduxjs/toolkit": "^2.9.0", 24 | "@tailwindcss/vite": "^4.1.14", 25 | "@tanstack/react-query": "^5.90.2", 26 | "@tanstack/react-query-devtools": "^5.90.2", 27 | "@types/node": "^24.7.1", 28 | "@types/react": "^19.2.2", 29 | "@types/react-dom": "^19.2.1", 30 | "@types/redux-persist": "^4.3.1", 31 | "@vitejs/plugin-react": "^5.0.4", 32 | "axios": "^1.12.2", 33 | "clsx": "^2.1.1", 34 | "crypto-js": "^4.2.0", 35 | "dayjs": "^1.11.18", 36 | "history": "^5.3.0", 37 | "i18next": "^25.6.0", 38 | "immer": "^10.1.3", 39 | "lodash": "^4.17.21", 40 | "lucide-react": "^0.545.0", 41 | "react": "^19.2.0", 42 | "react-dom": "^19.2.0", 43 | "react-error-boundary": "^6.0.0", 44 | "react-hook-form": "^7.64.0", 45 | "react-hot-toast": "^2.6.0", 46 | "react-i18next": "^16.0.0", 47 | "react-loading-skeleton": "^3.5.0", 48 | "react-material-ui-carousel": "^3.4.2", 49 | "react-redux": "^9.2.0", 50 | "react-router-dom": "^7.9.4", 51 | "redux-persist": "^6.0.0", 52 | "typescript": "^5.9.3", 53 | "vite": "^7.1.9", 54 | "zod": "^4.1.12" 55 | }, 56 | "scripts": { 57 | "dev": "vite --mode dev --port 5173", 58 | "uat": "vite --mode uat", 59 | "build:prod": "tsc && vite build --mode prod", 60 | "build:uat": "tsc && vite build --mode uat", 61 | "preview": "vite preview" 62 | }, 63 | "devDependencies": { 64 | "@types/ramda": "^0.31.1", 65 | "autoprefixer": "^10.4.21", 66 | "babel-plugin-react-compiler": "19.1.0-rc.3", 67 | "tailwindcss": "^4.1.14", 68 | "vite-tsconfig-paths": "6.0.0-beta.4" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apps/frontend/user/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/apps/frontend/user/public/favicon.ico -------------------------------------------------------------------------------- /apps/frontend/user/public/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/apps/frontend/user/public/images/404.png -------------------------------------------------------------------------------- /apps/frontend/user/public/images/500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/apps/frontend/user/public/images/500.png -------------------------------------------------------------------------------- /apps/frontend/user/public/images/UI-design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/apps/frontend/user/public/images/UI-design.png -------------------------------------------------------------------------------- /apps/frontend/user/public/images/authBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/apps/frontend/user/public/images/authBanner.png -------------------------------------------------------------------------------- /apps/frontend/user/public/images/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/apps/frontend/user/public/images/hero.png -------------------------------------------------------------------------------- /apps/frontend/user/public/images/hero1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/apps/frontend/user/public/images/hero1.png -------------------------------------------------------------------------------- /apps/frontend/user/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/apps/frontend/user/public/images/logo.png -------------------------------------------------------------------------------- /apps/frontend/user/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "tripSnap fronteend", 3 | "name": "tripSnap fronteend", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /apps/frontend/user/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/frontend/user/src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import AppRouterProvider from '../route/AppRouterProvider'; 4 | 5 | import AppProvider from './AppProvider'; 6 | 7 | const App: FC = () => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /apps/frontend/user/src/app/AppProvider.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from '@mui/material'; 2 | import '@pawhaven/theme/globalTailwind.css'; 3 | import getReactQueryOptions from '@pawhaven/shared-frontend/cores/react-query'; 4 | import MUITheme from '@pawhaven/theme/MUI-theme'; 5 | import { Loading } from '@pawhaven/ui'; 6 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 7 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 8 | import { type ReactNode, Suspense, useState } from 'react'; 9 | import { ErrorBoundary } from 'react-error-boundary'; 10 | import { Toaster } from 'react-hot-toast'; 11 | import { Provider } from 'react-redux'; 12 | import { PersistGate } from 'redux-persist/integration/react'; 13 | 14 | import envConfig from '../config'; 15 | // Enable i18n for the entire app 16 | import '@pawhaven/i18n'; 17 | 18 | import GlobalInitializer from './GlobalInitializer'; 19 | 20 | import SystemError from '@/components/SystemError'; 21 | import useIsProd from '@/hooks/useIsProd'; 22 | import { persistor, store } from '@/store/reduxStore'; 23 | 24 | type AppProviderProps = { 25 | children: ReactNode; 26 | }; 27 | 28 | const AppProvider = ({ children }: AppProviderProps) => { 29 | const [queryClient] = useState(() => { 30 | return new QueryClient(getReactQueryOptions(envConfig)); 31 | }); 32 | const isProd = useIsProd(); 33 | return ( 34 | 35 | } persistor={persistor}> 36 | }> 37 | 38 | 39 | {!isProd && } 40 | 41 | 42 | 43 | {children} 44 | 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default AppProvider; 54 | -------------------------------------------------------------------------------- /apps/frontend/user/src/app/GlobalInitializer.tsx: -------------------------------------------------------------------------------- 1 | import { Loading } from '@pawhaven/ui'; 2 | import { useEffect } from 'react'; 3 | 4 | import { 5 | useFetchGlobalMenu, 6 | useFetchGlobalRouters, 7 | } from './GlobalInitializationAPI'; 8 | 9 | import { useReduxDispatch } from '@/hooks/reduxHooks'; 10 | import { 11 | setGlobalMenuItems, 12 | setGlobalRouters, 13 | useGlobalState, 14 | } from '@/store/globalReducer'; 15 | 16 | /** 17 | * 18 | * This component is responsible for fetching and initializing global data required across the application. 19 | * It triggers the fetching of dynamic global menu and router configurations from the API. 20 | * The component does not render any UI elements and returns null. 21 | * 22 | * It is intended to be included at a high level in the component tree, such as in the main layout or App component, 23 | * to ensure that global data is available throughout the application. 24 | * 25 | * @returns {null} - This component does not render any UI elements. 26 | */ 27 | const GlobalInitializer = () => { 28 | const dispatch = useReduxDispatch(); 29 | const { 30 | userInfo: { userID }, 31 | } = useGlobalState(); 32 | 33 | const { data: menu, isPending: menuLoading } = useFetchGlobalMenu(userID); 34 | 35 | const { data: routers, isPending: routersLoading } = 36 | useFetchGlobalRouters(userID); 37 | 38 | useEffect(() => { 39 | if (menu && menu?.length > 0) { 40 | dispatch(setGlobalMenuItems(menu)); 41 | } 42 | }, [dispatch, menu]); 43 | 44 | useEffect(() => { 45 | if (routers && routers?.length > 0) { 46 | dispatch(setGlobalRouters(routers)); 47 | } 48 | }, [routers, dispatch]); 49 | 50 | if (menuLoading || routersLoading) return ; 51 | return null; 52 | }; 53 | 54 | export default GlobalInitializer; 55 | -------------------------------------------------------------------------------- /apps/frontend/user/src/components/Brand/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | import type { NavigateFunction } from 'react-router-dom'; 3 | 4 | const Brand = ({ navigate }: { navigate: NavigateFunction }) => { 5 | const { t } = useTranslation(); 6 | 7 | return ( 8 |
{ 11 | navigate('/'); 12 | }} 13 | onKeyDown={(e) => { 14 | if (e.key === 'Enter' || e.key === ' ') { 15 | e.preventDefault(); 16 | navigate('/'); 17 | } 18 | }} 19 | role="button" 20 | tabIndex={0} 21 | aria-label={t('common.name')} 22 | > 23 | {t('common.slogan')} 28 |
29 | ); 30 | }; 31 | export default Brand; 32 | -------------------------------------------------------------------------------- /apps/frontend/user/src/components/ErrorFallback/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteError } from 'react-router-dom'; 2 | 3 | import NotFund from '../NotFund'; 4 | import SystemError from '../SystemError'; 5 | 6 | import useIsProd from '@/hooks/useIsProd'; 7 | 8 | interface ErrorInfo { 9 | status: number; 10 | statusText?: string; 11 | } 12 | 13 | const ErrorFallback = () => { 14 | const errorInfo = useRouteError() as Partial; 15 | const isProd = useIsProd(); 16 | if (!isProd) { 17 | console.error('current errorInfo:', JSON.stringify(errorInfo)); 18 | } 19 | switch (errorInfo?.status) { 20 | case 404: 21 | return ; 22 | case 500: 23 | return ; 24 | default: 25 | return ; 26 | } 27 | }; 28 | 29 | export default ErrorFallback; 30 | -------------------------------------------------------------------------------- /apps/frontend/user/src/components/ImageUploader/index.module.css: -------------------------------------------------------------------------------- 1 | @reference '../../../../../packages/theme/global.css'; 2 | 3 | .title { 4 | @apply text-2xl lg:text-3xl font-bold mb-6 text-primary; 5 | } 6 | 7 | .form { 8 | @apply bg-white rounded-lg shadow-md p-6 space-y-6; 9 | } 10 | 11 | .section { 12 | @apply mb-6; 13 | } 14 | 15 | .sectionTitle { 16 | @apply text-lg font-semibold mb-4 text-gray-800; 17 | } 18 | 19 | .formGroup { 20 | @apply mb-4; 21 | } 22 | 23 | .label { 24 | @apply block text-sm font-medium text-gray-700 mb-1; 25 | } 26 | 27 | .buttonGroup { 28 | @apply flex justify-end gap-4 mt-8; 29 | } 30 | -------------------------------------------------------------------------------- /apps/frontend/user/src/components/NotFund/index.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | const NotFund = () => { 6 | const { t } = useTranslation(); 7 | const navigate = useNavigate(); 8 | 9 | const goToHome = () => { 10 | navigate('/'); 11 | }; 12 | 13 | return ( 14 |
15 | 16 |

{t('common.not_found')}

17 |

{t('common.not_found_info')}

18 | 26 |
27 | ); 28 | }; 29 | export default NotFund; 30 | -------------------------------------------------------------------------------- /apps/frontend/user/src/components/SystemError/index.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | const SystemError = () => { 6 | const { t } = useTranslation(); 7 | const navigate = useNavigate(); 8 | 9 | const goToHome = () => { 10 | navigate('/'); 11 | }; 12 | 13 | return ( 14 |
15 | System error 20 |

{t('common.system_error')}

21 |

{t('common.system_error_info')}

22 | 30 |
31 | ); 32 | }; 33 | 34 | export default SystemError; 35 | -------------------------------------------------------------------------------- /apps/frontend/user/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import devYaml from './dev/env/index.yaml'; 2 | import prodYaml from './prod/env/index.yaml'; 3 | import uatYaml from './uat/env/index.yaml'; 4 | 5 | const environment = import.meta.env; 6 | const currentEnv = environment?.MODE ?? 'dev'; 7 | 8 | export const EnvVariables = { 9 | dev: 'dev', 10 | uat: 'uat', 11 | prod: 'prod', 12 | }; 13 | 14 | const getConfigs = () => { 15 | try { 16 | switch (currentEnv) { 17 | case EnvVariables.dev: 18 | return devYaml; 19 | case EnvVariables.uat: 20 | return uatYaml; 21 | case EnvVariables.prod: 22 | return prodYaml; 23 | default: 24 | return devYaml; 25 | } 26 | } catch (error) { 27 | console.error('Error loading config file:', error); 28 | return {}; 29 | } 30 | }; 31 | 32 | export default getConfigs(); 33 | -------------------------------------------------------------------------------- /apps/frontend/user/src/constants/RescueStatus.ts: -------------------------------------------------------------------------------- 1 | const RescueStatus = { 2 | pending: 'pending', 3 | inProgress: 'inProgress', 4 | treated: 'treated', 5 | recovering: 'recovering', 6 | awaitingAdoption: 'awaitingAdoption', 7 | adopted: 'adopted', 8 | failed: 'failed', 9 | } as const; 10 | export default RescueStatus; 11 | -------------------------------------------------------------------------------- /apps/frontend/user/src/constants/StorageKeys.ts: -------------------------------------------------------------------------------- 1 | const StorageKeys = { 2 | refreshToken: 'refreshToken', 3 | accessToken: 'access-token', 4 | globalState: 'globalState', 5 | tripRecord: 'tripRecord', 6 | I18NKEY: 'I18NKEY', 7 | }; 8 | export default StorageKeys; 9 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/Auth/apis/queries.ts: -------------------------------------------------------------------------------- 1 | import storage from '@pawhaven/shared-frontend/utils/storage'; 2 | import { useMutation } from '@tanstack/react-query'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import type { AuthFieldType, LoginInfo } from '../types'; 6 | 7 | import * as AuthAPI from './requests'; 8 | 9 | import storageKeys from '@/constants/StorageKeys'; 10 | import { setUserInfo } from '@/store/globalReducer'; 11 | 12 | export const useLogin = () => { 13 | const navigate = useNavigate(); 14 | return useMutation({ 15 | mutationFn: (userInfo: AuthFieldType) => AuthAPI.login(userInfo), 16 | onSuccess: (loginInfo) => { 17 | if (loginInfo?.accessToken && loginInfo?.refreshToken) { 18 | storage.set(storageKeys.accessToken, loginInfo?.accessToken); 19 | storage.set(storageKeys.refreshToken, loginInfo?.refreshToken); 20 | setUserInfo(loginInfo?.baseUserInfo); 21 | navigate('/'); 22 | } 23 | }, 24 | }); 25 | }; 26 | 27 | export const useRegister = () => { 28 | const navigate = useNavigate(); 29 | return useMutation({ 30 | mutationFn: (userInfo: AuthFieldType) => AuthAPI.register(userInfo), 31 | onSuccess: (loginInfo) => { 32 | if (loginInfo?.accessToken && loginInfo?.refreshToken) { 33 | storage.set(storageKeys.accessToken, loginInfo?.accessToken); 34 | storage.set(storageKeys.refreshToken, loginInfo?.refreshToken); 35 | setUserInfo(loginInfo?.baseUserInfo); 36 | navigate('/'); 37 | } 38 | }, 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/Auth/apis/requests.ts: -------------------------------------------------------------------------------- 1 | import type { AuthFieldType, LoginInfo } from '../types'; 2 | 3 | import { apiClient } from '@/utils/apiClient'; 4 | 5 | export const register = (userInfo: AuthFieldType): Promise => { 6 | return apiClient.post('/auth/register', userInfo); 7 | }; 8 | 9 | export const login = (userInfo: AuthFieldType): Promise => { 10 | return apiClient.post('/auth/v1/login', userInfo); 11 | }; 12 | 13 | export const refreshToken = (token: { 14 | refreshToken: string; 15 | }): Promise => { 16 | return apiClient.post('/auth/v1/refresh', token); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/Auth/authLayout.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import Brand from '@/components/Brand'; 5 | 6 | const AuthLayout: FC<{ children: React.ReactNode }> = ({ children }) => { 7 | const navigate = useNavigate(); 8 | return ( 9 |
10 | 11 |
12 | {children} 13 |
14 |
15 | ); 16 | }; 17 | export default AuthLayout; 18 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/Auth/types.ts: -------------------------------------------------------------------------------- 1 | export type UserInfoType = { 2 | userName: string; 3 | userID: string; 4 | [key: string]: unknown; 5 | }; 6 | 7 | export type AuthFieldType = { 8 | userName?: string; 9 | password?: string; 10 | phoneNumber?: string; 11 | }; 12 | export type LoginInfo = { 13 | accessToken: string; 14 | refreshToken: string; 15 | baseUserInfo: UserInfoType; 16 | }; 17 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/Home/apis/queries.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { getLatestRescuesByNumber } from './requests'; 4 | 5 | export const useFetchLatestRescuesByNumber = () => { 6 | return useQuery({ 7 | queryKey: ['latestRescues'], 8 | queryFn: getLatestRescuesByNumber, 9 | }); 10 | }; 11 | 12 | export const useFetchLatestStories = () => { 13 | return useQuery({ 14 | queryKey: ['latestRescues'], 15 | queryFn: getLatestRescuesByNumber, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/Home/apis/requests.ts: -------------------------------------------------------------------------------- 1 | import type { RescueItemType } from '../types'; 2 | 3 | import apiClient from '@/utils/apiClient'; 4 | 5 | export const getLatestRescuesByNumber = (): Promise => { 6 | // return http.get(`/rescues/latest/${number}`); 7 | const mockdata: RescueItemType[] = [ 8 | { 9 | animalID: '001', 10 | name: 'Dense', 11 | img: 'https://daoinsights.com/wp-content/webp-express/webp-images/uploads/2022/03/anoir-chafik-2_3c4dIFYFU-unsplash-1870x1169.jpg.webp', 12 | description: 13 | 'A very cute cat with a gentle personality, loves being close to people, suitable for family care.', 14 | location: 'Shuangliu Tianfu Fifth Street', 15 | time: '2023-10-20 12:00', 16 | status: 'pending', 17 | }, 18 | { 19 | animalID: '002', 20 | name: 'Tiger', 21 | img: 'https://d.newsweek.com/en/full/2050102/stray-cats.jpg?w=1200&f=7d728c9cbd5cd73470c100ff9cd51d59', 22 | description: 23 | 'A lively and energetic dog, loves outdoor activities, needs a loving family to take care of it.', 24 | location: 'Paris Fashion District', 25 | time: '2023-10-20 12:00', 26 | status: 'treated', 27 | }, 28 | { 29 | animalID: '003', 30 | name: 'Lele', 31 | img: 'https://media.4-paws.org/1/b/1/4/1b14c5ffc386210e11c20c5dd139b772af045503/VIER%20PFOTEN_2023-10-19_00181-3801x2534-3662x2534-1920x1329.jpg', 32 | description: 33 | 'A gentle and well-behaved dog, enjoys a quiet environment, suitable for indoor care.', 34 | location: 'Central, Hong Kong', 35 | time: '2023-10-20 12:00', 36 | status: 'awaitingAdoption', 37 | }, 38 | ]; 39 | return Promise.resolve(mockdata); 40 | }; 41 | 42 | export const getLatestStories = (): Promise => { 43 | return apiClient.get('/rescues/latest'); 44 | }; 45 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/Home/components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const Hero = () => { 5 | const { t } = useTranslation(); 6 | const navigate = useNavigate(); 7 | 8 | return ( 9 |
10 |
11 |
12 |

13 | {t('common.slogan')} 14 |

15 |

16 | {t('common.subSlogan')} 17 |

18 |
19 | 28 | 34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Hero; 41 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/Home/components/RecentStory.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | 3 | const RecentStory = () => { 4 | const { t } = useTranslation(); 5 | return ( 6 |
7 |

8 | {t('common.love_story')} 9 |

10 |
11 | ); 12 | }; 13 | 14 | export default RecentStory; 15 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import Hero from './components/Hero'; 2 | import LatestRescue from './components/LatestRescue'; 3 | import RecentStory from './components/RecentStory'; 4 | 5 | const Home = () => { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default Home; 16 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/Home/types.ts: -------------------------------------------------------------------------------- 1 | import RescueStatus from '@/constants/RescueStatus'; 2 | 3 | export type RescueStatusType = (typeof RescueStatus)[keyof typeof RescueStatus]; 4 | 5 | export interface RescueItemType { 6 | animalID: string; 7 | name: string; 8 | img: string; 9 | description: string; 10 | location: string; 11 | time: string; 12 | status: RescueStatusType; 13 | } 14 | 15 | export type ColorPrefix = 'text' | 'bg' | 'border'; 16 | export type StatusColorType = `${ColorPrefix}-rescue-${RescueStatusType}`; 17 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/LoveStories/index.tsx: -------------------------------------------------------------------------------- 1 | const index = () => { 2 | return
index
; 3 | }; 4 | 5 | export default index; 6 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/ReportStray/apis/queries.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/apps/frontend/user/src/features/ReportStray/apis/queries.ts -------------------------------------------------------------------------------- /apps/frontend/user/src/features/ReportStray/apis/requests.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/apps/frontend/user/src/features/ReportStray/apis/requests.ts -------------------------------------------------------------------------------- /apps/frontend/user/src/features/ReportStray/constants.ts: -------------------------------------------------------------------------------- 1 | export const animalTypeOptions = [ 2 | { value: 'cat', label: 'reportStray.cat' }, 3 | { value: 'dog', label: 'reportStray.dog' }, 4 | { value: 'other', label: 'reportStray.other' }, 5 | ]; 6 | 7 | export const ageOptions = [ 8 | { value: 'baby', label: 'reportStray.baby' }, 9 | { value: 'young', label: 'reportStray.young' }, 10 | { value: 'adult', label: 'reportStray.adult' }, 11 | { value: 'senior', label: 'reportStray.senior' }, 12 | ]; 13 | 14 | export const statusOptions = [ 15 | { value: 'dangerous', label: 'reportStray.dangerous' }, 16 | { value: 'friendly', label: 'reportStray.friendly' }, 17 | { value: 'scared', label: 'reportStray.scared' }, 18 | { value: 'other', label: 'reportStray.other' }, 19 | ]; 20 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/ReportStray/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | import ReportForm from './components/ReportForm'; 5 | 6 | const ReportStray: React.FC = () => { 7 | const { t } = useTranslation(); 8 | 9 | return ( 10 |
11 |

12 | {t('reportStray.report_animal')} 13 |

14 | {}} isSubmitting={false} /> 15 |
16 | ); 17 | }; 18 | 19 | export default ReportStray; 20 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/ReportStray/types.ts: -------------------------------------------------------------------------------- 1 | export interface AnimalReport { 2 | animalType: string; 3 | age: 'baby' | 'young' | 'adult' | 'senior'; 4 | appearance: { 5 | color: string; 6 | hasInjury: boolean; 7 | injuryDescription?: string; 8 | otherFeatures?: string; 9 | }; 10 | location: { 11 | address: string; 12 | latitude: number; 13 | longitude: number; 14 | }; 15 | foundTime: string; // ISO string 16 | status: 'dangerous' | 'friendly' | 'scared' | 'other'; 17 | statusDescription: string; 18 | images: File[]; 19 | contactInfo: { 20 | name: string; 21 | phone: string; 22 | email?: string; 23 | }; 24 | } 25 | 26 | export interface ReportResponse { 27 | id: string; 28 | message: string; 29 | } 30 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/RescueDetail/apis/queries.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { getAnimalDetail } from './request'; 4 | 5 | export const useFetchAnimalDetail = (id: string) => { 6 | return useQuery({ 7 | queryKey: ['animalDetail', id], 8 | queryFn: () => getAnimalDetail(id), 9 | enabled: !!id, 10 | }); 11 | }; 12 | 13 | export const useFetchRescueLine = (id: string) => { 14 | return useQuery({ 15 | queryKey: ['animalDetail', id], 16 | queryFn: () => getAnimalDetail(id), 17 | enabled: !!id, 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/RescueDetail/types.ts: -------------------------------------------------------------------------------- 1 | import { AnimalRescueStatus } from '@/types/AnimalType'; 2 | 3 | export interface RescueUpdate { 4 | id: string; 5 | timestamp: string; 6 | status: AnimalRescueStatus; 7 | operator: { 8 | id: string; 9 | name: string; 10 | avatar: string; 11 | role: 'reporter' | 'rescuer' | 'admin'; 12 | }; 13 | content: string; 14 | images?: string[]; 15 | location?: { 16 | address: string; 17 | latitude: number; 18 | longitude: number; 19 | }; 20 | } 21 | 22 | export interface RescueParticipantType { 23 | id: string; 24 | name: string; 25 | avatar?: string; 26 | role: 'reporter' | 'rescuer' | 'admin'; 27 | joinedAt: string; 28 | } 29 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/RescueGuide/components/StepCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface StepCardProps { 4 | icon: string; 5 | title: string; 6 | desc: string; 7 | } 8 | 9 | const StepCard: React.FC = ({ icon, title, desc }) => { 10 | return ( 11 |
18 |
{icon}
19 | 20 |

{title}

21 | 22 |

{desc}

23 |
24 | ); 25 | }; 26 | export default StepCard; 27 | -------------------------------------------------------------------------------- /apps/frontend/user/src/features/RescueGuide/index.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Typography, Box } from '@mui/material'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | import StepCard from './components/StepCard'; 5 | 6 | const RescueGuidePage = () => { 7 | const { t } = useTranslation(); 8 | const stepsContent = t('rescueGuide.steps', { returnObjects: true }) as { 9 | icon: string; 10 | title: string; 11 | desc: string; 12 | }[]; 13 | return ( 14 | 15 | 16 | {t('rescueGuide.title')} 17 | 18 | 19 | 20 | {t('rescueGuide.intro')} 21 | 22 | 23 | 24 | {stepsContent?.map((step) => ( 25 | 26 | ))} 27 | 28 | 29 | 34 | {t('rescueGuide.quote')} 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default RescueGuidePage; 41 | -------------------------------------------------------------------------------- /apps/frontend/user/src/hooks/reduxHooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | 3 | import type { ReduxState, AppDispatch } from '@/store/reduxStore'; 4 | 5 | export const useReduxDispatch = () => useDispatch(); 6 | export const useReduxSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /apps/frontend/user/src/hooks/useIsProd.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | import envConfig, { EnvVariables } from '@/config'; 4 | 5 | const useIsProd = () => { 6 | const isProd = useMemo(() => envConfig?.env === EnvVariables.prod, []); 7 | return isProd; 8 | }; 9 | export default useIsProd; 10 | -------------------------------------------------------------------------------- /apps/frontend/user/src/layout/RootLayoutFooter.tsx: -------------------------------------------------------------------------------- 1 | import myPersonal from '@pawhaven/shared-frontend/constants/myPerson'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | // import { Link } from 'react-router-dom'; 5 | 6 | const RootLayoutFooter = () => { 7 | const { t } = useTranslation(); 8 | return ( 9 |
10 |

11 | {t('common.quick_links')} 12 | {/* 13 | {t('home.home_page')} 14 | 15 | 16 | {t('common.record')} 17 | */} 18 |

19 |

20 |

21 | {t('common.contact_me')} 22 | 28 | {t('common.github')} 29 | 30 | 36 | {t('common.email')} 37 | 38 | 44 | {t('common.linkedin')} 45 | 46 |

47 |
48 | ); 49 | }; 50 | 51 | export default RootLayoutFooter; 52 | -------------------------------------------------------------------------------- /apps/frontend/user/src/layout/RootLayoutMenu.tsx: -------------------------------------------------------------------------------- 1 | import useIsMobile from '@pawhaven/shared-frontend/hooks/useIsMobile'; 2 | import { AlignJustify } from 'lucide-react'; 3 | import { useState } from 'react'; 4 | 5 | import RootLayoutMenuRender from './RootLayoutMenuRender'; 6 | import RootLayoutSidebar from './RootLayoutSidebar'; 7 | 8 | import Brand from '@/components/Brand'; 9 | import type { RootLayoutHeaderProps } from '@/types/LayoutType'; 10 | 11 | const RootLayoutMenu = ({ 12 | menuItems, 13 | navigate, 14 | currentRouterInfo, 15 | }: RootLayoutHeaderProps) => { 16 | const isMobile = useIsMobile(); 17 | const [isSidebarOpen, setSidebarOpen] = useState(false); 18 | 19 | const onOpenSidebar = () => setSidebarOpen(true); 20 | const onCloseSidebar = () => setSidebarOpen(false); 21 | 22 | return ( 23 |
24 | 25 | {!isMobile && ( 26 | 31 | )} 32 | 33 | {/* Open Side bar Icon */} 34 | {isMobile && } 35 | {/* Side bar */} 36 | 42 |
43 | ); 44 | }; 45 | 46 | export default RootLayoutMenu; 47 | -------------------------------------------------------------------------------- /apps/frontend/user/src/layout/RootLayoutSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from '@mui/material'; 2 | import type { NavigateFunction } from 'react-router-dom'; 3 | 4 | import RootLayoutMenuRender from './RootLayoutMenuRender'; 5 | 6 | import type { MenuItemType } from '@/types/LayoutType'; 7 | 8 | interface RootLayoutSidebarProps { 9 | menuItems: MenuItemType[]; 10 | navigate: NavigateFunction; 11 | isSidebarOpen: boolean; 12 | onCloseSidebar: () => void; 13 | } 14 | 15 | const RootLayoutSidebar = ({ 16 | menuItems, 17 | isSidebarOpen, 18 | onCloseSidebar, 19 | navigate, 20 | }: RootLayoutSidebarProps) => { 21 | return ( 22 | 28 | 29 | 30 | ); 31 | }; 32 | export default RootLayoutSidebar; 33 | -------------------------------------------------------------------------------- /apps/frontend/user/src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import useRouterInfo from '@pawhaven/shared-frontend/hooks/useRouterInfo'; 2 | import type { NavigateFunction, UIMatch } from 'react-router-dom'; 3 | import { Outlet, useNavigate } from 'react-router-dom'; 4 | 5 | import RootLayoutFooter from './RootLayoutFooter'; 6 | import RootLayoutMenu from './RootLayoutMenu'; 7 | 8 | import type { GlobalStateType } from '@/store/globalReducer'; 9 | import { useGlobalState } from '@/store/globalReducer'; 10 | import type { MenuItemType, RouterInfoType } from '@/types/LayoutType'; 11 | 12 | export interface LayoutProps { 13 | menuItems: MenuItemType[]; 14 | navigate: NavigateFunction; 15 | routerMatches: Array>; 16 | } 17 | 18 | const RootLayout = () => { 19 | const { globalMenuItems } = useGlobalState() as GlobalStateType; 20 | const navigate = useNavigate(); 21 | const currentRouterInfo = useRouterInfo(); 22 | const { isMenuAvailable = true, isFooterAvailable = true } = 23 | currentRouterInfo?.handle ?? {}; 24 | 25 | return ( 26 |
27 | {isMenuAvailable && ( 28 | 33 | )} 34 | 35 |
36 |
37 | 38 |
39 | {isFooterAvailable && } 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default RootLayout; 46 | -------------------------------------------------------------------------------- /apps/frontend/user/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './app/App'; 5 | 6 | const rootElement = document.getElementById('root'); 7 | if (!rootElement) { 8 | throw new Error('Root element not found'); 9 | } 10 | createRoot(rootElement).render( 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /apps/frontend/user/src/route/AppRouterProvider.tsx: -------------------------------------------------------------------------------- 1 | import { Loading } from '@pawhaven/ui'; 2 | import type { ReactNode } from 'react'; 3 | import { useMemo } from 'react'; 4 | import type { RouteObject } from 'react-router-dom'; 5 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'; 6 | 7 | import routerElementMapping from '@/route/routerElementMapping'; 8 | import type { GlobalStateType } from '@/store/globalReducer'; 9 | import { useGlobalState } from '@/store/globalReducer'; 10 | 11 | export interface RouteMetaType { 12 | isRequireUserLogin?: boolean; 13 | children?: ReactNode; 14 | } 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | const routesMapping = (routesFromAPI: any[]): RouteObject[] => { 18 | const routes = routesFromAPI.map((route) => { 19 | const mappedRoute: RouteObject = { 20 | path: route?.path, 21 | element: routerElementMapping[route.element], 22 | handle: route?.handle, 23 | }; 24 | 25 | if (route?.children) { 26 | mappedRoute.children = routesMapping(route?.children); 27 | mappedRoute.errorElement = routerElementMapping.errorFallback; 28 | } 29 | 30 | return mappedRoute; 31 | }); 32 | return routes; 33 | }; 34 | 35 | const AppRouterProvider = () => { 36 | const { globalRouters } = useGlobalState() as GlobalStateType; 37 | 38 | const mappedRoutes = useMemo(() => { 39 | if (globalRouters?.length > 0) { 40 | return routesMapping(globalRouters); 41 | } 42 | return []; 43 | }, [globalRouters]); 44 | 45 | const router = useMemo(() => { 46 | if (mappedRoutes.length > 0) { 47 | return createBrowserRouter(mappedRoutes); 48 | } 49 | return null; 50 | }, [mappedRoutes]); 51 | 52 | if (router) { 53 | return ; 54 | } 55 | return ; 56 | }; 57 | 58 | export default AppRouterProvider; 59 | -------------------------------------------------------------------------------- /apps/frontend/user/src/route/routePaths.ts: -------------------------------------------------------------------------------- 1 | const routePaths = { 2 | login: '/auth/login', 3 | register: '/auth/register', 4 | }; 5 | export default routePaths; 6 | -------------------------------------------------------------------------------- /apps/frontend/user/src/route/routerElementMapping.tsx: -------------------------------------------------------------------------------- 1 | import { SuspenseWrapper } from '@pawhaven/ui'; 2 | import type { ReactElement } from 'react'; 3 | import { lazy } from 'react'; 4 | 5 | import ErrorFallback from '@/components/ErrorFallback'; 6 | // import GuardRoute from '@/components/GuardRoute'; 7 | import NotFund from '@/components/NotFund'; 8 | import Login from '@/features/Auth/Login'; 9 | import Register from '@/features/Auth/Register'; 10 | import Home from '@/features/Home'; 11 | import RescueGuide from '@/features/RescueGuide'; 12 | import RootLayout from '@/layout'; 13 | 14 | const ReportStray = lazy(() => import('@/features/ReportStray')); 15 | const ReportDetail = lazy(() => import('@/features/RescueDetail')); 16 | 17 | // Please use SuspenseWrapper to wrap the lazy loaded components 18 | 19 | const routerElementMapping: Record = { 20 | // guardRoute: ( 21 | // 22 | // 23 | // 24 | // ), 25 | rootLayout: , 26 | home: , 27 | auth_login: , 28 | auth_register: , 29 | report_stray: ( 30 | 31 | 32 | 33 | ), 34 | rescue_guides: ( 35 | 36 | 37 | 38 | ), 39 | rescue_detail: ( 40 | 41 | 42 | 43 | ), 44 | notFund: , 45 | errorFallback: , 46 | }; 47 | 48 | export default routerElementMapping; 49 | -------------------------------------------------------------------------------- /apps/frontend/user/src/store/globalReducer.ts: -------------------------------------------------------------------------------- 1 | import LocaleKeys from '@pawhaven/shared-frontend/constants/localeKey'; 2 | import storageTool from '@pawhaven/shared-frontend/utils/storage'; 3 | import { createSlice } from '@reduxjs/toolkit'; 4 | 5 | import { useReduxSelector } from '../hooks/reduxHooks'; 6 | 7 | import reducerNames from './reducerNames'; 8 | import type { ReduxState } from './reduxStore'; 9 | 10 | import storageKeys from '@/constants/StorageKeys'; 11 | import type { UserInfoType } from '@/features/Auth/types'; 12 | import type { MenuItemType } from '@/types/LayoutType'; 13 | 14 | export interface GlobalStateType { 15 | userInfo: UserInfoType; 16 | globalMenuItems: MenuItemType[]; 17 | globalRouters: []; 18 | locale: string; 19 | } 20 | const initialState: GlobalStateType = { 21 | userInfo: { 22 | userName: '', 23 | userID: '', 24 | }, 25 | globalMenuItems: [], 26 | globalRouters: [], 27 | locale: storageTool.get(storageKeys.I18NKEY) || LocaleKeys['en-US'], 28 | }; 29 | 30 | const globalReducer = createSlice({ 31 | name: reducerNames.global, 32 | initialState, 33 | reducers: { 34 | setGlobalMenuItems: (state, action) => { 35 | state.globalMenuItems = action.payload; 36 | }, 37 | setGlobalRouters: (state, action) => { 38 | state.globalRouters = action.payload; 39 | }, 40 | setUserInfo: (state, action) => { 41 | state.userInfo = action.payload; 42 | }, 43 | }, 44 | }); 45 | export default globalReducer.reducer; 46 | 47 | export const { setGlobalMenuItems, setUserInfo, setGlobalRouters } = 48 | globalReducer.actions; 49 | export const useGlobalState = () => { 50 | return useReduxSelector( 51 | (state: ReduxState) => state?.global ?? {}, 52 | ) as GlobalStateType; 53 | }; 54 | -------------------------------------------------------------------------------- /apps/frontend/user/src/store/reducerConfig.ts: -------------------------------------------------------------------------------- 1 | // All reducers should be registered here for centralized management and scalability 2 | import storage from 'redux-persist/lib/storage'; 3 | 4 | import globalReducer from './globalReducer'; 5 | import reducerNames from './reducerNames'; 6 | 7 | export const combinedReducers = { 8 | [reducerNames.global]: globalReducer, 9 | }; 10 | 11 | // List of reducers to be persisted 12 | // NOTE: Do NOT persist the root reducer, as it would persist the entire state tree, causing redundancy and potential compatibility issues 13 | const persistReducers = [reducerNames.global, reducerNames.rescue]; 14 | 15 | // redux-persist configuration object 16 | // storage: Specifies the storage engine (here, localStorage) 17 | // whitelist: Only reducers in this list will be persisted, improving security and flexibility 18 | export const persistConfig = { 19 | key: reducerNames.root, 20 | storage, 21 | whitelist: persistReducers, 22 | }; 23 | -------------------------------------------------------------------------------- /apps/frontend/user/src/store/reducerNames.ts: -------------------------------------------------------------------------------- 1 | const reducerNames = { 2 | // key: The root key for persistence, usually 'root' 3 | root: 'root', 4 | global: 'global', 5 | rescue: 'rescue', 6 | }; 7 | 8 | export default reducerNames; 9 | -------------------------------------------------------------------------------- /apps/frontend/user/src/store/reduxStore.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from '@reduxjs/toolkit'; 2 | import { persistReducer, persistStore } from 'redux-persist'; 3 | 4 | import envConfig from '../config'; 5 | 6 | import { combinedReducers, persistConfig } from './reducerConfig'; 7 | 8 | const rootReducer = combineReducers(combinedReducers); 9 | 10 | export const store = configureStore({ 11 | reducer: persistReducer(persistConfig, rootReducer), 12 | middleware: (getDefaultMiddleware) => 13 | getDefaultMiddleware({ 14 | serializableCheck: false, // needed for redux-persist 15 | }), 16 | devTools: envConfig?.env !== 'prod', // Enable Redux DevTools in non-production environments 17 | }); 18 | 19 | export const persistor = persistStore(store); 20 | 21 | // ReduxState represents the entire Redux state tree; use this type for typing selectors and state in components 22 | export type ReduxState = ReturnType; 23 | export type AppDispatch = typeof store.dispatch; 24 | -------------------------------------------------------------------------------- /apps/frontend/user/src/types/AnimalType.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from '@/features/RescueDetail/components/RescueInteraction'; 2 | import { 3 | RescueParticipantType, 4 | RescueUpdate, 5 | } from '@/features/RescueDetail/types'; 6 | 7 | export type AnimalRescueStatus = 8 | | 'pending' 9 | | 'inProgress' 10 | | 'treated' 11 | | 'recovering' 12 | | 'awaitingAdoption' 13 | | 'adopted' 14 | | 'failed'; 15 | 16 | export interface AnimalDetail { 17 | id: string; 18 | name: string; 19 | animalType: string; 20 | age: 'baby' | 'young' | 'adult' | 'senior'; 21 | appearance: { 22 | color: string; 23 | hasInjury: boolean; 24 | injuryDescription?: string; 25 | otherFeatures?: string; 26 | }; 27 | location: { 28 | address: string; 29 | latitude: number; 30 | longitude: number; 31 | }; 32 | foundTime: string; 33 | status: AnimalRescueStatus; 34 | statusDescription: string; 35 | reporterPhotos: string[]; 36 | videos?: string[]; 37 | reporter: { 38 | id: string; 39 | name: string; 40 | contactInfo: { 41 | phone: string; 42 | email?: string; 43 | }; 44 | }; 45 | updates: RescueUpdate[]; 46 | interactions: { 47 | comments: Comment[]; 48 | rescueParticipants: RescueParticipantType[]; 49 | }; 50 | 51 | stats: { 52 | viewCount: number; 53 | likeCount: number; 54 | shareCount: number; 55 | }; 56 | createdAt: string; 57 | updatedAt: string; 58 | } 59 | -------------------------------------------------------------------------------- /apps/frontend/user/src/types/LayoutType.ts: -------------------------------------------------------------------------------- 1 | import type { ReactElement } from 'react'; 2 | import type { NavigateFunction } from 'react-router-dom'; 3 | 4 | export const menuTypes = { 5 | link: 'link', 6 | component: 'component', 7 | } as const; 8 | 9 | export type MenuType = (typeof menuTypes)[keyof typeof menuTypes]; 10 | 11 | export interface MenuItemType { 12 | label: string; 13 | to?: string; 14 | classNames?: string[]; 15 | isAvailableOnMobile?: boolean; 16 | isOnlyMobile?: boolean; 17 | component?: ReactElement; 18 | action?: string; 19 | props?: Record; 20 | type: MenuType; 21 | } 22 | 23 | export interface MenuRenderType { 24 | menuItems: MenuItemType[]; 25 | activePath?: string; 26 | navigate: NavigateFunction; 27 | } 28 | 29 | export type RouterHandle = { 30 | isMenuAvailable?: boolean; 31 | isRequireUserLogin?: boolean; 32 | isFooterAvailable?: boolean; 33 | }; 34 | 35 | export interface RouterInfoType { 36 | data: Record | undefined; 37 | handle: RouterHandle; 38 | id: string; 39 | params: Record | undefined; 40 | pathname: string; 41 | } 42 | 43 | export interface RootLayoutHeaderProps { 44 | menuItems: MenuItemType[]; 45 | navigate: NavigateFunction; 46 | currentRouterInfo?: RouterInfoType; 47 | } 48 | -------------------------------------------------------------------------------- /apps/frontend/user/src/utils/apiClient.ts: -------------------------------------------------------------------------------- 1 | import createApiClient from '@pawhaven/shared-frontend/cores/http'; 2 | import storageTool from '@pawhaven/shared-frontend/utils/storage'; 3 | 4 | import envConfig from '@/config'; 5 | 6 | /** 7 | * Create a single shared API client instance for this app. 8 | * Ensures all API requests share the same configuration. 9 | */ 10 | export const apiClient = createApiClient({ 11 | timeout: envConfig?.http?.timeout, 12 | baseURL: envConfig?.http?.baseURL ?? '', 13 | accessToken: storageTool.get('access-token')!, 14 | enableSign: true, 15 | prefix: envConfig?.http?.prefix, 16 | privateKey: envConfig?.http?.privateKey, 17 | }); 18 | 19 | export default apiClient; 20 | -------------------------------------------------------------------------------- /apps/frontend/user/src/utils/getStatusColorByPrefix.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ColorPrefix, 3 | RescueStatusType, 4 | StatusColorType, 5 | } from '@/features/Home/types'; 6 | 7 | interface GetStatusColorParams { 8 | status: RescueStatusType; 9 | prefix: ColorPrefix; 10 | } 11 | 12 | const bgStatusColors: Record = { 13 | pending: 'bg-rescue-pending', 14 | inProgress: 'bg-rescue-inProgress', 15 | treated: 'bg-rescue-treated', 16 | recovering: 'bg-rescue-recovering', 17 | awaitingAdoption: 'bg-rescue-awaitingAdoption', 18 | adopted: 'bg-rescue-adopted', 19 | failed: 'bg-rescue-failed', 20 | }; 21 | 22 | const textStatusColors: Record = { 23 | pending: 'text-rescue-pending', 24 | inProgress: 'text-rescue-inProgress', 25 | treated: 'text-rescue-treated', 26 | recovering: 'text-rescue-recovering', 27 | awaitingAdoption: 'text-rescue-awaitingAdoption', 28 | adopted: 'text-rescue-adopted', 29 | failed: 'text-rescue-failed', 30 | }; 31 | 32 | const borderStatusColors: Record = { 33 | pending: 'border-rescue-pending', 34 | inProgress: 'border-rescue-inProgress', 35 | treated: 'border-rescue-treated', 36 | recovering: 'border-rescue-recovering', 37 | awaitingAdoption: 'border-rescue-awaitingAdoption', 38 | adopted: 'border-rescue-adopted', 39 | failed: 'border-rescue-failed', 40 | }; 41 | 42 | const getStatusColorByPrefix = ({ 43 | status, 44 | prefix, 45 | }: GetStatusColorParams): StatusColorType => { 46 | switch (prefix) { 47 | case 'bg': 48 | return bgStatusColors[status]; 49 | case 'text': 50 | return textStatusColors[status]; 51 | case 'border': 52 | return borderStatusColors[status]; 53 | default: 54 | throw new Error(`Unsupported prefix: ${prefix}`); 55 | } 56 | }; 57 | 58 | export default getStatusColorByPrefix; 59 | -------------------------------------------------------------------------------- /apps/frontend/user/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@pawhaven/tsconfig/web", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "types": ["node", "vite/client", "@modyfi/vite-plugin-yaml/modules"], 6 | "paths": { 7 | "@/*": ["./src/*"], 8 | "@pawhaven/shared-frontend": ["../../../packages//shared-frontend/*"], 9 | "@pawhaven/ui": ["../../../packages/ui/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/frontend/user/vite.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import ViteYaml from '@modyfi/vite-plugin-yaml'; 3 | import tailwindcss from '@tailwindcss/vite'; 4 | import react from '@vitejs/plugin-react'; 5 | import { defineConfig } from 'vite'; 6 | 7 | export default defineConfig({ 8 | envPrefix: 'REACT_APP_', 9 | base: '/', 10 | server: { 11 | port: 3001, 12 | strictPort: true, 13 | open: true, 14 | proxy: { 15 | '/api': { 16 | target: 'http://localhost:8080', 17 | changeOrigin: true, 18 | // rewrite: path => path.replace(/^\/api/, ""), 19 | }, 20 | }, 21 | }, 22 | define: { 23 | 'process.env': process.env, 24 | }, 25 | build: { 26 | outDir: 'build', 27 | }, 28 | plugins: [ 29 | tsconfigPaths(), 30 | react({ 31 | // Enable react19 features 32 | babel: { 33 | plugins: [['babel-plugin-react-compiler', { target: '19' }]], 34 | }, 35 | }), 36 | ViteYaml(), 37 | tailwindcss(), 38 | ], 39 | css: { 40 | modules: { 41 | // This is the default value, but you can customize it if needed 42 | generateScopedName: '[local]__[hash:base64:5]', 43 | }, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | // Header must follow the pattern: type(scope?): subject (e.g., 'feat(parser): add new feature') 2 | 3 | module.exports = { 4 | extends: ['@commitlint/config-conventional'], 5 | rules: { 6 | // Enforce type to be one of the specified types 7 | 'type-enum': [ 8 | 2, 9 | 'always', 10 | ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore'], 11 | ], 12 | 13 | // Scope is optional and can be any case (uppercase, lowercase, or Chinese) 14 | 'scope-case': [0], 15 | 16 | // Subject can include uppercase, lowercase, or Chinese 17 | // Setting to 0 disables the lowercase enforcement 18 | 'subject-case': [0], 19 | 20 | // Subject must be at least 5 characters long (since Chinese takes fewer characters) 21 | 'subject-min-length': [2, 'always', 5], 22 | 23 | // Enforce maximum header length of 100 characters 24 | 'header-max-length': [2, 'always', 100], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /libs/configs/eslint-config/README.MD: -------------------------------------------------------------------------------- 1 | # ESLint Config for Monorepo Projects 2 | 3 | This package provides a shared ESLint configuration tailored for monorepo projects, using ESLint v8 with CommonJS setup. 4 | 5 | ## How to Use in Sub-Packages 6 | 7 | Follow these two steps to apply this ESLint config in your sub-packages (e.g., `apps/frontend/user`): 8 | 9 | 1. **Add the workspace dependency** 10 | In your sub-package's `package.json`, add `@pawhaven/eslint-config` as a workspace-aware dependency: 11 | 12 | ```json 13 | { 14 | "dependencies": { 15 | "@pawhaven/eslint-config": "*" 16 | } 17 | } 18 | ``` 19 | 20 | 2. **Extend the config in `.eslintrc.cjs`** 21 | In the sub-package root, create or update `.eslintrc.cjs` to extend the desired config: 22 | 23 | ```js 24 | module.exports = { 25 | extends: ['@pawhaven/eslint-config/web'], 26 | // customize or override rules as needed 27 | }; 28 | ``` 29 | 30 | ## Available Configs 31 | 32 | Choose the config that matches your project type: 33 | 34 | - Web projects: `'@pawhaven/eslint-config/web'` 35 | - Node.js projects: `'@pawhaven/eslint-config/node'` 36 | 37 | ## Requirements 38 | 39 | Ensure your project uses ESLint v8 or higher and compatible peer dependencies as specified in this package’s `package.json` to prevent conflicts. 40 | 41 | --- 42 | 43 | For more details or issues, check the package source or open an issue. 44 | -------------------------------------------------------------------------------- /libs/configs/eslint-config/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | // Central export file for all ESLint configurations in the PawHaven monorepo. 3 | // This allows each app to import only what it needs, e.g.: 4 | // - "@pawhaven/eslint-config/base" 5 | // - "@pawhaven/eslint-config/react" 6 | // - "@pawhaven/eslint-config/node" 7 | 8 | module.exports = { 9 | base: require('./base'), 10 | web: require('./web'), 11 | node: require('./node'), 12 | }; 13 | -------------------------------------------------------------------------------- /libs/configs/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pawhaven/eslint-config", 3 | "version": "1.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "exports": { 7 | ".": "./index.js", 8 | "./base": "./base.js", 9 | "./web": "./web.js", 10 | "./node": "./node.js" 11 | }, 12 | "peerDependencies": { 13 | "@tanstack/eslint-plugin-query": "^5.74.7", 14 | "@typescript-eslint/eslint-plugin": "^8.32.1", 15 | "@typescript-eslint/parser": "^8.32.1", 16 | "eslint": "^8.5.7", 17 | "eslint-config-prettier": "^10.1.8", 18 | "eslint-import-resolver-typescript": "^4.4.4", 19 | "eslint-config-airbnb-base": "^15.0.0", 20 | "eslint-plugin-import": "^2.32.0", 21 | "eslint-plugin-jsx-a11y": "^6.10.2", 22 | "eslint-plugin-prettier": "^5.5.4", 23 | "eslint-plugin-react": "^7.37.5", 24 | "eslint-plugin-react-hooks": "^7.0.0", 25 | "eslint-plugin-react-refresh": "^0.4.20", 26 | "prettier": "^3.6.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /libs/configs/tsconfig/README.MD: -------------------------------------------------------------------------------- 1 | # PawHaven Shared TypeScript Configurations 2 | 3 | This package provides shared TypeScript configuration presets for the PawHaven monorepo, enabling consistent and maintainable TypeScript setups across all sub-packages. 4 | 5 | ## Usage 6 | 7 | Follow these two steps to apply this tsconfig config in your sub-packages (e.g., `apps/frontend/user`): 8 | 9 | 1. Add `@pawhaven/tsconfig` to your sub-package's `package.json` dependencies (workspace-aware): 10 | 11 | ```json 12 | { 13 | "dependencies": { 14 | "@pawhaven/tsconfig": "*" 15 | } 16 | } 17 | ``` 18 | 19 | 2. Extend the shared TypeScript config in your sub-package's `tsconfig.json`: 20 | 21 | ```json 22 | { 23 | "extends": "@pawhaven/tsconfig/web", 24 | "compilerOptions": { 25 | // your overrides here 26 | } 27 | } 28 | ``` 29 | 30 | Available presets include: 31 | 32 | - `@pawhaven/tsconfig/web` — for web projects 33 | - `@pawhaven/tsconfig/node` — for Node.js projects 34 | 35 | ## Features 36 | 37 | - **Workspace resolution:** Sub-packages can reference these configs directly without copying files. 38 | - **Consistency:** Centralized config ensures uniform compiler options across the monorepo. 39 | 40 | ## Notes 41 | 42 | - Ensure your TypeScript version is compatible with the shared configs. 43 | - When extending, configure `paths` and `baseUrl` in your sub-package as needed to resolve modules correctly. 44 | - Avoid duplicating configs; rely on this package to keep settings centralized and up to date. 45 | 46 | ## VSCode Path Aliases for IntelliSense 47 | 48 | To enable IntelliSense and autocomplete for TypeScript path aliases (e.g., `@pawhaven/*`) in VSCode, you should also add corresponding path mappings to your workspace or project `.vscode/settings.json`. This helps VSCode resolve aliases for navigation and suggestions. 49 | 50 | **Example:** 51 | 52 | ```json 53 | { 54 | "path-autocomplete.pathMappings": { 55 | "@pawhaven/ui/*": "${workspaceFolder}/packages/ui/*", 56 | "@pawhaven/shared-frontend/*": "${workspaceFolder}/packages/shared-frontend/*" 57 | }, 58 | "path-autocomplete.extensionOnImport": true 59 | } 60 | ``` 61 | 62 | Adjust the path (`../packages/*`) as needed for your workspace structure. This ensures that VSCode recognizes and autocompletes the `@pawhaven/*` imports. 63 | -------------------------------------------------------------------------------- /libs/configs/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Target latest ECMAScript version 4 | "target": "esnext", 5 | 6 | // Use ESNext module system 7 | "module": "esnext", 8 | 9 | // Include the latest ES standard library 10 | "lib": ["esnext"], 11 | 12 | // Allow compiling JavaScript files alongside TypeScript 13 | "allowJs": true, 14 | 15 | // Skip type checking for declaration files for faster builds 16 | "skipLibCheck": true, 17 | 18 | // Enable interoperability between CommonJS and ES modules 19 | "esModuleInterop": true, 20 | 21 | // Allow default imports from modules without a default export 22 | "allowSyntheticDefaultImports": true, 23 | 24 | // Enable strict type checking options 25 | "strict": true, 26 | "strictNullChecks": true, 27 | "noImplicitAny": true, 28 | 29 | // Enforce consistent casing in file names 30 | "forceConsistentCasingInFileNames": true, 31 | 32 | // Disallow fallthrough cases in switch statements 33 | "noFallthroughCasesInSwitch": true, 34 | 35 | // Report unused local variables and parameters 36 | "noUnusedLocals": true, 37 | "noUnusedParameters": true, 38 | 39 | // Require explicit return values in functions 40 | "noImplicitReturns": true, 41 | 42 | // Use bundler-style module resolution (Vite/Webpack friendly) 43 | "moduleResolution": "bundler", 44 | 45 | // Allow importing JSON modules 46 | "resolveJsonModule": true, 47 | 48 | // Ensure each file can be compiled in isolation (safe for Babel/TSX) 49 | "isolatedModules": true, 50 | 51 | // Enable project references for faster builds in monorepos 52 | "composite": true, 53 | // Generate declaration files for TypeScript files 54 | "declaration": true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /libs/configs/tsconfig/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Export TSConfig files for monorepo apps. 3 | * Allows each project to extend the appropriate config: 4 | * - base: common rules for all TS projects 5 | * - web: frontend projects 6 | * - node: backend Node/NestJS projects 7 | */ 8 | export { default as base } from './base.json'; 9 | export { default as web } from './web.json'; 10 | export { default as node } from './node.json'; 11 | -------------------------------------------------------------------------------- /libs/configs/tsconfig/node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", // Node.js standard module system 5 | "outDir": "dist", // Output directory for compiled JS (if needed) 6 | "types": ["node"], // Include Node.js types 7 | "noEmit": false // Allow emitting compiled JS in backend 8 | }, 9 | "exclude": ["node_modules", "dist"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/configs/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pawhaven/tsconfig", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "index.js", 7 | "exports": { 8 | ".": "./index.js", 9 | "./base": "./base.json", 10 | "./web": "./web.json", 11 | "./node": "./node.json" 12 | }, 13 | "dependencies": { 14 | "@modyfi/vite-plugin-yaml": "^1.1.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/configs/tsconfig/web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.json", 3 | "compilerOptions": { 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "jsx": "react-jsx", 6 | "moduleResolution": "bundler", 7 | "outDir": "build" 8 | }, 9 | "exclude": ["node_modules", "dist", "build"] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pawHaven", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "engines": { 7 | "node": "22.x", 8 | "pnpm": ">=10" 9 | }, 10 | "scripts": { 11 | "prepare": "husky", 12 | "lint-staged": "lint-staged" 13 | }, 14 | "lint-staged": { 15 | "**/*.{ts,tsx,js,jsx}": [ 16 | "eslint --fix", 17 | "prettier --write" 18 | ] 19 | }, 20 | "devDependencies": { 21 | "@commitlint/cli": "^20.1.0", 22 | "@commitlint/config-conventional": "^20.0.0", 23 | "@tanstack/eslint-plugin-query": "^5.74.7", 24 | "@typescript-eslint/eslint-plugin": "^8.32.1", 25 | "@typescript-eslint/parser": "^8.32.1", 26 | "eslint": "^8.5.7", 27 | "eslint-config-airbnb-base": "^15.0.0", 28 | "eslint-config-prettier": "^10.1.8", 29 | "eslint-import-resolver-typescript": "^4.4.4", 30 | "eslint-plugin-import": "^2.32.0", 31 | "eslint-plugin-jsx-a11y": "^6.10.2", 32 | "eslint-plugin-prettier": "^5.5.4", 33 | "eslint-plugin-react": "^7.37.5", 34 | "eslint-plugin-react-hooks": "^7.0.0", 35 | "eslint-plugin-react-refresh": "^0.4.20", 36 | "husky": "^9.1.7", 37 | "lint-staged": "^16.2.0", 38 | "prettier": "^3.6.2", 39 | "typescript": "^5.9.2" 40 | }, 41 | "packageManager": "pnpm@10.19.0" 42 | } 43 | -------------------------------------------------------------------------------- /packages/i18n/README.MD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/packages/i18n/README.MD -------------------------------------------------------------------------------- /packages/i18n/index.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import LocaleKeys from '@pawhaven/shared-frontend/constants/localeKey'; 4 | import getLocale from '@pawhaven/shared-frontend/utils/getLocale'; 5 | 6 | import deDE from './de-DE.json'; 7 | import enUS from './en-US.json'; 8 | import zhCN from './zh-CN.json'; 9 | 10 | const defaultLanguage = LocaleKeys['en-US']; 11 | const languageResources = { 12 | 'zh-CN': { translation: zhCN }, 13 | 'en-US': { translation: enUS }, 14 | 'de-DE': { translation: deDE }, 15 | }; 16 | const currentLanguage = getLocale( 17 | defaultLanguage, 18 | Object.keys(languageResources), 19 | ); 20 | 21 | i18n.use(initReactI18next).init({ 22 | resources: languageResources, 23 | lng: currentLanguage, 24 | fallbackLng: defaultLanguage, 25 | interpolation: { 26 | escapeValue: false, 27 | }, 28 | }); 29 | 30 | export default i18n; 31 | -------------------------------------------------------------------------------- /packages/i18n/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pawhaven/i18n", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": "22.x", 7 | "pnpm": ">=10" 8 | }, 9 | "dependencies": { 10 | "@pawhaven/shared-frontend": "workspace:*", 11 | "i18next": "^25.6.0", 12 | "react-i18next": "^16.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/shared-backend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@pawhaven/eslint-config/node'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/shared-backend/DTO/Auth/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { Type } from 'class-transformer' 3 | import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator' 4 | 5 | export default class CreateUserDTO { 6 | // 必选项描述 7 | @ApiProperty({ description: '用户姓名', default: '二狗' }) // 默认值设置 8 | @IsNotEmpty({ message: '姓名为必填项' }) 9 | @IsString() 10 | @Type(() => String) 11 | @MinLength(2, { message: '姓名最小长度为2' }) 12 | @MaxLength(10, { message: '姓名最大长度为10' }) 13 | readonly userName: string 14 | 15 | @ApiProperty({ description: '密码' }) 16 | @IsNotEmpty({ message: '密码为必填项' }) 17 | @IsString() 18 | @Type(() => String) 19 | readonly password: string 20 | } 21 | -------------------------------------------------------------------------------- /packages/shared-backend/DTO/Auth/interface.ts: -------------------------------------------------------------------------------- 1 | import type { Schema } from 'mongoose' 2 | 3 | // token中需要记录的用户信息类型 4 | export interface UserAccessInfo { 5 | userName: string | undefined 6 | userID: Schema.Types.ObjectId | undefined 7 | roles: string[] | undefined 8 | } 9 | -------------------------------------------------------------------------------- /packages/shared-backend/DTO/Auth/userInfo.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { Type } from 'class-transformer' 3 | import { IsArray, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator' 4 | import type { Schema } from 'mongoose' 5 | 6 | export default class UserInfoDTO { 7 | // 必选项描述 8 | @ApiProperty({ description: '用户姓名', default: '二狗' }) // 默认值设置 9 | @IsNotEmpty({ message: '姓名为必填项' }) 10 | @IsString() 11 | @Type(() => String) 12 | @MinLength(2, { message: '姓名最小长度为2' }) 13 | @MaxLength(10, { message: '姓名最大长度为10' }) 14 | readonly userName: string 15 | 16 | @ApiProperty({ description: '用户ID' }) 17 | userID: Schema.Types.ObjectId 18 | 19 | @ApiProperty({ description: '用户权限组' }) 20 | @IsArray() 21 | readonly roles?: string[] 22 | } 23 | -------------------------------------------------------------------------------- /packages/shared-backend/DTO/Document/create-PDF.DTO.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' 2 | import { Type } from 'class-transformer' 3 | import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator' 4 | import { PDFOptions } from 'puppeteer' 5 | 6 | export default class CreatePDFDTO { 7 | @ApiProperty({ description: 'PDF template name' }) 8 | @IsNotEmpty({ message: 'Template is requied' }) 9 | @IsString() 10 | @Type(() => String) 11 | template: string 12 | 13 | @ApiPropertyOptional({ description: 'Locale for generate PDF file' }) 14 | @Type(() => String) 15 | locale?: string 16 | 17 | @ApiPropertyOptional({ description: 'PDF header data for generate PDF file' }) 18 | @IsOptional() 19 | @IsObject() 20 | PDFHeaderData?: Record 21 | 22 | @ApiPropertyOptional({ description: 'PDF data for generate PDF file' }) 23 | @IsNotEmpty({ message: 'PDF content data is required' }) 24 | @IsObject() 25 | PDFContentData: Record 26 | 27 | @ApiPropertyOptional({ description: 'PDF footer data for generate PDF file' }) 28 | @IsOptional() 29 | @IsObject() 30 | PDFFooterData?: Record 31 | 32 | @ApiPropertyOptional({ description: 'PDF config for generate PDF file' }) 33 | @IsOptional() 34 | @IsObject() 35 | PDFOptions?: PDFOptions 36 | } 37 | -------------------------------------------------------------------------------- /packages/shared-backend/DTO/Document/email-options.DTO.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' 2 | import { Type } from 'class-transformer' 3 | import { IsArray, IsNotEmpty, IsOptional } from 'class-validator' 4 | import { IsString } from 'class-validator' 5 | 6 | // More options can refer to ISendMailOptions from '@nestjs-modules/mailer' 7 | export default class EmailOptionsDTO { 8 | @ApiProperty({ description: 'Email to' }) 9 | @IsNotEmpty({ message: 'To is required' }) 10 | @IsString({ each: true }) 11 | @Type(() => String) 12 | to: string | string[] 13 | 14 | @ApiProperty({ description: 'Email subject' }) 15 | @IsString() 16 | @Type(() => String) 17 | subject: string 18 | 19 | @ApiPropertyOptional({ description: 'Email cc' }) 20 | @IsOptional() 21 | @IsString() 22 | @Type(() => String) 23 | cc?: string | string[] 24 | 25 | @ApiPropertyOptional({ description: 'Email bcc' }) 26 | @IsOptional() 27 | @IsString() 28 | @Type(() => String) 29 | bcc?: string | string[] 30 | 31 | @ApiPropertyOptional({ description: 'Email attachments' }) 32 | @IsOptional() 33 | @IsArray() 34 | @Type(() => Array) 35 | attachments?: any[] 36 | } 37 | -------------------------------------------------------------------------------- /packages/shared-backend/DTO/Document/send-email.DTO.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { Type } from 'class-transformer' 3 | import { IsNotEmpty, IsObject, ValidateNested } from 'class-validator' 4 | import { IsString } from 'class-validator' 5 | import EmailOptionsDTO from './email-options.DTO' 6 | 7 | export default class EmailPayloadDTO { 8 | @ApiProperty({ description: 'Email template name' }) 9 | @IsNotEmpty({ message: 'Template is requied' }) 10 | @IsString() 11 | @Type(() => String) 12 | template: string 13 | 14 | @ApiProperty({ description: 'Email locale' }) 15 | @IsNotEmpty({ message: 'Locale is requied' }) 16 | @IsString() 17 | @Type(() => String) 18 | locale: string 19 | 20 | @ApiProperty({ description: 'Email data payload' }) 21 | @IsNotEmpty({ message: 'Email payload is requied' }) 22 | @IsObject() 23 | @Type(() => Object) 24 | payload: Record 25 | 26 | @ApiProperty({ description: 'Email options like from, to, subject, etc.' }) 27 | @IsNotEmpty({ message: 'Email options is requied' }) 28 | @ValidateNested() 29 | @Type(() => EmailOptionsDTO) 30 | options: EmailOptionsDTO 31 | } 32 | -------------------------------------------------------------------------------- /packages/shared-backend/DTO/Trip/tripInfo.DTO.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { Type } from 'class-transformer' 3 | import { IsNotEmpty, IsString } from 'class-validator' 4 | 5 | export default class TripInfoDTO { 6 | @ApiProperty({ description: 'Trip destination' }) 7 | @IsNotEmpty({ message: 'Trip destination is required !' }) 8 | @IsString() 9 | @Type(() => String) 10 | destination: string 11 | 12 | @ApiProperty({ description: 'Trip date' }) 13 | @IsNotEmpty({ message: 'Trip date is required !' }) 14 | @IsString() 15 | @Type(() => String) 16 | date: string 17 | 18 | @ApiProperty({ description: 'trip note' }) 19 | @IsNotEmpty({ message: 'note is requied' }) 20 | @IsString() 21 | @Type(() => String) 22 | note: string 23 | } 24 | -------------------------------------------------------------------------------- /packages/shared-backend/assets/HMAC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/packages/shared-backend/assets/HMAC.png -------------------------------------------------------------------------------- /packages/shared-backend/constants/MSMessagePatterns/auth.messagePattern.ts: -------------------------------------------------------------------------------- 1 | import { MicroServiceNames, Versions } from '../constant'; 2 | 3 | /* 4 | // Please define the message pattern by the following format: 5 | // 1: microservice name 6 | // 2: specific message action name (CRUD) 7 | // 3: Version (To clearfy different version of the same message) 8 | Example: 9 | GET_TRIP_LIST: `${MicroServiceNames.TRIP}.getTripList.${Versions.v1}` 10 | */ 11 | const AuthMessagePattern = { 12 | REGISTER: `${MicroServiceNames.AUTH}.register.${Versions.v1}`, 13 | LOGIN: `${MicroServiceNames.AUTH}.login.${Versions.v1}`, 14 | REFRESH: `${MicroServiceNames.AUTH}.refresh.${Versions.v1}`, 15 | }; 16 | export default AuthMessagePattern; 17 | -------------------------------------------------------------------------------- /packages/shared-backend/constants/MSMessagePatterns/trip.messagePattern.ts: -------------------------------------------------------------------------------- 1 | import { MicroServiceNames, Versions } from '../constant'; 2 | 3 | /* 4 | // Please define the message pattern by the following format: 5 | // 1: microservice name 6 | // 2: specific message action name (CRUD) 7 | // 3: Version (To clearfy different version of the same message) 8 | Example: 9 | GET_TRIP_LIST: `${MicroServiceNames.TRIP}.getTripList.${Versions.v1}` 10 | */ 11 | const TripMessagePattern = { 12 | GET_TRIP_LIST1: `${MicroServiceNames.TRIP}.getTripList.${Versions.v1}`, 13 | GET_TRIP_LIST2: `${MicroServiceNames.TRIP}.getTripList.${Versions.v2}`, 14 | ADD_TRIP: `${MicroServiceNames.TRIP}.addTrip.${Versions.v1}`, 15 | }; 16 | export default TripMessagePattern; 17 | -------------------------------------------------------------------------------- /packages/shared-backend/constants/constant.ts: -------------------------------------------------------------------------------- 1 | // environment constants 2 | export const EnvConstant = { 3 | dev: 'dev', 4 | uat: 'uat', 5 | test: 'test', 6 | prod: 'prod', 7 | }; 8 | export const MicroServiceNames = { 9 | TRIP: 'trip', 10 | DOCUMENT: 'document', 11 | AUTH: 'auth', 12 | }; 13 | 14 | // microservice names when using microservice client 15 | export const MSClientNames = { 16 | MS_TRIP: 'MS_TRIP', 17 | MS_DOCUMENT: 'MS_DOCUMENT', 18 | MS_AUTH: 'MS_AUTH', 19 | }; 20 | 21 | // config keys from env config file 22 | export const ConfigKeys = { 23 | DBConnections: 'DBConnections', 24 | MicroServices: 'MicroServices', 25 | microServiceOptions: 'microServiceOptions', 26 | I18n: 'I18nOptions', 27 | }; 28 | 29 | export const Versions = { 30 | v1: 'v1', 31 | v2: 'v2', 32 | }; 33 | 34 | type VersionType = (typeof Versions)[keyof typeof Versions]; 35 | type MicroServiceNameType = (typeof MicroServiceNames)[keyof typeof MicroServiceNames]; 36 | export type MSMessagePatternType = { 37 | [key: string]: `${MicroServiceNameType}.${string}.${VersionType}`; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/shared-backend/constants/enum.ts: -------------------------------------------------------------------------------- 1 | export enum LocaleKeys { 2 | zh_CN = 'zh_CN', 3 | en_US = 'en_US', 4 | } 5 | 6 | export enum UserRole { 7 | GUEST = 'GUEST', 8 | ADMIN = 'ADMIN', 9 | MAJOR = 'MAJOR', 10 | SUPER = 'SUPER', 11 | } 12 | 13 | export enum Decorators { 14 | noSign = 'NO_SIGN', 15 | noToken = 'NO_TOKEN', 16 | ACLPermissions = 'ACLPermissions', 17 | } 18 | -------------------------------------------------------------------------------- /packages/shared-backend/core/configModule/configs.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | 4 | import getConfigValues from '../../utils/getConfigValues'; 5 | 6 | @Global() 7 | @Module({}) 8 | class ConfigsModule { 9 | /** 10 | * dynamic configration 11 | * @param configFilePath config file path 12 | */ 13 | static forRoot(configFilePath: string): DynamicModule { 14 | const configValues = getConfigValues(configFilePath); 15 | const DynamicConfigModule = ConfigModule.forRoot({ 16 | load: [() => configValues], 17 | envFilePath: '.env', 18 | isGlobal: true, 19 | cache: true, 20 | }); 21 | 22 | return { 23 | module: ConfigsModule, 24 | imports: [DynamicConfigModule], 25 | }; 26 | } 27 | } 28 | export default ConfigsModule; 29 | -------------------------------------------------------------------------------- /packages/shared-backend/core/dataBase/db.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { MongooseModule } from '@nestjs/mongoose'; 4 | 5 | import { ConfigKeys } from '../../constants/constant'; 6 | import getConfigValues from '../../utils/getConfigValues'; 7 | 8 | @Module({}) 9 | class DatabaseModule { 10 | /** 11 | * dynamic Mongoose connection 12 | * @param dbConnectKeys the keys of the db connections in the config file 13 | */ 14 | static forRoot(configFilePath: string): DynamicModule { 15 | try { 16 | const configValues = getConfigValues(configFilePath); 17 | const availableDBConnections = 18 | configValues?.[ConfigKeys.DBConnections] 19 | ?.filter((item) => item?.enable) 20 | ?.map((item) => item?.options) ?? []; 21 | const isMultipleDB = availableDBConnections?.length > 1; 22 | const connectionProviders = availableDBConnections?.map((DBItem) => ({ 23 | connectionName: DBItem, 24 | useFactory: () => { 25 | return { ...(DBItem ?? {}) }; 26 | }, 27 | })); 28 | 29 | const DBConnection = connectionProviders.map((provider) => 30 | MongooseModule.forRootAsync({ 31 | // Must clearfy each DB name if there is multiple DB connections 32 | connectionName: isMultipleDB ? provider?.connectionName : null, 33 | useFactory: provider.useFactory, 34 | inject: provider.inject, 35 | }), 36 | ); 37 | 38 | return { 39 | module: DatabaseModule, 40 | imports: [ConfigModule, ...DBConnection], 41 | exports: [...DBConnection], 42 | }; 43 | } catch (error) { 44 | throw new Error(`DB connection error :${error}`); 45 | } 46 | } 47 | } 48 | export default DatabaseModule; 49 | -------------------------------------------------------------------------------- /packages/shared-backend/core/httpClient/httpClient.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@nestjs/axios'; 2 | import { Global, Module } from '@nestjs/common'; 3 | 4 | import HttpClientService from './HttpClient.service'; 5 | 6 | @Global() 7 | @Module({ 8 | imports: [HttpModule], 9 | providers: [HttpClientService], 10 | exports: [HttpClientService], 11 | }) 12 | export default class HttpClientModule {} 13 | -------------------------------------------------------------------------------- /packages/shared-backend/core/httpClient/httpExceptionFilter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | Catch, 4 | ExceptionFilter, 5 | HttpException, 6 | HttpStatus, 7 | Injectable, 8 | Logger, 9 | } from '@nestjs/common'; 10 | 11 | import { HttpResType } from './interface'; 12 | 13 | @Injectable() 14 | @Catch() 15 | export default class HttpExceptionFilter implements ExceptionFilter { 16 | private readonly logger = new Logger(HttpExceptionFilter.name); 17 | 18 | catch(exception: any, host: ArgumentsHost) { 19 | const isRpcContext = host.getType() === 'rpc'; 20 | const isHttpContext = host.getType() === 'http'; 21 | 22 | let message = 'Service error'; 23 | let status = HttpStatus.INTERNAL_SERVER_ERROR; 24 | let data = null; 25 | 26 | if (exception instanceof HttpException) { 27 | const res = exception.getResponse(); 28 | message = typeof res === 'string' ? res : (res as any)?.message || exception.message; 29 | status = 30 | typeof res === 'object' && 'status' in res 31 | ? (res as any).status || exception.getStatus() 32 | : exception.getStatus(); 33 | 34 | data = typeof res === 'object' ? (res as any).data || null : null; 35 | } else { 36 | message = exception?.message ?? message; 37 | status = exception?.status ?? status; 38 | data = exception?.data ?? null; 39 | } 40 | 41 | const errorResponse: HttpResType = { 42 | status, 43 | isSuccess: false, 44 | message, 45 | data, 46 | }; 47 | 48 | this.logger.error( 49 | `Exception caught (context: ${host.getType()}):`, 50 | JSON.stringify(errorResponse), 51 | ); 52 | if (isHttpContext) { 53 | const response = host.switchToHttp().getResponse(); 54 | response.status(status).json(errorResponse); 55 | } 56 | if (isRpcContext) { 57 | return errorResponse; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/shared-backend/core/httpClient/httpInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | @Injectable() 6 | export default class HttpSuccessInterceptor implements NestInterceptor { 7 | intercept(_context: ExecutionContext, next: CallHandler): Observable { 8 | return next.handle().pipe( 9 | map((data) => { 10 | return { 11 | status: 200, 12 | isSuccess: true, 13 | message: 'Request successful', 14 | data, 15 | }; 16 | }), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/shared-backend/core/httpClient/interface.ts: -------------------------------------------------------------------------------- 1 | export interface HttpResType { 2 | isSuccess: boolean; 3 | message: string; 4 | data: unknown; 5 | status: number; 6 | } 7 | export enum HttpBusinessCode { 8 | // jwt 过期 9 | jwtexpired = 'jwtexpired', 10 | invalidToken = 'invalidtoken', 11 | invalidSign = 'invalidsignature', 12 | } 13 | export enum HttpReqHeader { 14 | timestamp = 'x-timestamp', 15 | sign = 'x-sign', 16 | traceID = 'traceID', 17 | accessToken = 'access-token', 18 | locale = 'locale', 19 | } 20 | 21 | export enum HttpBusinessMappingCode { 22 | // jwt 过期 23 | jwtexpired = 'E4001', 24 | } 25 | -------------------------------------------------------------------------------- /packages/shared-backend/core/httpClient/rpcExceptionFillter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | Catch, 4 | RpcExceptionFilter, 5 | Logger, 6 | HttpStatus, 7 | } from '@nestjs/common'; 8 | import { RpcException } from '@nestjs/microservices'; 9 | import { Observable, throwError } from 'rxjs'; 10 | 11 | @Catch() 12 | export default class AllRpcExceptionsFilter implements RpcExceptionFilter { 13 | private readonly logger = new Logger(AllRpcExceptionsFilter.name); 14 | 15 | catch(exception: any, host: ArgumentsHost): Observable { 16 | let message = 'Internal microservice error'; 17 | let status = HttpStatus.INTERNAL_SERVER_ERROR; 18 | let data = null; 19 | 20 | if (exception instanceof RpcException) { 21 | const error = exception.getError(); 22 | if (typeof error === 'string') { 23 | message = error; 24 | } else if (typeof error === 'object' && error !== null) { 25 | message = (error as { message?: string }).message || message; 26 | status = (error as { status?: number }).status || status; 27 | data = (error as { data?: any }).data || null; 28 | } 29 | } else if (exception instanceof Error) { 30 | message = exception.message || message; 31 | } 32 | 33 | const errorResponse = { 34 | message, 35 | status, 36 | data, 37 | }; 38 | this.logger.error( 39 | '🚨 RPC Exception', 40 | JSON.stringify(errorResponse), 41 | exception?.stack, 42 | ); 43 | return throwError(() => new RpcException(errorResponse)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/shared-backend/core/microServiceClient/msClient.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Module } from '@nestjs/common'; 2 | import { ClientsModule } from '@nestjs/microservices'; 3 | 4 | import { ConfigKeys, MSClientNames } from '../../constants/constant'; 5 | import getConfigValues from '../../utils/getConfigValues'; 6 | 7 | @Global() 8 | @Module({}) 9 | class MSClientModule { 10 | static register(configFilePath: string): DynamicModule { 11 | const configValues = getConfigValues(configFilePath); 12 | const availableMicroServices = configValues?.[ 13 | ConfigKeys.MicroServices 14 | ]?.filter((item) => item?.enable && MSClientNames?.[item?.name]); 15 | const isExistMicroService = availableMicroServices?.length > 0; 16 | const microServices = 17 | isExistMicroService && 18 | availableMicroServices?.map((service) => ({ 19 | name: service?.name, 20 | transport: service?.transport, 21 | options: service?.options, 22 | })); 23 | 24 | const microServiceClients = ClientsModule.register(microServices || []); 25 | 26 | return { 27 | module: MSClientModule, 28 | imports: isExistMicroService ? [microServiceClients] : [], 29 | exports: isExistMicroService ? [microServiceClients] : [], 30 | }; 31 | } 32 | } 33 | 34 | export default MSClientModule; 35 | -------------------------------------------------------------------------------- /packages/shared-backend/core/microServiceClient/msClient.servicea.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { ClientProxy } from '@nestjs/microservices'; 3 | 4 | import { MSClientNames } from '../../constants/constant'; 5 | 6 | @Injectable() 7 | export default class MSService { 8 | constructor( 9 | @Inject(MSClientNames.MS_TRIP) 10 | private readonly tripClient: ClientProxy, 11 | @Inject(MSClientNames.MS_DOCUMENT) 12 | private readonly documentClient: ClientProxy, 13 | ) {} 14 | } 15 | -------------------------------------------------------------------------------- /packages/shared-backend/core/swagger/index.ts: -------------------------------------------------------------------------------- 1 | import type { INestApplication } from '@nestjs/common' 2 | import { ConfigService } from '@nestjs/config' 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' 4 | 5 | const initSwagger = (app: INestApplication) => { 6 | const swaggerTitle = app.get(ConfigService).get('swagger.title') ?? '' 7 | const swaggerDesc = app.get(ConfigService).get('swagger.description') ?? '' 8 | const swaggerVersion = app.get(ConfigService).get('swagger.version') ?? '' 9 | const swaggerPrefix = app.get(ConfigService).get('swagger.prefix') ?? '' 10 | const swaggerOptions = new DocumentBuilder() 11 | .setTitle(swaggerTitle) 12 | .setDescription(swaggerDesc) 13 | .setVersion(swaggerVersion) 14 | .addBearerAuth() 15 | .build() 16 | const document = SwaggerModule.createDocument(app, swaggerOptions) 17 | SwaggerModule.setup(swaggerPrefix, app, document) 18 | } 19 | export default initSwagger 20 | -------------------------------------------------------------------------------- /packages/shared-backend/middlewares/httpSetting.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, type NestMiddleware } from '@nestjs/common' 2 | import { HttpReqHeader } from '../core/httpClient/interface' 3 | import type { NextFunction, Request, Response } from 'express' 4 | import getTokenFromHeader from '../utils/overWriteHeader' 5 | @Injectable() 6 | export class HttpSettingMiddleware implements NestMiddleware { 7 | use(req: Request, _res: Response, next: NextFunction) { 8 | const token = getTokenFromHeader(req) 9 | // Override the access-token header with the Bearer token 10 | req.headers[HttpReqHeader.accessToken] = token 11 | next() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/shared-backend/middlewares/index.module.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common' 2 | import { HttpSettingMiddleware } from './httpSetting.middleware' 3 | 4 | @Module({ 5 | providers: [] 6 | }) 7 | export default class MiddlewareModule implements NestModule { 8 | configure(consumer: MiddlewareConsumer) { 9 | consumer 10 | .apply(HttpSettingMiddleware) 11 | .exclude( 12 | // exlude health route 13 | { path: 'health', method: RequestMethod.GET } 14 | ) 15 | .forRoutes({ 16 | path: '*', 17 | method: RequestMethod.ALL 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/shared-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@aoda/shared", 4 | "version": "1.0.0", 5 | "engines": { 6 | "node": ">=22", 7 | "pnpm": ">=10" 8 | }, 9 | "dependencies": { 10 | "@nestjs/axios": "^4.0.1", 11 | "@nestjs/common": "^11.1.6", 12 | "@nestjs/config": "^4.0.2", 13 | "@nestjs/core": "^11.1.6", 14 | "@nestjs/jwt": "^11.0.1", 15 | "@nestjs/microservices": "^11.1.6", 16 | "@nestjs/mongoose": "^11.0.3", 17 | "@nestjs/passport": "^11.0.5", 18 | "@nestjs/platform-express": "^11.1.6", 19 | "@nestjs/swagger": "^11.2.0", 20 | "@nestjs/terminus": "^11.0.0", 21 | "@nestjs/throttler": "^6.4.0", 22 | "@types/bcrypt": "^6.0.0", 23 | "@types/express": "^5.0.3", 24 | "@types/passport-jwt": "^4.0.1", 25 | "add": "^2.0.6", 26 | "axios": "^1.12.2", 27 | "bcrypt": "^6.0.0", 28 | "class-transformer": "^0.5.1", 29 | "class-validator": "^0.14.2", 30 | "cookie-parser": "^1.4.7", 31 | "cross-env": "^10.1.0", 32 | "crypto-js": "^4.2.0", 33 | "csurf": "^1.11.0", 34 | "dayjs": "^1.11.18", 35 | "dotenv": "^17.2.3", 36 | "helmet": "^8.1.0", 37 | "js-yaml": "^4.1.0", 38 | "mongoose": "^8.19.1", 39 | "passport": "^0.7.0", 40 | "passport-jwt": "^4.0.1", 41 | "passport-local": "^1.0.0", 42 | "puppeteer": "^24.24.0", 43 | "reflect-metadata": "^0.2.2", 44 | "rxjs": "^7.8.2", 45 | "swagger-ui-express": "^5.0.1", 46 | "uuid": "^13.0.0" 47 | }, 48 | "devDependencies": { 49 | "@nestjs/cli": "^11.0.10", 50 | "@nestjs/schematics": "^11.0.9", 51 | "@nestjs/testing": "^11.1.6", 52 | "@pawhaven/eslint-config": "workspace:*", 53 | "@pawhaven/tsconfig": "workspace:*", 54 | "@types/crypto-js": "^4.2.2", 55 | "@types/jest": "^30.0.0", 56 | "@types/js-yaml": "^4.0.9", 57 | "@types/node": "^24.7.1", 58 | "@types/passport-local": "^1.0.38", 59 | "@types/supertest": "^6.0.3", 60 | "source-map-support": "^0.5.21", 61 | "supertest": "^7.1.4", 62 | "ts-loader": "^9.5.4", 63 | "ts-node": "^10.9.2", 64 | "tsconfig-paths": "^4.2.0", 65 | "typescript": "^5.9.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/shared-backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@pawhaven/tsconfig/node" 3 | } 4 | -------------------------------------------------------------------------------- /packages/shared-backend/utils/convertImagToBase64.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const convertImageToBase64 = async (imageUrl: string) => { 4 | try { 5 | const response = await axios.get(imageUrl, { responseType: 'arraybuffer' }) 6 | 7 | const base64 = Buffer.from(response.data, 'binary').toString('base64') 8 | const contentType = response.headers['content-type'] 9 | if (contentType && base64) { 10 | // Return the Base64 string with the appropriate data URL scheme 11 | return `data:${contentType};base64,${base64}` 12 | } 13 | return null 14 | } catch (error) { 15 | console.error(`Converting image to base64 with issue: ${error}`) 16 | throw new Error(`Converting image to base64 with issue: ${error}`) 17 | } 18 | } 19 | 20 | export default convertImageToBase64 21 | 22 | // 数据迁移 23 | // 好的系统设计 24 | // 好的监控好 25 | // 系统维护页面 26 | -------------------------------------------------------------------------------- /packages/shared-backend/utils/getConfigValues.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | import * as yaml from 'js-yaml' 3 | import { EnvConstant } from '@shared/constants/constant' 4 | const isConfigAvaliable = (configFilePath: string) => { 5 | const avaliabEnvs = Object.values(EnvConstant) 6 | const configContent = readFileSync(configFilePath, 'utf8') 7 | const isAvaliable = 8 | avaliabEnvs.some((env) => configFilePath?.includes(env)) && 9 | Object.keys(configContent)?.length > 0 10 | return isAvaliable 11 | } 12 | 13 | const getConfigValues = (configFilePath: string) => { 14 | try { 15 | if (isConfigAvaliable(configFilePath)) { 16 | return yaml.load(readFileSync(configFilePath, 'utf8')) as Record 17 | } 18 | throw new Error('No config file exist!!!') 19 | } catch (error) { 20 | console.error(`get config value error: ${error}`) 21 | return {} 22 | } 23 | } 24 | export default getConfigValues 25 | -------------------------------------------------------------------------------- /packages/shared-backend/utils/overWriteHeader.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express' 2 | 3 | import { HttpReqHeader } from '@shared/core/httpClient/interface' 4 | 5 | const getTokenFromHeader = (request: Request) => { 6 | if (request?.headers?.[HttpReqHeader.accessToken]) { 7 | return request?.headers?.[HttpReqHeader.accessToken] 8 | } 9 | const [type, token] = request?.headers?.authorization?.split(' ') ?? [] 10 | return type === 'Bearer' ? token : undefined 11 | } 12 | export default getTokenFromHeader 13 | -------------------------------------------------------------------------------- /packages/shared-backend/utils/trim.ts: -------------------------------------------------------------------------------- 1 | export default function time(str: string) { 2 | return str.replace(/\s/g, ''); 3 | } 4 | -------------------------------------------------------------------------------- /packages/shared-frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@pawhaven/eslint-config/web'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/shared-frontend/constants/localeKey.ts: -------------------------------------------------------------------------------- 1 | enum LocaleKeys { 2 | 'zh-CN' = 'zh-CN', 3 | 'en-US' = 'en-US', 4 | 'de-DE' = 'de-DE', 5 | } 6 | export default LocaleKeys; 7 | -------------------------------------------------------------------------------- /packages/shared-frontend/constants/myPerson.ts: -------------------------------------------------------------------------------- 1 | // This is a personal information module that exports my personal details. 2 | // It should NOT be used in production code or shared publicly. 3 | const myPersonal = { 4 | profile: 'https://aoda.vercel.app/', 5 | github: 'https://github.com/aoda-zhang', 6 | email: 'mailto:aodazhang666@email.com', 7 | linkedin: 'https://www.linkedin.com/in/aodazhang"', 8 | }; 9 | export default myPersonal; 10 | -------------------------------------------------------------------------------- /packages/shared-frontend/cores/http/encrypt.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import CryptoJS from 'crypto-js'; 3 | import dayjs from 'dayjs'; 4 | import utc from 'dayjs/plugin/utc'; 5 | 6 | dayjs.extend(utc); 7 | export type SignParams = { 8 | config: Record; 9 | timestamp: string; 10 | prefix: string; 11 | privateKey: string; 12 | }; 13 | export const getUTCTimestamp = () => { 14 | return Math.floor(dayjs.utc().valueOf() / 1000); 15 | }; 16 | const formatUrl = (url: string, prefix: string) => { 17 | return url.replace(prefix, '').replace(/\//g, '')?.toLowerCase(); 18 | }; 19 | export const generateSign = ({ 20 | config, 21 | timestamp, 22 | prefix, 23 | privateKey, 24 | }: SignParams): string => { 25 | const { data, url = '', method = '' } = config; 26 | const bodyString = data ? JSON.stringify(data) : ''; 27 | return CryptoJS.HmacSHA256( 28 | `${formatUrl( 29 | url, 30 | prefix, 31 | )}> ${bodyString} +${method?.toUpperCase()}| ${timestamp} `, 32 | privateKey, 33 | ).toString(CryptoJS.enc.Hex); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/shared-frontend/cores/http/errorHandle.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import toast from 'react-hot-toast'; 3 | 4 | import { HttpBusinessMappingCode } from './types'; 5 | 6 | // const jwtExpiredHandle = async () => { 7 | // try { 8 | // if (storageTool.get(commonHeader.refreshToken)) { 9 | // const newAuthToken = await AuthAPI.refreshToken({ 10 | // refreshToken: storageTool.get(commonHeader.refreshToken), 11 | // }); 12 | // await storageTool.set( 13 | // commonHeader['access-token'], 14 | // newAuthToken?.accessToken, 15 | // ); 16 | // await storageTool.set( 17 | // commonHeader.refreshToken, 18 | // newAuthToken?.refreshToken, 19 | // ); 20 | // } 21 | // throw new Error('登陆信息有误,请重新检查'); 22 | // } catch (error) { 23 | // message.error(`登陆信息有误,请重新检查!${error}`); 24 | // await storageTool.remove(commonHeader['access-token']); 25 | // await storageTool.remove(commonHeader.refreshToken); 26 | // if (typeof window !== 'undefined') { 27 | // // eslint-disable-next-line no-undef 28 | // window.location.href = '/login'; 29 | // } 30 | // } 31 | // }; 32 | 33 | const httpErrorHandler = async (error: { 34 | data: any; 35 | isSuccess?: boolean; 36 | message: any; 37 | status: any; 38 | statusCode?: any; 39 | }) => { 40 | if (error?.data === HttpBusinessMappingCode.jwtexpired) { 41 | // jwtExpiredHandle(); 42 | } 43 | switch (error?.status ?? error?.statusCode) { 44 | case 401: 45 | case 403: 46 | // jwtExpiredHandle(); 47 | break; 48 | case 500: 49 | toast.error(error?.message); 50 | break; 51 | default: 52 | toast.error(error?.message); 53 | } 54 | }; 55 | 56 | export default httpErrorHandler; 57 | -------------------------------------------------------------------------------- /packages/shared-frontend/cores/http/types.ts: -------------------------------------------------------------------------------- 1 | export type HttpResponseType = { 2 | data: unknown; 3 | isSuccess: boolean; 4 | message: string | string[]; 5 | status: number; 6 | }; 7 | export enum commonHeader { 8 | 'access-token' = 'access-token', 9 | refreshToken = 'refreshToken', 10 | } 11 | export enum HttpBusinessMappingCode { 12 | // jwt 过期 13 | jwtexpired = 'E4001', 14 | unauthorized = 'Unauthorized', 15 | } 16 | -------------------------------------------------------------------------------- /packages/shared-frontend/cores/react-query/index.ts: -------------------------------------------------------------------------------- 1 | import { QueryCache } from '@tanstack/react-query'; 2 | import toast from 'react-hot-toast'; 3 | 4 | const getReactQueryOptions = (envConfig: Record) => { 5 | const { 6 | refetchOnReconnect = true, 7 | refetchOnWindowFocus = false, 8 | // Fresh data for 5 minutes 9 | staleTime = 5 * 60 * 1000, 10 | // Cache data for 30 minutes 11 | cacheTime = 30 * 60 * 1000, 12 | // Retry failed requests up to 2 times 13 | retry = 2, 14 | } = envConfig?.queryOptions ?? {}; 15 | return { 16 | defaultOptions: { 17 | queries: { 18 | refetchOnReconnect, 19 | refetchOnWindowFocus, 20 | staleTime, 21 | cacheTime, 22 | retry, 23 | }, 24 | }, 25 | queryCache: new QueryCache({ 26 | onError: (error, query) => { 27 | // Handle errors globally for queries 28 | if (query.state.data !== undefined) { 29 | toast.error( 30 | envConfig?.env === 'prod' 31 | ? envConfig?.systemSettings?.errorMessage 32 | : `Error: ${error instanceof Error ? error?.message : 'Unknown error'}`, 33 | ); 34 | } 35 | }, 36 | }), 37 | }; 38 | }; 39 | export default getReactQueryOptions; 40 | -------------------------------------------------------------------------------- /packages/shared-frontend/hooks/useDoubleClick.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | const useDoubleClick = (onDoubleClick: () => void) => { 4 | useEffect(() => { 5 | const handleDoubleClick = () => { 6 | if (onDoubleClick) { 7 | onDoubleClick(); 8 | } 9 | }; 10 | document.addEventListener('dblclick', handleDoubleClick); 11 | return () => { 12 | document.removeEventListener('dblclick', handleDoubleClick); 13 | }; 14 | }, [onDoubleClick]); 15 | }; 16 | export default useDoubleClick; 17 | -------------------------------------------------------------------------------- /packages/shared-frontend/hooks/useIsMobile.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useIsMobile = () => { 4 | const getIsMobile = () => window.innerWidth <= 768; 5 | const [isMobile, setIsMobile] = useState(getIsMobile()); 6 | 7 | useEffect(() => { 8 | const handleResize = () => { 9 | setIsMobile(getIsMobile()); 10 | }; 11 | 12 | window.addEventListener('resize', handleResize); 13 | return () => window.removeEventListener('resize', handleResize); 14 | }, []); 15 | 16 | return isMobile; 17 | }; 18 | 19 | export default useIsMobile; 20 | -------------------------------------------------------------------------------- /packages/shared-frontend/hooks/useRouterInfo.ts: -------------------------------------------------------------------------------- 1 | import { useMatches } from 'react-router-dom'; 2 | // Get current router information 3 | const useRouterInfo = (): T => { 4 | const matches = useMatches(); 5 | const current = matches.at(-1); 6 | return current as T; 7 | }; 8 | 9 | export default useRouterInfo; 10 | -------------------------------------------------------------------------------- /packages/shared-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pawhaven/shared-frontend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": "22.x", 7 | "pnpm": ">=10" 8 | }, 9 | "dependencies": { 10 | "@emotion/react": "^11.14.0", 11 | "@emotion/styled": "^11.14.1", 12 | "@pawhaven/eslint-config": "workspace:*", 13 | "@pawhaven/tsconfig": "workspace:*", 14 | "@tanstack/react-query": "^5.90.2", 15 | "@types/react": "^19.2.2", 16 | "@types/react-dom": "^19.2.1", 17 | "axios": "^1.12.2", 18 | "clsx": "^2.1.1", 19 | "crypto-js": "^4.2.0", 20 | "react-router-dom": "^7.9.4", 21 | "dayjs": "^1.11.18", 22 | "history": "^5.3.0", 23 | "lodash": "^4.17.21", 24 | "lucide-react": "^0.545.0", 25 | "or": "^0.2.0", 26 | "react": "^19.2.0", 27 | "react-dom": "^19.2.0", 28 | "react-hot-toast": "^2.6.0", 29 | "typescript": "^5.9.3" 30 | }, 31 | "devDependencies": { 32 | "@types/crypto-js": "^4.2.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/shared-frontend/resources/featurePlan.md: -------------------------------------------------------------------------------- 1 | # 流浪动物救助 2 | 3 | ## 核心功能 4 | 5 | 1. 动物发现上报 6 | - 用户上传照片、位置(地图定位)、简单描述(健康状况、毛色、性别、是否受伤,是否绝育等,出没得大概时间,给出一个大概的描述模版)。 7 | 表单内容:动物类型(猫 / 狗 / 其他)、大致年龄、外貌特征(毛色、是否受伤等)、发现地点(精确到街道,支持地图定位)、发现时间、现状描述(是否危险、是否亲人等)、上传现场照片 / 视频。 8 | - 在自己的定位附近可以直接找到需要救助的动物 9 | 2. 救助任务协作 10 | 可以发起救助任务,其他用户可报名加入,并标记任务进度(如:已送医、待领养,救助时会有提示,比如粮食够了,可以考虑其他选项,每次救助都会有一个专属的胸章)。 11 | 3. 领养申请与审核 12 | 平台可开放领养入口,并有基础的领养流程审核。 13 | 4. 救助记录档案 14 | 每只动物都有自己的档案,包括上报记录、救助人信息、领养人信息、健康状况等。 15 | 16 | ## 社区互动 17 | 18 | 1. 评论与点赞 19 | 其他用户可以留言或点赞支持救助信息。 20 | 互动功能:「我要救助」(报名参与)、「分享」(扩散信息)、「收藏」(持续关注)、「评论」(提供线索或建议,如 “这只猫我见过,经常在 XX 超市附近”)。 21 | 2. 救助故事分享 22 | 用户可发布救助成功的故事,增强参与感。 23 | 3. 志愿者榜单 24 | 按地区或全国排名救助次数、志愿时长。 25 | 26 | ## 救助对接与协作 27 | 28 | 1. 救助响应机制 29 | 普通用户点击「我要救助」后,可选择救助方式:亲自前往、提供物资(食物 / 药品)、联系机构、提供临时寄养。 30 | 2. 发布者可查看响应列表,选择合适的救助者对接(支持私信沟通细节)。 31 | 3. 若超过 48 小时无人响应,系统自动向合作的救助机构推送提醒。 32 | 4. 救助者可更新进度:「已到达现场」「已带往医院」「已安置」「已找到领养人」,并上传最新照片 / 视频。 33 | 34 | ## 信息列表与筛选 35 | 36 | 1. 首页展示最新发布的流浪动物信息,支持按「地区」「动物类型」「紧急程度」「发布时间」筛选。 37 | 2. 每条信息卡片显示关键信息:缩略图、地点、紧急程度、发布时间、当前状态(待救助 / 处理中)。 38 | 39 | ## 增强功能 40 | 41 | 1. 地图救助热点 42 | 展示当前地区的救助信息分布,方便就近支援。 43 | 2. 动物健康知识库 44 | 常见疾病、急救方法、临时喂养指南,在救助时选择阅读必要的提示文档,同时会有专业的救助文档和医疗知识提供参考等。 45 | 3. 公益合作 46 | 与宠物医院、NGO 合作,提供优惠医疗或物资支持。 47 | 48 | ## 用户系统 49 | 50 | 1. 注册 / 登录 51 | 支持手机号、微信快捷注册,区分「普通用户」和「救助机构 / 志愿者团队」两类账号(机构账号需审核资质)。 52 | 登录后可使用发布信息、参与救助、收藏关注等功能。 53 | 2. 个人中心 54 | 个人资料:头像、昵称、联系方式(可设置是否公开,保护隐私)、救助偏好(如擅长照顾猫 / 狗、可提供临时寄养等)。 55 | 3. 我的记录:自己发布的流浪动物信息(状态跟踪:待救助 / 已救助 / 已领养)。 56 | 4. 我的参与:报名参与的救助活动、捐赠记录、认领的救助任务。 57 | 5. 消息通知:有人响应自己发布的信息、救助进度更新、活动提醒等。 58 | 59 | ## 数据与公示(增强信任) 60 | 61 | 救助数据看板 62 | 首页展示累计救助动物数量、成功领养数量、参与用户数等数据,用可视化图表呈现(如月度救助趋势)。 63 | 捐赠公示 64 | 若开通捐款功能,公开资金使用明细(如用于医疗、粮食采购),提升透明度。 65 | -------------------------------------------------------------------------------- /packages/shared-frontend/resources/ideas.md: -------------------------------------------------------------------------------- 1 | 1. 不登录也可用的菜单(公共区) 2 | 3 | 这些是为了吸引新用户、传播公益理念,让他们先了解平台价值:1. 首页(Home) 4 | • 平台介绍、最新救助动态、数据统计(本月救助数量等)2. 发现流浪动物(Discover Animals) 5 | • 地图模式/列表模式,查看附近流浪动物信息(仅基础信息,不显示精确位置)3. 救助故事(Rescue Stories) 6 | • 成功案例、志愿者分享、温暖故事 4. 关于我们(About Us) 7 | • 平台使命、团队介绍、合作机构 5. 联系我们(Contact) 8 | • 提供联系表单、社交媒体链接、邮箱等 6. 登录 / 注册(Login / Sign Up) 9 | 10 | ⸻ 11 | 12 | 2. 必须登录后才能用的菜单(会员区) 13 | 14 | 这些功能涉及到数据提交、互动、个人信息,需要验证身份:1. 上报流浪动物(Report an Animal) 15 | • 上传照片、位置、健康状态描述 2. 参与救助(Join Rescue) 16 | • 报名成为某只动物的救助人 3. 我的救助记录(My Rescues) 17 | • 我发布、我参与的救助任务 4. 领养申请(Adoption Requests) 18 | • 填写领养表单、查看审核进度 5. 我的收藏(Favorites) 19 | • 收藏的动物档案或救助故事 6. 个人中心 / 设置(Profile / Settings) 20 | • 修改头像、昵称、联系方式等 21 | -------------------------------------------------------------------------------- /packages/shared-frontend/resources/主页.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/packages/shared-frontend/resources/主页.png -------------------------------------------------------------------------------- /packages/shared-frontend/resources/动物详情.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/packages/shared-frontend/resources/动物详情.png -------------------------------------------------------------------------------- /packages/shared-frontend/resources/爱心故事.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/packages/shared-frontend/resources/爱心故事.png -------------------------------------------------------------------------------- /packages/shared-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@pawhaven/tsconfig/web" 3 | } 4 | -------------------------------------------------------------------------------- /packages/shared-frontend/utils/SRTime.ts: -------------------------------------------------------------------------------- 1 | const createSRTime = () => { 2 | const start = new Date('1970-01-01T09:30:00'); 3 | const end = new Date('1970-01-01T10:10:00'); 4 | const randomTime = new Date( 5 | start.getTime() + Math.random() * (end.getTime() - start.getTime()), 6 | ); 7 | const hours = String(randomTime.getHours()).padStart(2, '0'); 8 | const minutes = String(randomTime.getMinutes()).padStart(2, '0'); 9 | return `${hours}:${minutes}`; 10 | }; 11 | 12 | export default createSRTime; 13 | -------------------------------------------------------------------------------- /packages/shared-frontend/utils/convertToColonFormat.ts: -------------------------------------------------------------------------------- 1 | function convertToColonFormat(input: string | number) { 2 | if ( 3 | typeof input !== 'string' || 4 | input.length !== 4 || 5 | Number.isNaN(Number(input)) 6 | ) { 7 | return 'Invalid input. Please provide a 4-digit string.'; 8 | } 9 | const firstTwo = input.slice(0, 2); 10 | const lastTwo = input.slice(2); 11 | return `${firstTwo}:${lastTwo}`; 12 | } 13 | export default convertToColonFormat; 14 | -------------------------------------------------------------------------------- /packages/shared-frontend/utils/getCountryCode.ts: -------------------------------------------------------------------------------- 1 | const getCountryCode = (locale: string) => { 2 | const parts = locale?.split(/[_-]/); 3 | return parts.length > 1 ? parts[1].toUpperCase() : 'CN'; 4 | }; 5 | export default getCountryCode; 6 | -------------------------------------------------------------------------------- /packages/shared-frontend/utils/getCurrencyCode.ts: -------------------------------------------------------------------------------- 1 | const getCurrencyCode = (countryCode: string | number) => { 2 | const currencyCodes = { 3 | 'en-US': '$', // United States 4 | 'zh-CN': '¥', // China 5 | 'ja-JP': '¥', // Japan 6 | 'en-GB': '£', // United Kingdom 7 | 'de-DE': '€', // Germany (Euro) 8 | 'fr-FR': '€', // France (Euro) 9 | 'it-IT': '€', // Italy (Euro) 10 | 'es-ES': '€', // Spain (Euro) 11 | 'ko-KR': '₩', // South Korea 12 | 'ru-RU': '₽', // Russia 13 | 'in-IN': '₹', // India 14 | 'pt-BR': 'R$', // Brazil 15 | 'ar-SA': '﷼', // Saudi Arabia 16 | 'tr-TR': '₺', // Turkey 17 | 'pl-PL': 'zł', // Poland 18 | 'th-TH': '฿', // Thailand 19 | 'vn-VN': '₫', // Vietnam 20 | 'id-ID': 'Rp', // Indonesia 21 | 'my-MY': 'RM', // Malaysia 22 | 'ph-PH': '₱', // Philippines 23 | 'ch-CH': 'Fr', // Switzerland 24 | 'se-SE': 'kr', // Sweden 25 | 'dk-DK': 'kr', // Denmark 26 | 'no-NO': 'kr', // Norway 27 | 'sg-SG': 'S$', // Singapore 28 | 'hk-HK': 'HK$', // Hong Kong 29 | 'tw-TW': 'NT$', // Taiwan 30 | 'au-AU': 'A$', // Australia 31 | 'nz-NZ': 'NZ$', // New Zealand 32 | 'ca-CA': 'C$', // Canada 33 | 'mx-MX': 'Mex$', // Mexico 34 | 'za-ZA': 'R', // South Africa 35 | 'il-IL': '₪', // Israel 36 | 'ae-AE': 'د.إ', // UAE 37 | 'eg-EG': 'E£', // Egypt 38 | // Euro zone countries 39 | 'nl-NL': '€', // Netherlands 40 | 'be-BE': '€', // Belgium 41 | 'pt-PT': '€', // Portugal 42 | 'gr-GR': '€', // Greece 43 | 'ie-IE': '€', // Ireland 44 | 'at-AT': '€', // Austria 45 | 'fi-FI': '€', // Finland 46 | 'sk-SK': '€', // Slovakia 47 | 'lv-LV': '€', // Latvia 48 | 'lt-LT': '€', // Lithuania 49 | 'ee-EE': '€', // Estonia 50 | }; 51 | 52 | return currencyCodes[countryCode as keyof typeof currencyCodes] ?? '$'; 53 | }; 54 | 55 | export default getCurrencyCode; 56 | -------------------------------------------------------------------------------- /packages/shared-frontend/utils/getIconByName.ts: -------------------------------------------------------------------------------- 1 | // The function is get icon from lucide-react 2 | // The parameter name MUST be from lucide-react 3 | // e.g. import { Camera } from 'lucide-react'; the name should be Camera 4 | import * as Icons from 'lucide-react'; 5 | import type { FC } from 'react'; 6 | 7 | const getIconByName = (name: string): FC | undefined => { 8 | return (Icons as unknown as Record>)[name]; 9 | }; 10 | 11 | export default getIconByName; 12 | -------------------------------------------------------------------------------- /packages/shared-frontend/utils/getLocale.ts: -------------------------------------------------------------------------------- 1 | import LocaleKeys from '../constants/localeKey'; 2 | 3 | import storageTool from './storage'; 4 | 5 | const getLocale = ( 6 | defaultLanguage: string = LocaleKeys['en-US'], 7 | supportLanguages: string[] = [], 8 | ) => { 9 | const currentBrowserLanguage = 10 | typeof window !== 'undefined' ? window.navigator.language : ''; 11 | const choosedLanguage = storageTool.get('I18NKEY'); 12 | if (choosedLanguage) { 13 | return choosedLanguage; 14 | } 15 | 16 | if ( 17 | currentBrowserLanguage && 18 | supportLanguages.includes(currentBrowserLanguage) 19 | ) { 20 | return currentBrowserLanguage; 21 | } 22 | 23 | return defaultLanguage; 24 | }; 25 | export default getLocale; 26 | -------------------------------------------------------------------------------- /packages/shared-frontend/utils/storage.ts: -------------------------------------------------------------------------------- 1 | const storageTool = { 2 | set(key: string, value: T): void { 3 | try { 4 | localStorage.setItem(key, JSON.stringify(value)); 5 | } catch (error) { 6 | console.error('Get errors in localStorage set value:', error); 7 | } 8 | }, 9 | 10 | get(key: string): T | string | null { 11 | try { 12 | const value = localStorage.getItem(key); 13 | if (value === null) return null; 14 | 15 | try { 16 | return JSON.parse(value) as T; 17 | } catch { 18 | return value; 19 | } 20 | } catch (error) { 21 | console.error('Get errors in localStorage get item:', error); 22 | return null; 23 | } 24 | }, 25 | 26 | getRaw(key: string): string | null { 27 | try { 28 | return localStorage.getItem(key); 29 | } catch (error) { 30 | console.error('Get errors in localStorage getRaw:', error); 31 | return null; 32 | } 33 | }, 34 | 35 | has(key: string): boolean { 36 | try { 37 | return localStorage.getItem(key) !== null; 38 | } catch (error) { 39 | console.error('Get errors in localStorage has check:', error); 40 | return false; 41 | } 42 | }, 43 | 44 | remove(key: string): void { 45 | try { 46 | localStorage.removeItem(key); 47 | } catch (error) { 48 | console.error('Get errors in localStorage remove item:', error); 49 | } 50 | }, 51 | 52 | clearAll(): void { 53 | try { 54 | localStorage.clear(); 55 | sessionStorage.clear(); 56 | } catch (error) { 57 | console.error('Get errors in localStorage delete all items:', error); 58 | } 59 | }, 60 | }; 61 | 62 | export default storageTool; 63 | -------------------------------------------------------------------------------- /packages/theme/MUI-theme.js: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material/styles'; 2 | 3 | import designTokens from './designTokens'; 4 | 5 | const MUITheme = createTheme({ 6 | palette: { 7 | mode: 'light', 8 | primary: { 9 | main: designTokens.colors.primary, 10 | light: designTokens.colors.primaryLight, 11 | dark: designTokens.colors.primaryDark, 12 | contrastText: '#ffffff', 13 | }, 14 | secondary: { 15 | main: designTokens.colors.secondary, 16 | light: designTokens.colors.secondaryLight, 17 | dark: designTokens.colors.secondaryDark, 18 | contrastText: '#ffffff', 19 | }, 20 | background: { 21 | default: designTokens.colors.background, 22 | paper: designTokens.colors.surface, 23 | }, 24 | text: { 25 | primary: designTokens.colors.textPrimary, 26 | secondary: designTokens.colors.textSecondary, 27 | }, 28 | divider: designTokens.colors.divider, 29 | error: { 30 | main: designTokens.colors.error, 31 | }, 32 | success: { 33 | main: designTokens.colors.success, 34 | }, 35 | warning: { 36 | main: designTokens.colors.warning, 37 | }, 38 | info: { 39 | main: designTokens.colors.accent, 40 | }, 41 | }, 42 | spacing: designTokens.spacing.base, 43 | }); 44 | 45 | export default MUITheme; 46 | -------------------------------------------------------------------------------- /packages/theme/README.MD: -------------------------------------------------------------------------------- 1 | # PawHaven Theme Package 2 | 3 | This package provides shared theme resources—including colors, fonts, spacing, and global styles—for the entire PawHaven monorepo. It centralizes design tokens and style definitions to ensure consistency across all projects. 4 | 5 | ## Usage 6 | 7 | Sub-packages within the monorepo can import theme values directly from this package without duplicating files. This workspace-aware setup simplifies maintenance and promotes reuse. 8 | 9 | ### Importing theme values 10 | 11 | You can import specific theme parts like colors or spacing as follows: 12 | 13 | ```js 14 | import { colors, spacing } from '@pawhaven/theme'; 15 | 16 | console.log(colors.primary); 17 | console.log(spacing.md); 18 | ``` 19 | 20 | Use these imports in your frontend apps or other packages to apply consistent styling throughout PawHaven projects. 21 | -------------------------------------------------------------------------------- /packages/theme/common.css: -------------------------------------------------------------------------------- 1 | /* Common style class */ 2 | @layer components { 3 | .link { 4 | @apply text-primary hover:underline; 5 | } 6 | 7 | .roundedButton { 8 | @apply px-6 py-3 font-semibold rounded-full transition cursor-pointer; 9 | } 10 | 11 | .baseFormContainer { 12 | @apply mb-[1.5rem]; 13 | } 14 | .formErrorMessage { 15 | @apply text-[.75rem] text-error mt-[.5rem]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/theme/designTokens.js: -------------------------------------------------------------------------------- 1 | const designTokens = { 2 | colors: { 3 | primary: '#f7823a', 4 | primaryLight: '#f89c61', 5 | primaryDark: '#e06716', 6 | 7 | secondary: '#4CAF50', 8 | secondaryLight: '#81C784', 9 | secondaryDark: '#388E3C', 10 | 11 | background: '#eee3d8', 12 | surface: '#FFFFFF', 13 | 14 | textPrimary: '#2F2F2F', 15 | textSecondary: '#6B6B6B', 16 | 17 | divider: '#E0DCD6', 18 | accent: '#3B82F6', 19 | 20 | neutral: '#9E9E9E', 21 | muted: '#F0EEEB', 22 | 23 | error: '#EF4444', 24 | success: '#4CAF50', 25 | warning: '#F59E0B', 26 | }, 27 | spacing: { 28 | base: '0.25rem', 29 | }, 30 | }; 31 | 32 | export default designTokens; 33 | -------------------------------------------------------------------------------- /packages/theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pawhaven/theme", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": "22.x", 7 | "pnpm": ">=10" 8 | }, 9 | "dependencies": { 10 | "@mui/material": "^7.3.4", 11 | "tailwindcss": "^4.1.14", 12 | "typescript": "^5.9.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@pawhaven/eslint-config/web'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/ui/AvatarMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from 'react'; 2 | 3 | type Props = { 4 | userInfo: Record; 5 | }; 6 | 7 | const AvatarMenu: FC = () => { 8 | return
touxiang
; 9 | }; 10 | export default AvatarMenu; 11 | -------------------------------------------------------------------------------- /packages/ui/Form/FormCheckBox/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Checkbox, 3 | FormControlLabel, 4 | FormHelperText, 5 | type CheckboxProps, 6 | } from '@mui/material'; 7 | import clsx from 'clsx'; 8 | import type { FC } from 'react'; 9 | import { Controller, useFormContext } from 'react-hook-form'; 10 | 11 | import type { BaseFormType } from '../formBase.type'; 12 | 13 | const FormCheckbox: FC = ({ 14 | name, 15 | label, 16 | defaultValue = false, 17 | ...props 18 | }) => { 19 | const { control } = useFormContext(); 20 | 21 | return ( 22 | ( 27 |
28 | field.onChange(e.target.checked)} 35 | /> 36 | } 37 | label={label} 38 | /> 39 | {error && {error.message}} 40 |
41 | )} 42 | /> 43 | ); 44 | }; 45 | 46 | export default FormCheckbox; 47 | -------------------------------------------------------------------------------- /packages/ui/Form/FormDateRanger/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormControl, FormHelperText } from '@mui/material'; 2 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 3 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; 4 | import type { SingleInputDateRangeFieldProps } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; 5 | import { SingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; 6 | import clsx from 'clsx'; 7 | import type { FC } from 'react'; 8 | import { Controller, useFormContext } from 'react-hook-form'; 9 | 10 | import type { BaseFormType } from '../formBase.type'; 11 | 12 | type FormSingleDateRangerProps = BaseFormType & 13 | Omit & { 14 | fullWidth?: boolean; 15 | }; 16 | 17 | const FormDateRanger: FC = ({ 18 | name, 19 | label, 20 | defaultValue = [null, null], 21 | fullWidth = true, 22 | ...props 23 | }) => { 24 | const { control } = useFormContext(); 25 | 26 | return ( 27 | ( 32 |
33 |
{label}
34 | 35 | 36 | 47 | 48 | {error && {error.message}} 49 | 50 |
51 | )} 52 | /> 53 | ); 54 | }; 55 | 56 | export default FormDateRanger; 57 | -------------------------------------------------------------------------------- /packages/ui/Form/FormInput/index.tsx: -------------------------------------------------------------------------------- 1 | import { TextField, type TextFieldProps } from '@mui/material'; 2 | import clsx from 'clsx'; 3 | import React from 'react'; 4 | import { Controller, useFormContext } from 'react-hook-form'; 5 | 6 | import type { BaseFormType, BaseTextFieldType } from '../formBase.type'; 7 | 8 | const FormInput: React.FC< 9 | BaseFormType & TextFieldProps & BaseTextFieldType 10 | > = ({ 11 | name, 12 | label, 13 | defaultValue = '', 14 | type = 'text', 15 | fullWidth = true, 16 | ...props 17 | }) => { 18 | const { control } = useFormContext(); 19 | 20 | return ( 21 | ( 26 |
27 |
{label}
28 | 38 |
39 | )} 40 | /> 41 | ); 42 | }; 43 | 44 | export default FormInput; 45 | -------------------------------------------------------------------------------- /packages/ui/Form/FormRadio/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import type { FC } from 'react'; 3 | import { useFormContext } from 'react-hook-form'; 4 | 5 | import type { BaseFormType } from '../formBase.type'; 6 | 7 | interface Option { 8 | value: string; 9 | label: string; 10 | } 11 | 12 | interface FormRadioProps { 13 | options: Option[]; 14 | } 15 | 16 | const FormRadio: FC = ({ 17 | name, 18 | label, 19 | options, 20 | required, 21 | }) => { 22 | const { 23 | register, 24 | formState: { errors }, 25 | } = useFormContext(); 26 | 27 | return ( 28 |
29 |

{label}

30 | {options.map((option) => ( 31 | 48 | ))} 49 | {errors[name] && ( 50 | 51 | {errors[name]?.message?.toString()} 52 | 53 | )} 54 |
55 | ); 56 | }; 57 | 58 | export default FormRadio; 59 | -------------------------------------------------------------------------------- /packages/ui/Form/FormSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormHelperText, 4 | MenuItem, 5 | Select, 6 | type SelectProps, 7 | } from '@mui/material'; 8 | import clsx from 'clsx'; 9 | import React from 'react'; 10 | import { Controller, useFormContext } from 'react-hook-form'; 11 | 12 | import type { BaseFormType, BaseSelectType } from '../formBase.type'; 13 | 14 | const FormSelect: React.FC = ({ 15 | name, 16 | label, 17 | options, 18 | defaultValue = '', 19 | fullWidth = true, 20 | ...props 21 | }) => { 22 | const { control } = useFormContext(); 23 | return ( 24 | ( 29 | 34 |
{label}
35 | 47 | {error?.message} 48 |
49 | )} 50 | /> 51 | ); 52 | }; 53 | 54 | export default FormSelect; 55 | -------------------------------------------------------------------------------- /packages/ui/Form/FormTextArea/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormHelperText, 4 | TextareaAutosize, 5 | type TextareaAutosizeProps, 6 | } from '@mui/material'; 7 | import clsx from 'clsx'; 8 | import React from 'react'; 9 | import { Controller, useFormContext } from 'react-hook-form'; 10 | 11 | import type { BaseFormType } from '../formBase.type'; 12 | 13 | const FormTextArea: React.FC = ({ 14 | name, 15 | label, 16 | 17 | defaultValue = '', 18 | fullWidth = true, 19 | ...props 20 | }) => { 21 | const { control } = useFormContext(); 22 | return ( 23 | ( 31 | 36 |
{label}
37 | 45 | {error?.message} 46 |
47 | )} 48 | /> 49 | ); 50 | }; 51 | 52 | export default FormTextArea; 53 | -------------------------------------------------------------------------------- /packages/ui/Form/formBase.type.ts: -------------------------------------------------------------------------------- 1 | export interface BaseFormType { 2 | name: string; 3 | label?: string; 4 | defaultValue?: string | number; 5 | type?: string; 6 | fullWidth?: boolean; 7 | className?: string | string[]; 8 | required?: boolean; 9 | } 10 | 11 | export interface BaseSelectType { 12 | options: Array<{ label: string; value: string | number }>; 13 | } 14 | 15 | export interface BaseTextFieldType { 16 | prefix?: string; 17 | defaultPrefix?: boolean; 18 | suffix?: string; 19 | defaultSuffix?: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/IconComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Icons from 'lucide-react'; 2 | import type { LucideProps } from 'lucide-react'; 3 | import React from 'react'; 4 | 5 | export type LucideIconName = keyof typeof Icons; 6 | 7 | type IconProps = { 8 | name?: LucideIconName | string; 9 | className?: string; 10 | size?: number; 11 | strokeWidth?: number; 12 | children?: React.ReactNode; 13 | }; 14 | 15 | const IconComponent: React.FC = ({ 16 | name, 17 | className = '', 18 | size = 24, 19 | strokeWidth, 20 | children, 21 | }) => { 22 | const isValidLucideIcon = typeof name === 'string' && name in Icons; 23 | 24 | if (isValidLucideIcon) { 25 | const LucideIcon = Icons[name as LucideIconName] as React.FC; 26 | return ( 27 | 28 | ); 29 | } 30 | 31 | return ( 32 |
33 | {children} 34 |
35 | ); 36 | }; 37 | 38 | export default IconComponent; 39 | -------------------------------------------------------------------------------- /packages/ui/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import { CircularProgress, Box } from '@mui/material'; 2 | 3 | const Loading = () => ( 4 | 18 | 19 | 20 | ); 21 | 22 | export default Loading; 23 | -------------------------------------------------------------------------------- /packages/ui/Phase/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react'; 2 | 3 | interface SectionType { 4 | label: string | ReactNode; 5 | value: string | ReactNode; 6 | } 7 | 8 | interface Props { 9 | title?: string | ReactNode; 10 | sections: SectionType[]; 11 | } 12 | 13 | const Phase: FC = (props) => { 14 | const { title, sections } = props; 15 | return ( 16 |
17 | {title &&

{title}

} 18 | {sections?.map((section, idx) => ( 19 |

24 | {section.label} 25 | {section.value} 26 |

27 | ))} 28 |
29 | ); 30 | }; 31 | export default Phase; 32 | -------------------------------------------------------------------------------- /packages/ui/README.MD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoda-zhang/PawHaven/04fb06c4c3569b8d2dec2cebccadc44ed563ac64/packages/ui/README.MD -------------------------------------------------------------------------------- /packages/ui/SuspenseWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { Suspense } from 'react'; 3 | 4 | import Loading from '../Loading'; 5 | 6 | const SuspenseWrapper = ({ children }: { children: ReactNode }) => { 7 | return }>{children}; 8 | }; 9 | 10 | export default SuspenseWrapper; 11 | -------------------------------------------------------------------------------- /packages/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Loading } from './Loading'; 2 | export { default as FormInput } from './Form/FormInput'; 3 | export { default as FormCheckbox } from './Form/FormCheckBox'; 4 | export { default as FormDateRanger } from './Form/FormDateRanger'; 5 | export { default as FormTextArea } from './Form/FormTextArea'; 6 | export { default as FormRadio } from './Form/FormRadio'; 7 | export { default as FormSelect } from './Form/FormSelect'; 8 | export { default as SuspenseWrapper } from './SuspenseWrapper'; 9 | export { default as Phase } from './Phase'; 10 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pawhaven/ui", 3 | "version": "3.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": "22.x", 7 | "pnpm": ">=10" 8 | }, 9 | "dependencies": { 10 | "@emotion/react": "^11.14.0", 11 | "@emotion/styled": "^11.14.1", 12 | "@mui/material": "^7.3.4", 13 | "@mui/x-date-pickers": "^8.14.0", 14 | "@mui/x-date-pickers-pro": "^8.14.0", 15 | "@pawhaven/eslint-config": "workspace:*", 16 | "@pawhaven/tsconfig": "workspace:*", 17 | "@types/node": "^24.7.1", 18 | "@types/react": "^19.2.2", 19 | "@types/react-dom": "^19.2.1", 20 | "clsx": "^2.1.1", 21 | "dayjs": "^1.11.18", 22 | "lucide-react": "^0.545.0", 23 | "or": "^0.2.0", 24 | "react": "^19.2.0", 25 | "react-hook-form": "^7.64.0", 26 | "react-hot-toast": "^2.6.0", 27 | "react-loading-skeleton": "^3.5.0", 28 | "tailwindcss": "^4.1.14", 29 | "typescript": "^5.9.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@pawhaven/tsconfig/web" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | # One * matches a single directory level, while ** matches any nested directory levels. 2 | packages: 3 | - apps/** 4 | - packages/** 5 | - libs/** 6 | 7 | onlyBuiltDependencies: 8 | - '@mui/x-telemetry' 9 | - '@nestjs/core' 10 | - '@scarf/scarf' 11 | - '@tailwindcss/oxide' 12 | - bcrypt 13 | - esbuild 14 | - unrs-resolver 15 | --------------------------------------------------------------------------------