├── .all-contributorsrc ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cloudbaserc-fx.json ├── cloudbaserc-wx.json ├── cloudbaserc.json ├── community └── posts │ └── README.md ├── docs ├── assets │ ├── banner.jpg │ ├── overview.png │ └── schema.png └── examples │ ├── cloudbase.png │ ├── featblog.png │ ├── hi-avatar.jpg │ ├── hip-pop.jpeg │ ├── livewallpaper.png │ ├── realtime-earthquake.jpeg │ ├── wedding-app.jpeg │ └── yami.png ├── lerna.json ├── package.json ├── packages ├── admin │ ├── .eslintignore │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── config.ts │ │ ├── defaultSettings.ts │ │ ├── platform.ts │ │ ├── proxy.ts │ │ └── routes.ts │ ├── package.json │ ├── public │ │ ├── cmsSmsTemplate.csv │ │ ├── config.example.js │ │ ├── home_bg.png │ │ ├── icon-wx.svg │ │ ├── icon.png │ │ ├── icon.svg │ │ ├── icons │ │ │ ├── icon-128x128.png │ │ │ ├── icon-512x512.png │ │ │ └── logo-192x192.png │ │ ├── img │ │ │ ├── empty.svg │ │ │ └── logo.png │ │ └── raven.min.js │ ├── src │ │ ├── access.ts │ │ ├── app.tsx │ │ ├── assets │ │ │ └── empty.svg │ │ ├── common │ │ │ ├── default.ts │ │ │ ├── field.tsx │ │ │ ├── hooks.tsx │ │ │ └── index.ts │ │ ├── components │ │ │ ├── AvatarDropdown │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── BackNavigator │ │ │ │ └── index.tsx │ │ │ ├── ChannelSelector │ │ │ │ └── index.tsx │ │ │ ├── Charts │ │ │ │ ├── Funnel.tsx │ │ │ │ ├── Line.tsx │ │ │ │ ├── Pie.tsx │ │ │ │ └── index.ts │ │ │ ├── ErrorBoundary │ │ │ │ └── index.tsx │ │ │ ├── Fields │ │ │ │ ├── Connect.tsx │ │ │ │ ├── Date.tsx │ │ │ │ ├── FieldContentEditor.tsx │ │ │ │ ├── FieldContentRender.tsx │ │ │ │ ├── File.tsx │ │ │ │ ├── FileAction.tsx │ │ │ │ ├── FileAndImageEditor.tsx │ │ │ │ ├── Image.tsx │ │ │ │ ├── Markdown.tsx │ │ │ │ ├── Media.tsx │ │ │ │ ├── Object.tsx │ │ │ │ ├── RichText.tsx │ │ │ │ ├── Switch.tsx │ │ │ │ └── index.ts │ │ │ ├── Footer │ │ │ │ └── index.tsx │ │ │ ├── HeaderTitle │ │ │ │ └── index.tsx │ │ │ ├── Loading │ │ │ │ ├── ContentLoading.tsx │ │ │ │ ├── PageLoading.tsx │ │ │ │ └── index.ts │ │ │ ├── Modal │ │ │ │ ├── ModalForm.tsx │ │ │ │ └── index.ts │ │ │ ├── QrCode │ │ │ │ └── index.tsx │ │ │ ├── RightContent │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── SecurityWrapper │ │ │ │ └── index.tsx │ │ │ ├── Typography │ │ │ │ ├── Text.tsx │ │ │ │ └── index.ts │ │ │ └── Upload │ │ │ │ └── index.tsx │ │ ├── constants.ts │ │ ├── global.less │ │ ├── global.tsx │ │ ├── layout │ │ │ └── index.tsx │ │ ├── manifest.json │ │ ├── models │ │ │ ├── content.ts │ │ │ ├── global.ts │ │ │ ├── index.ts │ │ │ ├── microApp.ts │ │ │ ├── role.ts │ │ │ ├── schema.ts │ │ │ └── webhook.ts │ │ ├── pages │ │ │ ├── 404.tsx │ │ │ ├── document.ejs │ │ │ ├── home │ │ │ │ ├── HomePageContainer.tsx │ │ │ │ ├── Notice.tsx │ │ │ │ ├── ProjectCardView.tsx │ │ │ │ ├── ProjectListView.tsx │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── login │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── project │ │ │ │ ├── content │ │ │ │ │ ├── ContentEditor.tsx │ │ │ │ │ ├── ContentTable.tsx │ │ │ │ │ ├── DataExport.tsx │ │ │ │ │ ├── DataImport.tsx │ │ │ │ │ ├── SearchForm.tsx │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── tool.ts │ │ │ │ ├── microapp │ │ │ │ │ └── index.tsx │ │ │ │ ├── migrate │ │ │ │ │ └── index.tsx │ │ │ │ ├── operation │ │ │ │ │ ├── Activity │ │ │ │ │ │ ├── ActivityEditor.tsx │ │ │ │ │ │ ├── ActivityTable.tsx │ │ │ │ │ │ ├── Channel.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── schema.ts │ │ │ │ │ ├── Analytics │ │ │ │ │ │ ├── DataSource.tsx │ │ │ │ │ │ ├── OverviewRow.tsx │ │ │ │ │ │ ├── RealTimeView.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Message │ │ │ │ │ │ ├── Connect.tsx │ │ │ │ │ │ ├── TaskCreator.tsx │ │ │ │ │ │ ├── TaskResult.tsx │ │ │ │ │ │ ├── columns.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ └── util.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── overview │ │ │ │ │ └── index.tsx │ │ │ │ ├── schema │ │ │ │ │ ├── SchemaEditor.tsx │ │ │ │ │ ├── SchemaFieldEditor │ │ │ │ │ │ ├── Field.tsx │ │ │ │ │ │ ├── SchemaFieldDelete.tsx │ │ │ │ │ │ ├── SchemaFieldEdit.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── SchemaFieldPicker.tsx │ │ │ │ │ ├── SchemaMenuList.tsx │ │ │ │ │ ├── SchemaShare.tsx │ │ │ │ │ ├── SchmeaContent │ │ │ │ │ │ ├── FieldListRender.tsx │ │ │ │ │ │ ├── SchemaFieldList.tsx │ │ │ │ │ │ ├── SchemaToolbar.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── setting │ │ │ │ │ ├── ApiAccess.tsx │ │ │ │ │ ├── ProjectInfo.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── webhook │ │ │ │ │ ├── WebhookExecLog.tsx │ │ │ │ │ ├── WebhookForm.tsx │ │ │ │ │ ├── columns.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── redirect.tsx │ │ │ └── system-setting │ │ │ │ ├── ApiAccess │ │ │ │ └── index.tsx │ │ │ │ ├── CustomMenu │ │ │ │ └── index.tsx │ │ │ │ ├── MicroApp │ │ │ │ ├── MicroAppEditor.tsx │ │ │ │ └── index.tsx │ │ │ │ ├── RoleManagement │ │ │ │ ├── RoleEditor │ │ │ │ │ ├── RoleInfo.tsx │ │ │ │ │ ├── RolePermission.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ │ ├── SettingContainer.tsx │ │ │ │ ├── UserManagement │ │ │ │ ├── CreateUserWithQrCode.tsx │ │ │ │ ├── CreateUserWithUsername.tsx │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ ├── service-worker.js │ │ ├── services │ │ │ ├── apis.ts │ │ │ ├── content.ts │ │ │ ├── global.ts │ │ │ ├── login.ts │ │ │ ├── notice.ts │ │ │ ├── operation.ts │ │ │ ├── project.ts │ │ │ ├── role.ts │ │ │ ├── schema.ts │ │ │ ├── user.ts │ │ │ └── webhook.ts │ │ └── utils │ │ │ ├── cloudbase.ts │ │ │ ├── common.ts │ │ │ ├── config.ts │ │ │ ├── date.ts │ │ │ ├── doc.ts │ │ │ ├── field.ts │ │ │ ├── file.ts │ │ │ ├── index.ts │ │ │ ├── qrcode.ts │ │ │ ├── route.ts │ │ │ ├── templateCompile.ts │ │ │ └── text.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── typings │ │ ├── field.d.ts │ │ ├── global.d.ts │ │ ├── store.d.ts │ │ ├── typings.d.ts │ │ ├── user.d.ts │ │ └── webhook.d.ts │ └── yarn.lock ├── cms-api │ ├── .env │ ├── .env.example │ ├── .eslintignore │ ├── .gitignore │ ├── app.js │ ├── dockerfile │ ├── index.js │ ├── nest-cli.json │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── api.controller.ts │ │ │ ├── api.module.ts │ │ │ └── api.service.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── common │ │ │ ├── error.ts │ │ │ └── index.ts │ │ ├── constants.ts │ │ ├── exceptions.filter.ts │ │ ├── global.module.ts │ │ ├── guards │ │ │ ├── action.guard.ts │ │ │ ├── auth.guard.ts │ │ │ └── index.ts │ │ ├── interceptors │ │ │ ├── timecost.interceptor.ts │ │ │ └── timeout.interceptor.ts │ │ ├── main.ts │ │ ├── middlewares │ │ │ └── converter.middleware.ts │ │ ├── services │ │ │ ├── cache.service.ts │ │ │ ├── cloudbase.service.ts │ │ │ └── index.ts │ │ └── utils │ │ │ ├── cloudbase.ts │ │ │ ├── date.ts │ │ │ ├── index.ts │ │ │ ├── output.ts │ │ │ └── tools.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── typings │ │ ├── global.d.ts │ │ ├── index.d.ts │ │ └── schema.d.ts │ └── yarn.lock ├── cms-fx-openapi │ ├── .env │ ├── .env.example │ ├── .eslintignore │ ├── .gitignore │ ├── app.js │ ├── dockerfile │ ├── index.js │ ├── nest-cli.json │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── api.controller.ts │ │ │ ├── api.module.ts │ │ │ └── api.service.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── common │ │ │ ├── error.ts │ │ │ └── index.ts │ │ ├── constants.ts │ │ ├── exceptions.filter.ts │ │ ├── global.module.ts │ │ ├── guards │ │ │ ├── auth.guard.ts │ │ │ └── index.ts │ │ ├── interceptors │ │ │ ├── timecost.interceptor.ts │ │ │ └── timeout.interceptor.ts │ │ ├── main.ts │ │ ├── middlewares │ │ │ └── converter.middleware.ts │ │ ├── services │ │ │ ├── cloudbase.service.ts │ │ │ └── index.ts │ │ └── utils │ │ │ ├── cloudbase.ts │ │ │ ├── date.ts │ │ │ ├── index.ts │ │ │ └── tools.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── typings │ │ └── index.d.ts │ └── yarn.lock ├── cms-init │ ├── .gitignore │ ├── index.js │ ├── package.json │ ├── scripts │ │ ├── _schema.js │ │ ├── deploy.js │ │ ├── migrate.js │ │ └── users.js │ └── yarn.lock ├── cms-openapi │ ├── .env │ ├── .env.example │ ├── .eslintignore │ ├── .gitignore │ ├── app.js │ ├── dockerfile │ ├── index.js │ ├── nest-cli.json │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── api.controller.ts │ │ │ ├── api.module.ts │ │ │ └── api.service.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── common │ │ │ ├── error.ts │ │ │ └── index.ts │ │ ├── constants.ts │ │ ├── exceptions.filter.ts │ │ ├── global.module.ts │ │ ├── guards │ │ │ ├── auth.guard.ts │ │ │ ├── index.ts │ │ │ ├── permission.guard.ts │ │ │ └── role.guard.ts │ │ ├── interceptors │ │ │ ├── timecost.interceptor.ts │ │ │ └── timeout.interceptor.ts │ │ ├── main.ts │ │ ├── middlewares │ │ │ └── converter.middleware.ts │ │ ├── services │ │ │ ├── cache.service.ts │ │ │ ├── cloudbase.service.ts │ │ │ └── index.ts │ │ └── utils │ │ │ ├── cloudbase.ts │ │ │ ├── date.ts │ │ │ ├── index.ts │ │ │ └── tools.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── typings │ │ ├── global.d.ts │ │ ├── index.d.ts │ │ └── schema.d.ts │ └── yarn.lock ├── cms-sms-page │ ├── .browserslistrc │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── weui.min.css │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ ├── DesktopWeb.vue │ │ │ ├── Loading.vue │ │ │ ├── PublicWeb.vue │ │ │ ├── WeDialog.vue │ │ │ └── WechatWeb.vue │ │ └── main.js │ ├── vue.config.js │ └── yarn.lock ├── cms-sms │ ├── app.js │ ├── index.js │ ├── package.json │ ├── report.js │ ├── url.js │ └── yarn.lock └── service │ ├── .dockerignore │ ├── .env.example │ ├── .eslintignore │ ├── .gitignore │ ├── Dockerfile │ ├── app.js │ ├── config.json │ ├── index.js │ ├── nest-cli.json │ ├── package.json │ ├── src │ ├── app.module.ts │ ├── common │ │ ├── decorators.ts │ │ ├── error.ts │ │ ├── field.ts │ │ └── index.ts │ ├── config │ │ └── index.ts │ ├── constants.ts │ ├── decorators │ │ ├── index.ts │ │ └── permission.decorator.ts │ ├── exceptions.filter.ts │ ├── global.module.ts │ ├── guards │ │ ├── auth.guard.ts │ │ ├── index.ts │ │ ├── permission.guard.ts │ │ └── role.guard.ts │ ├── interceptors │ │ ├── context.interceptor.ts │ │ ├── timecost.interceptor.ts │ │ └── timeout.interceptor.ts │ ├── main.ts │ ├── middlewares │ │ └── converter.middleware.ts │ ├── modules │ │ ├── apis │ │ │ ├── apis.controller.ts │ │ │ ├── apis.module.ts │ │ │ ├── apis.service.ts │ │ │ ├── auth.service.ts │ │ │ └── util.service.ts │ │ ├── file │ │ │ ├── file.controller.ts │ │ │ ├── file.module.ts │ │ │ └── file.service.ts │ │ ├── projects │ │ │ ├── contents │ │ │ │ ├── contents.controller.ts │ │ │ │ └── contents.service.ts │ │ │ ├── migrate │ │ │ │ └── migrate.controller.ts │ │ │ ├── operation │ │ │ │ ├── operation.controller.ts │ │ │ │ ├── operation.service.ts │ │ │ │ └── template │ │ │ │ │ ├── .gitkeep │ │ │ │ │ └── raw.html │ │ │ ├── projects.controller.ts │ │ │ ├── projects.module.ts │ │ │ ├── projects.service.ts │ │ │ ├── schemas │ │ │ │ ├── schema.controller.ts │ │ │ │ ├── schema.pipe.ts │ │ │ │ ├── schema.service.ts │ │ │ │ └── types.ts │ │ │ └── webhooks │ │ │ │ ├── type.ts │ │ │ │ ├── webhooks.controller.ts │ │ │ │ └── webhooks.service.ts │ │ ├── role │ │ │ ├── role.controller.ts │ │ │ ├── role.dto.ts │ │ │ └── role.module.ts │ │ ├── setting │ │ │ ├── setting.controller.ts │ │ │ ├── setting.module.ts │ │ │ └── setting.service.ts │ │ └── user │ │ │ ├── user.controller.ts │ │ │ ├── user.dto.ts │ │ │ ├── user.module.ts │ │ │ └── user.service.ts │ ├── services │ │ ├── cache.service.ts │ │ ├── cloudbase.service.ts │ │ ├── index.ts │ │ └── schema-cache.service.ts │ └── utils │ │ ├── cache.ts │ │ ├── cloudbase.ts │ │ ├── cos.ts │ │ ├── date.ts │ │ ├── db.ts │ │ ├── env.ts │ │ ├── field.ts │ │ ├── index.ts │ │ ├── lowcode.ts │ │ ├── permission.ts │ │ └── tools.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── typings │ ├── file.d.ts │ ├── global.d.ts │ ├── index.d.ts │ └── schema.d.ts │ └── yarn.lock ├── sam-preview.yml ├── sam-ui.yml ├── sam-wx.yml ├── sam.yml ├── scripts ├── setup.sh └── zip.sh ├── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "chhpt", 10 | "name": "chhpt", 11 | "avatar_url": "https://avatars2.githubusercontent.com/u/19288423?v=4", 12 | "profile": "https://github.com/chhpt", 13 | "contributions": [ 14 | "infra", 15 | "code", 16 | "doc" 17 | ] 18 | }, 19 | { 20 | "login": "binggg", 21 | "name": "Booker Zhao", 22 | "avatar_url": "https://avatars2.githubusercontent.com/u/7686861?v=4", 23 | "profile": "https://github.com/binggg", 24 | "contributions": [ 25 | "code" 26 | ] 27 | }, 28 | { 29 | "login": "fantasticsoul", 30 | "name": "幻魂", 31 | "avatar_url": "https://avatars0.githubusercontent.com/u/7334950?v=4", 32 | "profile": "https://github.com/fantasticsoul", 33 | "contributions": [ 34 | "code" 35 | ] 36 | }, 37 | { 38 | "login": "geeeeeeeeeek", 39 | "name": "June", 40 | "avatar_url": "https://avatars0.githubusercontent.com/u/9697715?v=4", 41 | "profile": "https://github.com/geeeeeeeeeek", 42 | "contributions": [ 43 | "code" 44 | ] 45 | } 46 | ], 47 | "contributorsPerLine": 7, 48 | "projectName": "cloudbase-extension-cms", 49 | "projectOwner": "TencentCloudBase", 50 | "repoType": "github", 51 | "repoHost": "https://github.com", 52 | "skipCi": true 53 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = false -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 您的云开发环境 Id 2 | ENV_ID= 3 | # 管理员账户名,账号名长度需要大于 4 位,支持字母和数字 4 | administratorName=admin 5 | # 管理员账号密码,8~32位,密码支持字母、数字、字符、不能由纯字母或存数字组成 6 | administratorPassword= 7 | # CMS 控制台路径,如 /tcb-cms/,建议使用根路径 / 8 | deployPath= 9 | # 云接入自定义域名(选填),如 tencent.com 10 | accessDomain= 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | packages/cms-sms-page 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['alloy', 'alloy/react', 'alloy/typescript'], 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | globals: { 8 | WX_MP: true, 9 | SERVER_MODE: true, 10 | REACT_APP_ENV: true, 11 | }, 12 | rules: { 13 | complexity: 'off', 14 | 'max-params': ['error', 4], 15 | 'prefer-promise-reject-errors': 'off', 16 | '@typescript-eslint/explicit-member-accessibility': 'off', 17 | '@typescript-eslint/no-parameter-properties': 'off', 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /build 3 | node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | .eslintcache 37 | 38 | # local env files 39 | .env.local 40 | .env.*.local 41 | config/config.json 42 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | *.ejs 3 | *.lock 4 | assets 5 | .umi 6 | 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 一行最多 100 字符 3 | printWidth: 100, 4 | // 使用 2 个空格缩进 5 | tabWidth: 2, 6 | // 不使用缩进符,而使用空格 7 | useTabs: false, 8 | // 行尾需要有分号 9 | semi: false, 10 | // 使用单引号 11 | singleQuote: true, 12 | // 对象的 key 仅在必要时用引号 13 | quoteProps: 'as-needed', 14 | // 末尾不需要逗号 15 | trailingComma: 'es5', 16 | // 大括号内的首尾需要空格 17 | bracketSpacing: true, 18 | // 箭头函数,只有一个参数的时候,也需要括号 19 | arrowParens: 'always', 20 | // 每个文件格式化的范围是文件的全部内容 21 | rangeStart: 0, 22 | rangeEnd: Infinity, 23 | // 不需要写文件开头的 @prettier 24 | requirePragma: false, 25 | // 不需要自动在文件开头插入 @prettier 26 | insertPragma: false, 27 | // 使用默认的折行标准 28 | proseWrap: 'preserve', 29 | // 换行符使用 lf 30 | endOfLine: 'lf', 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "启动 Service 调试", 11 | "runtimeExecutable": "npm", 12 | "runtimeArgs": ["run", "dev"], 13 | "skipFiles": ["/**"], 14 | "cwd": "${workspaceFolder}/packages/service" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Droppable", 4 | "EJSON", 5 | "KUBERNETES", 6 | "braft", 7 | "bson", 8 | "envid", 9 | "metas", 10 | "pino", 11 | "plyr", 12 | "qrcode", 13 | "secretid", 14 | "seqid", 15 | "sider", 16 | "tinymce", 17 | "umijs", 18 | "unparse", 19 | "vditor" 20 | ], 21 | "search.exclude": { 22 | "**/dist": true, 23 | "**/node_modules": true, 24 | "**/bower_components": true, 25 | "**/*.code-search": true, 26 | "**/*.lock": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /community/posts/README.md: -------------------------------------------------------------------------------- 1 | # 贡献一篇文章 2 | 3 | 感谢您有兴趣成为 CloudBase CMS 社区贡献者! 4 | 5 | 欢迎贡献您撰写的有关 CloudBase CMS 体验的外部内容(博客帖子,教程,在线课程,视频)。 6 | 7 | 您可能已经在外部技术社区或您自己的个人博客上写了这篇文章。将您的文章添加到[云开发社区官网](https://cloudbase.net/community.html),可以让其他开发阅读和通过标签来检索您的文章。 8 | 9 | ## 步骤 10 | 11 | 1. Fork 这个 Git 仓库 12 | 2. 在仓库中为您的文章添加一个新文件夹 `/community/posts/YYYY-MM-DD-Desc`,例如 `/community/posts/2020-08-12-如何用云开发快速搭建实时 TodoList 应用` 13 | 3. 在创建好的文件夹目录中添加一个 `index.md`,文件格式可以参考如下模板 14 | 15 | ```markdown 16 | --- 17 | title: 如何用云开发快速搭建实时 TodoList 应用 18 | description: '本文基于 web 端实时更新的 TodoList 案例,详细介绍了云开发数据库实时推送能力的使用。整个案例使用 CloudBase Framework 前后端一体化部署工具,一站式完成项目的创建、开发以及部署。' 19 | banner: './banner.jpg' 20 | # github 用户名 21 | authorIds: 22 | - shryzhang 23 | href: https://juejin.im/post/6859930183030292488 24 | platforms: 25 | - Web 26 | tags: 27 | - 实时推送 28 | - 数据库 29 | - 静态网站托管 30 | --- 31 | 32 | 以下是正文内容的 Markdown 33 | ``` 34 | 35 | 4. 如果文章有 banner 图片,在项目目录下创建一个 `banner.jpg` 36 | 37 | 5. 提交 Pull Request 38 | 39 | 6. 在通过 Pull Request 之后,我们会将您的帖子添加到我们的社区文章列表,并将您加入[我们的贡献者名单](https://github.com/TencentCloudBase/cloudbase-extensions-cms) 40 | 41 | 7. 贡献成功 🎉 42 | -------------------------------------------------------------------------------- /docs/assets/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/assets/banner.jpg -------------------------------------------------------------------------------- /docs/assets/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/assets/overview.png -------------------------------------------------------------------------------- /docs/assets/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/assets/schema.png -------------------------------------------------------------------------------- /docs/examples/cloudbase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/examples/cloudbase.png -------------------------------------------------------------------------------- /docs/examples/featblog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/examples/featblog.png -------------------------------------------------------------------------------- /docs/examples/hi-avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/examples/hi-avatar.jpg -------------------------------------------------------------------------------- /docs/examples/hip-pop.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/examples/hip-pop.jpeg -------------------------------------------------------------------------------- /docs/examples/livewallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/examples/livewallpaper.png -------------------------------------------------------------------------------- /docs/examples/realtime-earthquake.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/examples/realtime-earthquake.jpeg -------------------------------------------------------------------------------- /docs/examples/wedding-app.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/examples/wedding-app.jpeg -------------------------------------------------------------------------------- /docs/examples/yami.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/docs/examples/yami.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "npmClient": "yarn", 4 | "version": "2.13.9" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudbase-cms", 3 | "version": "2.13.19", 4 | "private": true, 5 | "scripts": { 6 | "dev": "lerna run dev --stream", 7 | "build": "lerna run build --stream", 8 | "build:wx": "cross-env WX_MP=true lerna run build --stream", 9 | "build:server": "cross-env SERVER_MODE=true lerna run build --stream", 10 | "setup": "lerna bootstrap", 11 | "prettier": "prettier -c --write \"**/*.{ts,tsx,js,jsx,json}\"", 12 | "lint-staged": "lint-staged", 13 | "lint-staged:ts": "eslint --ext .ts,.tsx", 14 | "lint:fix": "eslint --fix --cache --ext .ts,.tsx --format=pretty ./packages", 15 | "lint:prettier": "prettier --check \"**/*.{ts,tsx,js,jsx,json}\" --end-of-line auto", 16 | "deploy": "tcb framework deploy", 17 | "preext:zip": "rm -rf build && mkdir build && yarn run build", 18 | "preext:zip-wx": "rm -rf build && mkdir build && yarn run build:wx", 19 | "ext:zip": "bash ./scripts/zip.sh", 20 | "ext:zip-wx": "bash ./scripts/zip.sh" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "npm run lint-staged" 25 | } 26 | }, 27 | "lint-staged": { 28 | "**/*.{ts,tsx}": "npm run lint-staged:ts", 29 | "**/*.{tsx,ts,less,md,json}": [ 30 | "prettier --write" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "@typescript-eslint/eslint-plugin": "~3.7.0", 35 | "@typescript-eslint/parser": "~3.7.0", 36 | "cross-env": "^7.0.2", 37 | "eslint": "~7.5.0", 38 | "eslint-config-alloy": "~3.7.4", 39 | "eslint-formatter-pretty": "~4.0.0", 40 | "eslint-plugin-react": "~7.20.3", 41 | "husky": "^4.2.5", 42 | "lerna": "^4.0.0", 43 | "lint-staged": "^10.2.11", 44 | "prettier": "^2.0.5", 45 | "typescript": "^3.9.7" 46 | }, 47 | "engines": { 48 | "node": ">=10.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/admin/.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history -------------------------------------------------------------------------------- /packages/admin/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | /.vscode 12 | 13 | # misc 14 | .DS_Store 15 | npm-debug.log* 16 | yarn-error.log 17 | 18 | /coverage 19 | .idea 20 | package-lock.json 21 | *bak 22 | .vscode 23 | 24 | # visual studio code 25 | .history 26 | *.log 27 | functions/* 28 | .temp/** 29 | 30 | # umi 31 | .umi 32 | .umi-production 33 | 34 | # screenshot 35 | screenshot 36 | .firebase 37 | .eslintcache 38 | 39 | build 40 | 41 | public/config.js -------------------------------------------------------------------------------- /packages/admin/README.md: -------------------------------------------------------------------------------- 1 | # CMS Admin 2 | -------------------------------------------------------------------------------- /packages/admin/config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/consistent-type-assertions */ 2 | import { Settings as LayoutSettings } from '@ant-design/pro-layout' 3 | import platformConfig from './platform' 4 | 5 | export default { 6 | navTheme: 'light', 7 | primaryColor: '#0052d9', 8 | layout: 'mix', 9 | contentWidth: 'Fluid', 10 | fixedHeader: false, 11 | fixSiderbar: true, 12 | colorWeak: false, 13 | menu: { 14 | locale: false, 15 | defaultOpenAll: true, 16 | }, 17 | title: platformConfig.title, 18 | pwa: false, 19 | iconfontUrl: '', 20 | // 请求 prefix 21 | globalPrefix: '/api/v1.0', 22 | } as LayoutSettings & { 23 | pwa: boolean 24 | globalPrefix: string 25 | } 26 | -------------------------------------------------------------------------------- /packages/admin/config/platform.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from 'umi' 2 | 3 | const { WX_MP, SERVER_MODE } = process.env 4 | 5 | WX_MP && console.log('微信构建') 6 | 7 | SERVER_MODE && console.log('容器服务模式构建') 8 | 9 | const name = WX_MP ? '内容管理(CMS)' : 'CloudBase CMS' 10 | 11 | const { REACT_APP_ENV } = process.env 12 | 13 | /** 14 | * 和平台(小程序 OR 腾讯云)相关的一些配置 15 | */ 16 | const platformConfig: IConfig = { 17 | title: name, 18 | define: { 19 | WX_MP, 20 | SERVER_MODE, 21 | CMS_TITLE: name, 22 | ENV: REACT_APP_ENV, 23 | ICON_PATH: WX_MP ? 'icon-wx.svg' : 'icon.svg', 24 | }, 25 | layout: { 26 | name: name, 27 | }, 28 | } 29 | 30 | export default platformConfig 31 | -------------------------------------------------------------------------------- /packages/admin/config/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 3 | * The agent cannot take effect in the production environment 4 | * so there is no configuration of the production environment 5 | * For details, please see 6 | * https://pro.ant.design/docs/deploy 7 | */ 8 | export default { 9 | dev: { 10 | '/api': { 11 | target: 'http://localhost:5000', 12 | changeOrigin: true, 13 | pathRewrite: { '^': '' }, 14 | }, 15 | }, 16 | test: { 17 | '/api/': { 18 | target: 'http://localhost:5000', 19 | changeOrigin: true, 20 | pathRewrite: { '^': '' }, 21 | }, 22 | }, 23 | pre: { 24 | '/api/': { 25 | target: 'your pre url', 26 | changeOrigin: true, 27 | pathRewrite: { '^': '' }, 28 | }, 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /packages/admin/public/cmsSmsTemplate.csv: -------------------------------------------------------------------------------- 1 | 客户手机号,短信内容(小程序名称+短信内容 需要在33个字内,否则会变成2条短信),,请在大规模发送前先用测试号码对要提交的内容进行测试以避免发送错误而给您带来的损失。 2 | 1762044xxxx,你好,, 3 | 1667517xxxx,测试内容,, 4 | -------------------------------------------------------------------------------- /packages/admin/public/config.example.js: -------------------------------------------------------------------------------- 1 | window.TcbCmsConfig = { 2 | // 可用区,默认上海,可选:ap-shanghai 或 ap-guangzhou 3 | region: 'ap-shanghai', 4 | // 路由方式:hash 或 browser 5 | history: 'hash', 6 | // 环境 Id 7 | envId: 'Your EnvId', 8 | // 禁用通知 9 | disableNotice: false, 10 | // 禁用帮助按钮 11 | disableHelpButton: false, 12 | // 云接入默认域名/自定义域名 + 云接入路径,不带 https 协议符 13 | // https://console.cloud.tencent.com/tcb/env/access 14 | cloudAccessPath: 'xxx-xxx.service.tcloudbase.com/tcb-ext-cms-service', 15 | 16 | // === 17 | // 下面的配置为选择性配置 18 | // === 19 | 20 | // 容器模式时的访问路径 21 | containerAccessPath: 'xxx-xxx.service.tcloudbase.com/tcb-ext-cms-service-container', 22 | // 微信小程序 Id 23 | mpAppID: '', 24 | // CMS 文案配置 25 | cmsTitle: 'CloudBase CMS', 26 | // Logo 图片 27 | cmsLogo: './icon.svg', 28 | // 文档链接 29 | cmsDocLink: '', 30 | // 帮助链接 31 | cmsHelpLink: '', 32 | // 产品官网链接 33 | officialSiteLink: '', 34 | // 产品名 35 | appName: '', 36 | } 37 | -------------------------------------------------------------------------------- /packages/admin/public/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/packages/admin/public/home_bg.png -------------------------------------------------------------------------------- /packages/admin/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/packages/admin/public/icon.png -------------------------------------------------------------------------------- /packages/admin/public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/packages/admin/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /packages/admin/public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/packages/admin/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /packages/admin/public/icons/logo-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/packages/admin/public/icons/logo-192x192.png -------------------------------------------------------------------------------- /packages/admin/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/packages/admin/public/img/logo.png -------------------------------------------------------------------------------- /packages/admin/src/access.ts: -------------------------------------------------------------------------------- 1 | export default function access(initialState: { currentUser?: CurrentUser }) { 2 | const { currentUser } = initialState || {} 3 | 4 | console.log('当前用户', currentUser) 5 | 6 | const { username, isAdmin = false, isProjectAdmin = false, accessibleService, _id } = 7 | currentUser || {} 8 | 9 | // 是否能够访问服务 10 | const isServiceAccessible = (service: string) => 11 | canProjectAdmin || accessibleService?.includes('*') || accessibleService?.includes(service) 12 | 13 | const canProjectAdmin = isAdmin || isProjectAdmin 14 | 15 | const canContent = isServiceAccessible('content') 16 | 17 | const canSchema = isServiceAccessible('schema') 18 | 19 | const canWebhook = isServiceAccessible('webhook') 20 | 21 | const canOperation = isServiceAccessible('operation') 22 | 23 | return { 24 | isAdmin, 25 | canWebhook, 26 | canContent, 27 | canSchema, 28 | canOperation, 29 | canProjectAdmin, 30 | isLogin: Boolean(username || _id), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/admin/src/common/default.ts: -------------------------------------------------------------------------------- 1 | export const DefaultChannels = [ 2 | { 3 | value: '_cms_sms_', 4 | label: '短信', 5 | }, 6 | { 7 | value: 'zhihu', 8 | label: '知乎', 9 | }, 10 | { 11 | value: 'qqvideo', 12 | label: '腾讯视频', 13 | }, 14 | { 15 | value: 'wecom', 16 | label: '企业微信', 17 | }, 18 | { 19 | value: 'qq', 20 | label: 'QQ', 21 | }, 22 | ] 23 | -------------------------------------------------------------------------------- /packages/admin/src/common/hooks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // export const 4 | -------------------------------------------------------------------------------- /packages/admin/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './field' 2 | export * from './default' 3 | -------------------------------------------------------------------------------- /packages/admin/src/components/AvatarDropdown/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 4 | 5 | .menu { 6 | font-weight: bold; 7 | 8 | :global(.anticon) { 9 | margin-right: 8px; 10 | } 11 | 12 | :global(.ant-dropdown-menu-item) { 13 | min-width: 160px; 14 | } 15 | } 16 | 17 | .right { 18 | display: flex; 19 | float: right; 20 | height: 48px; 21 | margin-left: auto; 22 | overflow: hidden; 23 | 24 | .action { 25 | display: flex; 26 | align-items: center; 27 | height: 48px; 28 | padding: 0 12px; 29 | cursor: pointer; 30 | transition: all 0.3s; 31 | 32 | &:hover { 33 | background: @pro-header-hover-bg; 34 | } 35 | 36 | &:global(.opened) { 37 | background: @pro-header-hover-bg; 38 | } 39 | } 40 | 41 | .search { 42 | padding: 0 12px; 43 | 44 | &:hover { 45 | background: transparent; 46 | } 47 | } 48 | 49 | .account { 50 | .avatar { 51 | margin-right: 8px; 52 | color: @primary-color; 53 | vertical-align: top; 54 | background: rgba(255, 255, 255, 0.85); 55 | } 56 | } 57 | } 58 | 59 | .dark { 60 | .action { 61 | &:hover { 62 | background: #252a3d; 63 | } 64 | 65 | &:global(.opened) { 66 | background: #252a3d; 67 | } 68 | } 69 | } 70 | 71 | @media only screen and (max-width: @screen-md) { 72 | :global(.ant-divider-vertical) { 73 | vertical-align: unset; 74 | } 75 | 76 | .name { 77 | display: none; 78 | } 79 | 80 | .right { 81 | position: absolute; 82 | top: 0; 83 | right: 12px; 84 | 85 | .account { 86 | .avatar { 87 | margin-right: 0; 88 | } 89 | } 90 | 91 | .search { 92 | display: none; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/admin/src/components/BackNavigator/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space } from 'antd' 3 | import { history } from 'umi' 4 | import { LeftCircleTwoTone } from '@ant-design/icons' 5 | 6 | export interface IAppProps { 7 | children?: React.ReactNode 8 | } 9 | 10 | export default function BackNavigator({ children }: IAppProps) { 11 | return ( 12 |
history.goBack()}> 13 | 14 | 15 |

返回

16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/admin/src/components/ChannelSelector/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space, Typography, Select } from 'antd' 3 | import { useConcent } from 'concent' 4 | import { GlobalCtx } from 'typings/store' 5 | import { DefaultChannels } from '@/common' 6 | 7 | const { Option } = Select 8 | const { Text } = Typography 9 | 10 | const ChannelSelector: React.FC<{ onSelect: (v: string) => void }> = ({ onSelect }) => { 11 | const ctx = useConcent<{}, GlobalCtx>('global') 12 | const { setting } = ctx.state 13 | 14 | const { activityChannels = [] } = setting 15 | 16 | return ( 17 | 18 | 投放渠道 19 | 30 | 31 | ) 32 | } 33 | 34 | export default ChannelSelector 35 | -------------------------------------------------------------------------------- /packages/admin/src/components/Charts/Funnel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Funnel } from '@ant-design/charts' 3 | 4 | interface DataItem { 5 | stage: string 6 | number: number 7 | } 8 | 9 | export const FunnelChart: React.FC<{ data: DataItem[] }> = ({ data }) => { 10 | const config = { 11 | data, 12 | xField: 'stage', 13 | yField: 'number', 14 | } 15 | 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /packages/admin/src/components/Charts/Line.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { Line } from '@ant-design/charts' 3 | 4 | export const LineChart: React.FC = (props) => { 5 | const [data, setData] = useState([]) 6 | useEffect(() => {}, []) 7 | 8 | const config = { 9 | data: data, 10 | xField: 'year', 11 | yField: 'value', 12 | seriesField: 'category', 13 | xAxis: { type: 'time' }, 14 | yAxis: { 15 | label: { 16 | formatter: function formatter(v: any) { 17 | return ''.concat(v).replace(/\d{1,3}(?=(\d{3})+$)/g, function (s) { 18 | return ''.concat(s, ',') 19 | }) 20 | }, 21 | }, 22 | }, 23 | } 24 | 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /packages/admin/src/components/Charts/Pie.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pie } from '@ant-design/charts' 3 | 4 | interface DataItem { 5 | label: string 6 | value: number 7 | } 8 | 9 | export const PieChart: React.FC<{ data: DataItem[] }> = ({ data }) => { 10 | const config: any = { 11 | data, 12 | radius: 0.7, 13 | angleField: 'value', 14 | colorField: 'label', 15 | label: { 16 | type: 'outer', 17 | content: '{name} {percentage}', 18 | }, 19 | legend: { 20 | layout: 'horizontal', 21 | position: 'bottom', 22 | }, 23 | interactions: [{ type: 'element-active' }], 24 | } 25 | 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /packages/admin/src/components/Charts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Pie' 2 | export * from './Funnel' 3 | export * from './Line' 4 | -------------------------------------------------------------------------------- /packages/admin/src/components/Fields/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import VditorX from 'vditor' 3 | import 'vditor/dist/index.css' 4 | import { getAuthHeader, getHttpAccessPath } from '@/utils' 5 | 6 | // 工具栏 7 | const Toolbar = [ 8 | 'emoji', 9 | 'headings', 10 | 'bold', 11 | 'italic', 12 | 'strike', 13 | 'link', 14 | '|', 15 | 'list', 16 | 'ordered-list', 17 | 'check', 18 | 'outdent', 19 | 'indent', 20 | '|', 21 | 'quote', 22 | 'line', 23 | 'code', 24 | 'inline-code', 25 | 'insert-before', 26 | 'insert-after', 27 | '|', 28 | 'upload', 29 | 'record', 30 | 'table', 31 | '|', 32 | 'undo', 33 | 'redo', 34 | '|', 35 | 'fullscreen', 36 | 'edit-mode', 37 | { 38 | name: 'more', 39 | toolbar: [ 40 | 'both', 41 | 'code-theme', 42 | 'content-theme', 43 | 'export', 44 | 'outline', 45 | 'preview', 46 | 'devtools', 47 | 'info', 48 | 'help', 49 | ], 50 | }, 51 | ] 52 | 53 | export const MarkdownEditor: React.FC<{ 54 | id: number 55 | value?: any 56 | onChange?: (...args: any) => void 57 | }> = (props) => { 58 | const { value, id = 'default', onChange = (...args: any) => {} } = props 59 | 60 | const authHeader = getAuthHeader() 61 | 62 | useEffect(() => { 63 | // eslint-disable-next-line 64 | new VditorX(`${id}-editor`, { 65 | value, 66 | toolbar: Toolbar, 67 | input: (text, html) => { 68 | onChange(text) 69 | }, 70 | upload: { 71 | headers: authHeader, 72 | url: `${getHttpAccessPath()}/upload`, 73 | }, 74 | theme: 'classic', 75 | placeholder: '欢迎使用云开发 CMS Markdown编辑器', 76 | mode: 'sv', 77 | minHeight: 600, 78 | debugger: false, 79 | typewriterMode: false, 80 | cache: { 81 | enable: false, 82 | }, 83 | }) 84 | }, [authHeader?.['x-cloudbase-credentials']]) 85 | 86 | return
87 | } 88 | 89 | export default MarkdownEditor 90 | -------------------------------------------------------------------------------- /packages/admin/src/components/Fields/Switch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Switch } from 'antd' 3 | 4 | /** 5 | * 将 Switch 转换成符合 Form 表单要求的格式 6 | */ 7 | export const ISwitch: React.FC<{ 8 | // 非显式声明 9 | value?: boolean 10 | onChange?: (v: boolean) => void 11 | }> = ({ value, onChange }) => { 12 | return ( 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/admin/src/components/Fields/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Date' 2 | export * from './File' 3 | export * from './Image' 4 | export * from './Switch' 5 | export * from './Connect' 6 | export * from './FileAndImageEditor' 7 | export * from './FieldContentEditor' 8 | export * from './FieldContentRender' 9 | -------------------------------------------------------------------------------- /packages/admin/src/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { GithubOutlined } from '@ant-design/icons' 3 | import { DefaultFooter } from '@ant-design/pro-layout' 4 | import { getCmsConfig, getYear } from '@/utils' 5 | import pkg from '../../../package.json' 6 | 7 | export default () => ( 8 | , 22 | href: 'https://github.com/TencentCloudBase', 23 | blankTarget: true, 24 | }, 25 | { 26 | key: getCmsConfig('appName'), 27 | title: getCmsConfig('appName'), 28 | href: WX_MP 29 | ? 'https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html' 30 | : getCmsConfig('officialSiteLink'), 31 | blankTarget: true, 32 | }, 33 | ]} 34 | /> 35 | ) 36 | -------------------------------------------------------------------------------- /packages/admin/src/components/Loading/ContentLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Spin, SpinProps } from 'antd' 3 | 4 | export const ContentLoading: React.FC<{ tip?: string } & SpinProps> = ({ 5 | tip = '加载中', 6 | ...spinProps 7 | }) => ( 8 |
9 | 10 |
11 | ) 12 | -------------------------------------------------------------------------------- /packages/admin/src/components/Loading/PageLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Spin } from 'antd' 3 | import styled from 'styled-components' 4 | 5 | const LoadingBox = styled.div` 6 | position: fixed; 7 | left: 0; 8 | top: 0; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | flex-direction: column; 13 | width: 100%; 14 | height: 100vh; 15 | ` 16 | 17 | export default () => ( 18 |
19 | 20 |
21 | ) 22 | 23 | // export default () => { 24 | // return ( 25 | // 26 | //
27 | //
28 | //
29 | //
30 | //
31 | //
32 | //
33 | //
34 | //
35 | //
36 | //
37 | //
38 | //
39 | //
40 | //
41 | //
42 | //
43 | //
44 | //
45 | //
46 | //
47 | //
48 | //
49 | //
50 | //
51 | //
52 | // 53 | // ) 54 | // } 55 | -------------------------------------------------------------------------------- /packages/admin/src/components/Loading/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ContentLoading' 2 | -------------------------------------------------------------------------------- /packages/admin/src/components/Modal/ModalForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { MutableRefObject, useEffect, useRef, useState } from 'react' 2 | import { FormInstance, ModalForm, ModalFormProps } from '@ant-design/pro-form' 3 | 4 | export type ModalRefType = 5 | | { 6 | show?: () => void 7 | hide?: () => void 8 | } 9 | | undefined 10 | 11 | /** 12 | * 使用 ref 操控的 modal form 组件 13 | */ 14 | export const RefModalForm: React.FC< 15 | { 16 | modalRef: MutableRefObject 17 | } & ModalFormProps 18 | > = (props) => { 19 | const formRef = useRef() 20 | const { modalRef, ...modalProps } = props 21 | const [visible, setVisible] = useState(false) 22 | 23 | // 设置 ref 属性 24 | useEffect(() => { 25 | const show = () => setVisible(true) 26 | const hide = () => setVisible(false) 27 | 28 | if (modalRef.current) { 29 | modalRef.current.show = show 30 | modalRef.current.hide = hide 31 | } else { 32 | modalRef.current = { 33 | show, 34 | hide, 35 | } 36 | } 37 | }, []) 38 | 39 | // 重置表单数据 40 | useEffect(() => { 41 | formRef.current?.resetFields() 42 | }, [visible]) 43 | 44 | return ( 45 | 46 | {props.children} 47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /packages/admin/src/components/Modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ModalForm' 2 | -------------------------------------------------------------------------------- /packages/admin/src/components/RightContent/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 4 | 5 | .menu { 6 | :global(.anticon) { 7 | margin-right: 8px; 8 | } 9 | :global(.ant-dropdown-menu-item) { 10 | min-width: 160px; 11 | } 12 | } 13 | 14 | .right { 15 | display: flex; 16 | float: right; 17 | height: 48px; 18 | margin-left: auto; 19 | overflow: hidden; 20 | .action { 21 | display: flex; 22 | align-items: center; 23 | height: 48px; 24 | padding: 0 12px; 25 | cursor: pointer; 26 | transition: all 0.3s; 27 | &:hover { 28 | background: @pro-header-hover-bg; 29 | } 30 | &:global(.opened) { 31 | background: @pro-header-hover-bg; 32 | } 33 | } 34 | .search { 35 | padding: 0 12px; 36 | &:hover { 37 | background: transparent; 38 | } 39 | } 40 | .account { 41 | .avatar { 42 | margin-right: 8px; 43 | color: @primary-color; 44 | vertical-align: top; 45 | background: rgba(255, 255, 255, 0.85); 46 | } 47 | } 48 | } 49 | 50 | @media only screen and (max-width: @screen-md) { 51 | :global(.ant-divider-vertical) { 52 | vertical-align: unset; 53 | } 54 | .name { 55 | display: none; 56 | } 57 | .right { 58 | position: absolute; 59 | top: 0; 60 | right: 12px; 61 | .account { 62 | .avatar { 63 | margin-right: 0; 64 | } 65 | } 66 | .search { 67 | display: none; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/admin/src/components/RightContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useModel } from 'umi' 3 | import { Tooltip, Tag, Space } from 'antd' 4 | import { QuestionCircleOutlined } from '@ant-design/icons' 5 | import { getCmsConfig } from '@/utils' 6 | import styles from './index.less' 7 | 8 | export type SiderTheme = 'light' | 'dark' 9 | 10 | const ENVTagColor = { 11 | dev: 'orange', 12 | test: 'green', 13 | pre: '#87d068', 14 | } 15 | 16 | const GlobalHeaderRight: React.FC<{}> = () => { 17 | const { initialState } = useModel('@@initialState') 18 | 19 | if (!initialState || !initialState.settings) { 20 | return null 21 | } 22 | 23 | const { navTheme, layout } = initialState.settings 24 | let className = styles.right 25 | 26 | if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') { 27 | className = `${styles.right} ${styles.dark}` 28 | } 29 | 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | {REACT_APP_ENV && ( 38 | 39 | {REACT_APP_ENV} 40 | 41 | )} 42 | 43 | ) 44 | } 45 | export default GlobalHeaderRight 46 | -------------------------------------------------------------------------------- /packages/admin/src/components/SecurityWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Access, history, IRoute, useAccess } from 'umi' 3 | import { Result, Button } from 'antd' 4 | 5 | const PageAccess: React.FC<{ 6 | route: IRoute 7 | }> = (props) => { 8 | const access = useAccess() 9 | const { children, route } = props 10 | 11 | const accessible = Boolean(access[route.access]) 12 | 13 | return ( 14 | { 25 | history.push('/home') 26 | }} 27 | > 28 | 回到首页 29 | 30 | } 31 | /> 32 | } 33 | > 34 | {children} 35 | 36 | ) 37 | } 38 | 39 | export default PageAccess 40 | -------------------------------------------------------------------------------- /packages/admin/src/components/Typography/Text.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Typography } from 'antd' 3 | 4 | const { Text } = Typography 5 | 6 | export const BoldText: React.FC = ({ children }) => ( 7 | 8 | {children} 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /packages/admin/src/components/Typography/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Text' 2 | -------------------------------------------------------------------------------- /packages/admin/src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * CMS 资源名前缀 3 | */ 4 | export const RESOURCE_PREFIX = WX_MP ? 'wx-ext-cms' : 'tcb-ext-cms' 5 | 6 | export const codeMessage = { 7 | 200: '服务器成功返回请求的数据。', 8 | 201: '服务器成功返回请求的数据。', 9 | 202: '一个请求已经进入后台排队(异步任务)。', 10 | 204: '删除数据成功。', 11 | 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', 12 | 401: '您还没有登录,或登录身份过期,请登录后再操作!', 13 | 403: '您没有权限访问此资源或进行此操作!', 14 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', 15 | 405: '请求方法不被允许。', 16 | 406: '请求的格式不可得。', 17 | 410: '请求的资源被永久删除,且不会再得到的。', 18 | 422: '当创建一个对象时,发生一个验证错误。', 19 | 500: '服务器发生错误,请检查服务器。', 20 | 502: '网关错误。', 21 | 503: '服务不可用,服务器暂时过载或维护。', 22 | 504: '网关超时。', 23 | } 24 | -------------------------------------------------------------------------------- /packages/admin/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CloudBase CMS", 3 | "short_name": "CloudBase CMS", 4 | "display": "standalone", 5 | "start_url": "./?utm_source=homescreen", 6 | "theme_color": "#002140", 7 | "background_color": "#001529", 8 | "icons": [ 9 | { 10 | "src": "icons/icon-192x192.png", 11 | "sizes": "192x192" 12 | }, 13 | { 14 | "src": "icons/icon-128x128.png", 15 | "sizes": "128x128" 16 | }, 17 | { 18 | "src": "icons/icon-512x512.png", 19 | "sizes": "512x512" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/admin/src/models/content.ts: -------------------------------------------------------------------------------- 1 | import { IActionCtx } from 'concent' 2 | import { getContentSchemas } from '@/services/content' 3 | 4 | interface ContentState { 5 | schemas: Schema[] 6 | loading: boolean 7 | contentAction: 'create' | 'edit' 8 | selectedContent: any 9 | searchFields: any[] 10 | searchParams: any 11 | currentSchema: any 12 | } 13 | 14 | const state: ContentState = { 15 | schemas: [], 16 | loading: false, 17 | // create 或 edit 18 | contentAction: 'create', 19 | selectedContent: {}, 20 | // 保存搜索条件 21 | searchFields: [], 22 | searchParams: {}, 23 | currentSchema: {}, 24 | } 25 | 26 | export default { 27 | state, 28 | reducer: { 29 | addSearchField(field: any, state: ContentState) { 30 | const { searchFields } = state 31 | return { 32 | searchFields: searchFields.concat(field), 33 | } 34 | }, 35 | removeSearchField(field: any, state: ContentState) { 36 | const { searchFields } = state 37 | const index = searchFields.findIndex((_) => _.id === field.id) 38 | searchFields.splice(index, 1) 39 | return { 40 | searchFields, 41 | } 42 | }, 43 | clearSearchField() { 44 | return { 45 | searchFields: [], 46 | } 47 | }, 48 | setSearchFields(fields: any[], state: ContentState) { 49 | return { 50 | searchFields: fields, 51 | } 52 | }, 53 | async getContentSchemas(projectId: string, state: any, ctx: IActionCtx) { 54 | ctx.setState({ 55 | loading: true, 56 | }) 57 | 58 | try { 59 | const { data } = await getContentSchemas(projectId) 60 | 61 | return { 62 | schemas: data, 63 | loading: false, 64 | } 65 | } catch (error) { 66 | console.log(error) 67 | return { 68 | schemas: [], 69 | loading: false, 70 | } 71 | } 72 | }, 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /packages/admin/src/models/global.ts: -------------------------------------------------------------------------------- 1 | import { getSetting, updateSetting } from '@/services/global' 2 | import { getCloudBaseApp } from '@/utils' 3 | 4 | interface GlobalState { 5 | /** 6 | * 当前项目信息 7 | */ 8 | currentProject?: Project 9 | 10 | /** 11 | * 设置 12 | */ 13 | setting: GlobalSetting 14 | } 15 | 16 | const state: GlobalState = { 17 | currentProject: undefined, 18 | 19 | setting: {}, 20 | } 21 | 22 | export default { 23 | state, 24 | reducer: { 25 | // 重新获取设置信息 26 | async getSetting() { 27 | const { data } = await getSetting() 28 | return { 29 | setting: data, 30 | } 31 | }, 32 | // 更新设置信息 33 | async updateSetting(setting: GlobalSetting & { keepApiPath?: boolean }, state: GlobalState) { 34 | await updateSetting(setting) 35 | 36 | return { 37 | setting: { 38 | ...state.setting, 39 | ...setting, 40 | }, 41 | } 42 | }, 43 | // 创建 API Token 44 | // async createApiAuthToken(_: undefined, state: GlobalState) { 45 | // console.log(state) 46 | // const res = await createApiAuthToken() 47 | // console.log(res) 48 | 49 | // return { 50 | // setting: { 51 | // ...state.setting, 52 | // }, 53 | // } 54 | // }, 55 | }, 56 | init: async () => { 57 | try { 58 | // 校验是否登录 59 | const app = await getCloudBaseApp() 60 | const loginState = await app.auth({ persistence: 'local' }).getLoginState() 61 | if (!loginState) return {} 62 | 63 | // 获取全局设置 64 | const { data = {} } = await getSetting() 65 | 66 | return { 67 | setting: data, 68 | } 69 | } catch (error) { 70 | console.log(error) 71 | return {} 72 | } 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /packages/admin/src/models/index.ts: -------------------------------------------------------------------------------- 1 | // export { default as $$global } from './global' 2 | export { default as global } from './global' 3 | export { default as schema } from './schema' 4 | export { default as content } from './content' 5 | export { default as role } from './role' 6 | export { default as microApp } from './microApp' 7 | -------------------------------------------------------------------------------- /packages/admin/src/models/microApp.ts: -------------------------------------------------------------------------------- 1 | interface AppState { 2 | appAction: 'create' | 'edit' 3 | selectedApp: any 4 | } 5 | 6 | const state: AppState = { 7 | appAction: 'create', 8 | selectedApp: {}, 9 | } 10 | 11 | export default { 12 | state, 13 | reducer: {}, 14 | } 15 | -------------------------------------------------------------------------------- /packages/admin/src/models/role.ts: -------------------------------------------------------------------------------- 1 | interface RoleState { 2 | roleAction: 'create' | 'edit' 3 | selectedRole: any 4 | } 5 | 6 | const state: RoleState = { 7 | roleAction: 'create', 8 | selectedRole: {}, 9 | } 10 | 11 | export default { 12 | state, 13 | reducer: {}, 14 | } 15 | -------------------------------------------------------------------------------- /packages/admin/src/models/webhook.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | state: { 3 | projectId: '', 4 | webhookAction: 'create', 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/admin/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd' 2 | import React, { useEffect, useState } from 'react' 3 | import { history } from 'umi' 4 | 5 | const NoFoundPage: React.FC<{}> = () => { 6 | const [count, setCount] = useState(5) 7 | 8 | useEffect(() => { 9 | const timer = setInterval(() => { 10 | setCount((count) => count - 1) 11 | }, 1000) 12 | 13 | return () => { 14 | clearInterval(timer) 15 | } 16 | }, []) 17 | 18 | useEffect(() => { 19 | if (count <= 0) { 20 | history.push('/home') 21 | } 22 | }, [count]) 23 | 24 | return ( 25 | history.push('/home')}> 31 | 回到首页 32 | 33 | } 34 | /> 35 | ) 36 | } 37 | 38 | export default NoFoundPage 39 | -------------------------------------------------------------------------------- /packages/admin/src/pages/home/index.less: -------------------------------------------------------------------------------- 1 | .home { 2 | min-height: 100vh !important; 3 | 4 | .header { 5 | position: relative; 6 | background: #262f3e; 7 | background: -webkit-linear-gradient(to right, #262f3e, #536976); 8 | background: linear-gradient(to right, #262f3e, #536976); 9 | 10 | color: #fff; 11 | box-shadow: 0 2px 8px #f0f1f2; 12 | font-weight: bolder; 13 | text-align: center; 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | padding: 0 16px; 18 | 19 | .title { 20 | color: #fff; 21 | height: 32px; 22 | line-height: 32px; 23 | margin-bottom: 0; 24 | font-size: 18px; 25 | font-weight: 600; 26 | } 27 | 28 | .left { 29 | display: flex; 30 | align-items: center; 31 | 32 | h1 { 33 | margin: 0 0 0 12px; 34 | } 35 | } 36 | 37 | .right { 38 | position: absolute; 39 | right: 40px; 40 | display: flex; 41 | align-items: center; 42 | } 43 | 44 | .logo { 45 | width: 35px; 46 | height: 35px; 47 | } 48 | } 49 | 50 | .content { 51 | margin: 48px auto; 52 | width: 90%; 53 | max-width: 850px; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/microapp/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Skeleton } from 'antd' 3 | import { MicroApp, history } from 'umi' 4 | import { PageContainer } from '@ant-design/pro-layout' 5 | import ErrorBoundary from '@/components/ErrorBoundary' 6 | import { useConcent } from 'concent' 7 | import { GlobalCtx } from 'typings/store' 8 | 9 | /** 10 | * 挂载微应用 11 | */ 12 | const MicroContainer = () => { 13 | const ctx = useConcent<{}, GlobalCtx>('global') 14 | const { setting } = ctx.state 15 | 16 | window.__POWERED_BY_QIANKUN__ = true 17 | // 添加实例方法 18 | window.TcbCmsInsRef = { 19 | history, 20 | } 21 | 22 | // TODO 通信 23 | window.addEventListener('_FROM_CMS_MICRO_APP_SLAVE_', (e: Event) => { 24 | // console.log('收到信息', e) 25 | }) 26 | 27 | // 从路径中获取微应用 id 28 | const microAppID = history.location.pathname.replace('/project/microapp/', '').split('/').shift() 29 | 30 | if (!microAppID) { 31 | return 32 | } 33 | 34 | const microApp = setting?.microApps?.find((_) => _.id === microAppID) 35 | 36 | return ( 37 | 38 | { 40 | return
微应用渲染异常
41 | }} 42 | > 43 | 44 |
45 |
46 | ) 47 | } 48 | 49 | export default MicroContainer 50 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/operation/Activity/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import ProCard from '@ant-design/pro-card' 3 | import { PageContainer } from '@ant-design/pro-layout' 4 | import { useConcent } from 'concent' 5 | import { redirectTo } from '@/utils' 6 | import { ContentCtx, GlobalCtx } from 'typings/store' 7 | import { ActivityTable } from './ActivityTable' 8 | import { ActivitySchema } from './schema' 9 | 10 | export default (): React.ReactNode => { 11 | const globalCtx = useConcent<{}, GlobalCtx>('global') 12 | const contentCtx = useConcent<{}, ContentCtx>('content') 13 | const { setting } = globalCtx.state 14 | 15 | useEffect(() => { 16 | contentCtx.setState({ 17 | currentSchema: ActivitySchema, 18 | }) 19 | }, []) 20 | 21 | if (!setting?.enableOperation) { 22 | redirectTo('operation') 23 | return '' 24 | } 25 | 26 | return ( 27 | 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/operation/Analytics/DataSource.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space, Spin } from 'antd' 3 | import ProCard from '@ant-design/pro-card' 4 | import { PieChartTwoTone } from '@ant-design/icons' 5 | 6 | const cardStyle = { 7 | height: '500px', 8 | } 9 | 10 | /** 11 | * 获取 metric 对应的数据 12 | */ 13 | const DataSource: React.FC<{ data: any; title: React.ReactNode; loading: boolean }> = ({ 14 | title, 15 | children, 16 | data, 17 | loading, 18 | }) => { 19 | // 加载中 20 | if (loading || !data || data === -1) { 21 | return ( 22 | 23 |
24 | 25 | 26 | {loading ? ( 27 | 28 | ) : ( 29 |

{data === -1 ? '数据为空' : '加载中...'}

30 | )} 31 |
32 |
33 |
34 | ) 35 | } 36 | 37 | // 数据为空 38 | const isEmptyData = !data?.filter((_: any) => _?.value || _?.number)?.length 39 | if (isEmptyData) { 40 | return ( 41 | 42 |
43 | 44 | 45 | {loading ? :

数据为空

} 46 |
47 |
48 |
49 | ) 50 | } 51 | 52 | // set data 53 | const childrenWithProps = React.Children.map(children, (child) => { 54 | if (React.isValidElement(child)) { 55 | return React.cloneElement(child, { data }) 56 | } 57 | return child 58 | }) 59 | 60 | return ( 61 | 62 | {childrenWithProps} 63 | 64 | ) 65 | } 66 | 67 | export default DataSource 68 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/operation/Analytics/OverviewRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Col, Row, Card, Typography, Spin, Space, Tooltip } from 'antd' 3 | import { QuestionCircleOutlined } from '@ant-design/icons' 4 | 5 | const { Text } = Typography 6 | 7 | const topColResponsiveProps = { 8 | xs: 24, 9 | sm: 12, 10 | md: 12, 11 | lg: 12, 12 | xl: 8, 13 | style: { marginBottom: 24 }, 14 | } 15 | 16 | const OverviewRow: React.FC<{ data: any; loading: boolean }> = ({ data, loading }) => { 17 | return ( 18 | 19 | 20 | 24 | H5 访问用户总数 25 | 26 | 27 | 28 | 29 | } 30 | count={data?.webPageViewCount || 0} 31 | /> 32 | 36 | 跳转小程序用户总数 37 | 38 | 39 | 40 | 41 | } 42 | count={data?.miniappViewCount || 0} 43 | /> 44 | 45 | ) 46 | } 47 | 48 | const ShowCardCol: React.FC<{ loading: boolean; title: React.ReactNode; count: number }> = ({ 49 | loading, 50 | title, 51 | count, 52 | }) => { 53 | return ( 54 | 55 | 56 | {title} 57 |

{loading ? : count || 0}

58 |
59 | 60 | ) 61 | } 62 | 63 | export default OverviewRow 64 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/operation/Message/schema.ts: -------------------------------------------------------------------------------- 1 | import { RESOURCE_PREFIX } from '@/constants' 2 | 3 | export const TaskSchema: any = { 4 | fields: [ 5 | { 6 | description: '【小程序名称】,点击 云开发静态网站 URL 打开小程序名称小程序,退订回T。', 7 | displayName: '短信内容', 8 | id: 'y0um9jhk9v9mrk424wbj95jleg2d2f8y', 9 | isRequired: true, 10 | max: 30, 11 | name: 'content', 12 | order: 0, 13 | type: 'String', 14 | }, 15 | { 16 | displayName: '活动', 17 | description: '关联的活动', 18 | connectField: 'activityName', 19 | connectResource: 'b45a21d55ff939720430e24e0f94cb12', 20 | id: 'o91ouff816sbu0owdjqbcluira1enlqs', 21 | isRequired: true, 22 | name: 'activityId', 23 | order: 1, 24 | type: 'Connect', 25 | }, 26 | { 27 | description: '以 , 号分割', 28 | displayName: '手机号码包', 29 | id: '70qq715n2ytm4dhp67to8j4xoou9kijt', 30 | isRequired: true, 31 | name: 'phoneNumberList', 32 | order: 2, 33 | type: 'MultiLineString', 34 | }, 35 | { 36 | defaultValue: '', 37 | description: '发送状态', 38 | displayName: '发送状态', 39 | id: 'sp7adqpyejpwt86721plcvlk15ijnn6m', 40 | isHidden: false, 41 | isRequired: false, 42 | name: 'status', 43 | order: 3, 44 | type: 'string', 45 | }, 46 | { 47 | defaultValue: 0, 48 | description: '电话号码数量', 49 | displayName: '发送用户数', 50 | id: 'tmaimgo7jtr9t3c7fiprvkl6knxb42ji', 51 | max: 1000, 52 | min: 0, 53 | name: 'total', 54 | order: 4, 55 | type: 'Number', 56 | }, 57 | { 58 | dateFormatType: 'timestamp-ms', 59 | description: '创建时间', 60 | displayName: '创建时间', 61 | id: 'fmpx6prjehenfvl0v0amzo58ckwf7kqb', 62 | isRequired: true, 63 | name: 'createTime', 64 | order: 5, 65 | type: 'DateTime', 66 | }, 67 | ], 68 | collectionName: `${RESOURCE_PREFIX}-sms-tasks`, 69 | displayName: '发送短信', 70 | _id: '21ded5cb5ff93faa0456bdef6be2a7d6', 71 | } 72 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/operation/Message/util.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'antd' 2 | 3 | /** 4 | * 从输入字符串中解析号码列表 5 | */ 6 | export const resolveAndCheckPhoneNumbers = (phoneNumbers: string): string[] | undefined => { 7 | if (phoneNumbers.includes('\n') && phoneNumbers.includes(',')) { 8 | message.error('请勿混用换行和英文分号 ,') 9 | return 10 | } 11 | 12 | let phoneNumberList: string[] = [phoneNumbers] 13 | 14 | if (phoneNumbers.includes('\n')) { 15 | phoneNumberList = phoneNumbers 16 | .split('\n') 17 | .filter((_) => _) 18 | .map((num) => num.trim()) 19 | } 20 | if (phoneNumbers.includes(',')) { 21 | phoneNumberList = phoneNumbers 22 | .split(',') 23 | .filter((_) => _) 24 | .map((num) => num.trim()) 25 | } 26 | 27 | if (!phoneNumberList?.length) { 28 | message.error('号码不能为空') 29 | return 30 | } 31 | 32 | // 去重 33 | phoneNumberList = phoneNumberList.filter((num, i, arr) => arr.findIndex((_) => _ === num) === i) 34 | 35 | return phoneNumberList 36 | } 37 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/overview/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card } from 'antd' 3 | import { PageContainer } from '@ant-design/pro-layout' 4 | import { getCmsConfig } from '@/utils' 5 | 6 | export default (): React.ReactNode => { 7 | return ( 8 | 9 | 欢迎使用 {getCmsConfig('cmsTitle')} 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/schema/SchemaFieldEditor/SchemaFieldDelete.tsx: -------------------------------------------------------------------------------- 1 | import { useConcent } from 'concent' 2 | import React, { useState } from 'react' 3 | import { Modal, message } from 'antd' 4 | import { ContentCtx, SchmeaCtx } from 'typings/store' 5 | import { updateSchema } from '@/services/schema' 6 | import { getProjectId } from '@/utils' 7 | 8 | /** 9 | * 删除字段 10 | */ 11 | export const SchemaFieldDeleteModal: React.FC<{ 12 | visible: boolean 13 | onClose: () => void 14 | }> = ({ visible, onClose }) => { 15 | const projectId = getProjectId() 16 | const ctx = useConcent<{}, SchmeaCtx>('schema') 17 | const contentCtx = useConcent<{}, ContentCtx>('content') 18 | const [loading, setLoading] = useState(false) 19 | 20 | const { 21 | state: { currentSchema, selectedField }, 22 | } = ctx 23 | 24 | return ( 25 | { 34 | setLoading(true) 35 | const fields: any[] = (currentSchema.fields || []).slice() 36 | const index = fields.findIndex( 37 | (_: any) => _.id === selectedField.id || _.name === selectedField.name 38 | ) 39 | 40 | if (index > -1) { 41 | fields.splice(index, 1) 42 | } 43 | 44 | try { 45 | await updateSchema(projectId, currentSchema?._id, { 46 | fields, 47 | }) 48 | currentSchema.fields.splice(index, 1) 49 | message.success('删除字段成功') 50 | ctx.mr.getSchemas(projectId) 51 | contentCtx.mr.getContentSchemas(projectId) 52 | } catch (error) { 53 | message.error('删除字段失败') 54 | } finally { 55 | onClose() 56 | setLoading(false) 57 | } 58 | }} 59 | onCancel={() => onClose()} 60 | > 61 | 确认删除【{selectedField.displayName}({selectedField?.name})】字段吗? 62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/schema/SchemaFieldEditor/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SchemaFieldEdit' 2 | export * from './SchemaFieldDelete' 3 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/schema/SchemaFieldPicker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useConcent } from 'concent' 3 | import { Card, Layout, List, message, Typography } from 'antd' 4 | import { FieldTypes } from '@/common' 5 | import { SchmeaCtx } from 'typings/store' 6 | import styled from 'styled-components' 7 | 8 | const { Sider } = Layout 9 | 10 | export interface TableListItem { 11 | key: number 12 | name: string 13 | status: string 14 | updatedAt: number 15 | createdAt: number 16 | progress: number 17 | money: number 18 | } 19 | 20 | const ListContainer = styled.div` 21 | overflow: auto; 22 | padding: 0 10px 0 10px; 23 | height: calc(100% - 100px); 24 | ` 25 | 26 | const SchemaFieldPicker: React.FC = () => { 27 | const ctx = useConcent<{}, SchmeaCtx>('schema') 28 | const { 29 | state: { currentSchema }, 30 | } = ctx 31 | 32 | return ( 33 | 34 | 35 | 内容类型 36 | 37 | 38 | ( 42 | { 46 | if (!currentSchema) { 47 | message.info('请选择需要编辑的模型') 48 | return 49 | } 50 | ctx.setState({ 51 | fieldAction: 'create', 52 | selectedField: item, 53 | editFieldVisible: true, 54 | }) 55 | }} 56 | > 57 | 58 | {item.icon} 59 | {item.name} 60 | 61 | 62 | )} 63 | /> 64 | 65 | 66 | ) 67 | } 68 | 69 | export default SchemaFieldPicker 70 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/schema/SchemaMenuList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useConcent } from 'concent' 3 | import { Menu, Row, Col, Spin } from 'antd' 4 | import { SchmeaCtx } from 'typings/store' 5 | 6 | export interface TableListItem { 7 | key: number 8 | name: string 9 | status: string 10 | updatedAt: number 11 | createdAt: number 12 | progress: number 13 | money: number 14 | } 15 | 16 | /** 17 | * 展示模型列表 18 | */ 19 | const SchemaMenuList: React.FC = () => { 20 | const ctx = useConcent<{}, SchmeaCtx>('schema') 21 | const { 22 | state: { currentSchema, schemas, loading }, 23 | } = ctx 24 | 25 | const defaultSelectedMenu = currentSchema?._id ? [currentSchema._id] : [] 26 | 27 | return loading ? ( 28 | 29 | 30 | 31 | 32 | 33 | ) : schemas?.length ? ( 34 | { 38 | const schema = schemas.find((item: any) => item._id === key) 39 | ctx.setState({ 40 | currentSchema: schema, 41 | }) 42 | }} 43 | > 44 | {schemas.map((item: Schema) => ( 45 | {item.displayName} 46 | ))} 47 | 48 | ) : ( 49 | 50 | 模型为空 51 | 52 | ) 53 | } 54 | 55 | export default SchemaMenuList 56 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/schema/SchmeaContent/SchemaFieldList.tsx: -------------------------------------------------------------------------------- 1 | import { useConcent } from 'concent' 2 | import React, { useCallback } from 'react' 3 | import { Button, Empty, Space } from 'antd' 4 | import { SchmeaCtx } from 'typings/store' 5 | import { SchemaFieldListRender } from './FieldListRender' 6 | 7 | export interface TableListItem { 8 | key: number 9 | name: string 10 | status: string 11 | updatedAt: number 12 | createdAt: number 13 | progress: number 14 | money: number 15 | } 16 | 17 | const SchemaFields: React.FC = () => { 18 | const ctx = useConcent<{}, SchmeaCtx>('schema') 19 | const { 20 | state: { currentSchema }, 21 | } = ctx 22 | 23 | // 编辑字段 24 | const editFiled = useCallback((field: SchemaField, index: number) => { 25 | ctx.setState({ 26 | fieldAction: 'edit', 27 | selectedField: field, 28 | editFieldVisible: true, 29 | selectedFieldIndex: index, 30 | }) 31 | }, []) 32 | 33 | return currentSchema?.fields?.length ? ( 34 | editFiled(field, index)} 37 | actionRender={(field, index) => ( 38 | 39 | 49 | 63 | 64 | )} 65 | /> 66 | ) : ( 67 |
68 | 69 |
70 | ) 71 | } 72 | 73 | export default SchemaFields 74 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/setting/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tabs, Row, Col } from 'antd' 3 | import ProCard from '@ant-design/pro-card' 4 | import { PageContainer } from '@ant-design/pro-layout' 5 | import ProjectInfo from './ProjectInfo' 6 | import ApiAccess from './ApiAccess' 7 | 8 | const { TabPane } = Tabs 9 | 10 | const TabPaneContent: React.FC = ({ children }) => ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ) 17 | 18 | export default (): React.ReactNode => { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/admin/src/pages/project/webhook/WebhookExecLog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ProTable, { ProColumns } from '@ant-design/pro-table' 3 | import { getWebhookLog } from '@/services/webhook' 4 | import { getProjectId } from '@/utils' 5 | import { WebhookLogColumns } from './columns' 6 | 7 | const columns: ProColumns[] = WebhookLogColumns.map((item) => ({ 8 | ...item, 9 | align: 'center', 10 | })) 11 | 12 | export default () => { 13 | const projectId = getProjectId() 14 | 15 | // 获取 webhooks 16 | const tableRequest = async ( 17 | params: { pageSize: number; current: number; [key: string]: any }, 18 | sort: { 19 | [key: string]: 'ascend' | 'descend' | null 20 | }, 21 | filter: { 22 | [key: string]: React.ReactText[] 23 | } 24 | ) => { 25 | const { current, pageSize } = params 26 | 27 | try { 28 | const { data = [], total } = await getWebhookLog(projectId, { 29 | sort: { 30 | timestamp: 'descend', 31 | }, 32 | filter, 33 | pageSize, 34 | page: current, 35 | }) 36 | 37 | return { 38 | data, 39 | total, 40 | success: true, 41 | } 42 | } catch (error) { 43 | console.log(error) 44 | return { 45 | data: [], 46 | total: 0, 47 | success: true, 48 | } 49 | } 50 | } 51 | 52 | return ( 53 | 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /packages/admin/src/pages/redirect.tsx: -------------------------------------------------------------------------------- 1 | import { Spin } from 'antd' 2 | import React, { useEffect } from 'react' 3 | import { history, useRequest } from 'umi' 4 | import { getCollectionInfo } from '@/services/apis' 5 | import { redirectTo } from '@/utils' 6 | 7 | /** 8 | * 重定向来自低码的访问,到对应的集合 9 | * from=lowcode&customId=xxx 10 | */ 11 | export default () => { 12 | const { collectionName, from, customId } = history.location.query || {} 13 | 14 | // 不存在 projectCustomId 15 | if (!customId || !from) { 16 | history.push('/home') 17 | return '' 18 | } 19 | 20 | // 获取项目、模型信息 21 | const { data } = useRequest<{ 22 | data: { 23 | schema: Schema 24 | project: Project 25 | } 26 | }>(() => getCollectionInfo(customId as string, collectionName as string)) 27 | 28 | useEffect(() => { 29 | if (!data) return 30 | const { schema, project } = data 31 | const projectId = project?._id || schema?.projectId 32 | 33 | // 跳转到对应的集合管理页面 34 | if (schema?._id) { 35 | redirectTo(`content/${schema._id}`, { 36 | projectId, 37 | }) 38 | } else if (project) { 39 | redirectTo('home', { 40 | projectId, 41 | }) 42 | } else { 43 | history.push('/home') 44 | } 45 | }, [data]) 46 | 47 | return ( 48 |
49 | 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /packages/admin/src/pages/system-setting/RoleManagement/RoleEditor/RoleInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Form, Space, Button, Input } from 'antd' 3 | 4 | const RoleInfo: React.FC<{ 5 | initialValues: any 6 | onConfrim: (...args: any) => void 7 | }> = ({ onConfrim, initialValues }) => { 8 | return ( 9 |
{ 15 | onConfrim(v) 16 | }} 17 | > 18 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 |
47 | ) 48 | } 49 | 50 | export default RoleInfo 51 | -------------------------------------------------------------------------------- /packages/admin/src/pages/system-setting/SettingContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Col, Row } from 'antd' 2 | import React from 'react' 3 | 4 | export interface IAppProps { 5 | className?: string 6 | children: React.ReactNode 7 | } 8 | 9 | export default function SettingContainer({ className, children }: IAppProps) { 10 | return ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/admin/src/services/apis.ts: -------------------------------------------------------------------------------- 1 | import request from 'umi-request' 2 | import { getAuthHeader, getHttpAccessPath, tcbRequest } from '@/utils' 3 | 4 | interface ApiRequestPayload { 5 | service: 'util' | 'file' | 'setting' | 'auth' 6 | action: string 7 | [key: string]: any 8 | } 9 | 10 | export const apiRequest = (data: ApiRequestPayload) => { 11 | return tcbRequest('/', { 12 | data, 13 | method: 'POST', 14 | }) 15 | } 16 | 17 | /** 18 | * 获取当前登录的用户信息 19 | * @param file 20 | */ 21 | export async function getCurrentUser() { 22 | return apiRequest({ 23 | service: 'auth', 24 | action: 'getCurrentUser', 25 | }) 26 | } 27 | 28 | /** 29 | * 上传文件到静态网站托管 30 | */ 31 | export const uploadFilesToHosting = (file: File, filePath: string) => { 32 | const formData = new FormData() 33 | formData.append('filePath', filePath) 34 | formData.append('file', file) 35 | 36 | const url = getHttpAccessPath() 37 | const authHeader = getAuthHeader() 38 | 39 | return request(`${url}/upload/hosting`, { 40 | data: formData, 41 | method: 'POST', 42 | headers: authHeader, 43 | }) 44 | } 45 | 46 | /** 47 | * 获取集合信息 48 | */ 49 | export const getCollectionInfo = async (customId: string, collectionName: string) => { 50 | return tcbRequest('/', { 51 | method: 'POST', 52 | data: { 53 | customId, 54 | collectionName, 55 | service: 'util', 56 | action: 'getCollectionInfo', 57 | }, 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /packages/admin/src/services/global.ts: -------------------------------------------------------------------------------- 1 | import { tcbRequest } from '@/utils' 2 | 3 | /** 4 | * 获取全局设置 5 | */ 6 | export const getSetting = async (): Promise<{ 7 | data: GlobalSetting 8 | }> => { 9 | return tcbRequest('/setting', { 10 | method: 'GET', 11 | }) 12 | } 13 | 14 | /** 15 | * 更新全局设置 16 | */ 17 | export const updateSetting = async (payload: Partial) => { 18 | return tcbRequest('/setting', { 19 | method: 'PATCH', 20 | data: payload, 21 | }) 22 | } 23 | 24 | /** 25 | * 创建微应用 26 | */ 27 | export const createMicroApp = async (data: MicroApp) => { 28 | return tcbRequest('/setting/createMicroApp', { 29 | data, 30 | method: 'POST', 31 | }) 32 | } 33 | 34 | /** 35 | * 更新微应用 36 | */ 37 | export const updateMicroApp = async (data: MicroApp) => { 38 | return tcbRequest('/setting/updateMicroApp', { 39 | data, 40 | method: 'POST', 41 | }) 42 | } 43 | 44 | /** 45 | * 删除微应用 46 | */ 47 | export const deleteMicroApp = async (data: MicroApp) => { 48 | return tcbRequest('/setting/deleteMicroApp', { 49 | data, 50 | method: 'POST', 51 | }) 52 | } 53 | 54 | /** 55 | * 创建 API Auth Token 56 | */ 57 | export const createApiAuthToken = async (data: { name: string; permissions: string[] }) => { 58 | return tcbRequest('/setting/createApiAuthToken', { 59 | data, 60 | method: 'POST', 61 | }) 62 | } 63 | 64 | /** 65 | * 删除 API Auth Token 66 | */ 67 | export const deleteApiAuthToken = async (id: string) => { 68 | return tcbRequest('/setting/deleteApiAuthToken', { 69 | method: 'POST', 70 | data: { 71 | id, 72 | }, 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /packages/admin/src/services/login.ts: -------------------------------------------------------------------------------- 1 | import { tcbRequest } from '@/utils' 2 | 3 | export interface LoginParamsType { 4 | username: string 5 | password: string 6 | mobile?: string 7 | captcha?: string 8 | type?: string 9 | } 10 | 11 | export async function getFakeCaptcha(mobile: string) { 12 | return tcbRequest(`/login/captcha?mobile=${mobile}`) 13 | } 14 | -------------------------------------------------------------------------------- /packages/admin/src/services/notice.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi' 2 | 3 | export async function getCmsNotices(startTime: number) { 4 | return request(`https://tcli.service.tcloudbase.com/cms-notice?startTime=${startTime}`, { 5 | prefix: '', 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /packages/admin/src/services/operation.ts: -------------------------------------------------------------------------------- 1 | import { callWxOpenAPI, tcbRequest } from '@/utils' 2 | 3 | export async function enableOperationService(projectId: string, data: any = {}) { 4 | return tcbRequest(`/projects/${projectId}/operation/enableOperationService`, { 5 | data, 6 | method: 'POST', 7 | }) 8 | } 9 | 10 | export async function createBatchTask(projectId: string, data: any = {}) { 11 | return tcbRequest(`/projects/${projectId}/operation/createBatchTask`, { 12 | data, 13 | method: 'POST', 14 | }) 15 | } 16 | 17 | export async function enableNonLogin(projectId: string) { 18 | return tcbRequest(`/projects/${projectId}/operation/enableNonLogin`, { 19 | method: 'POST', 20 | }) 21 | } 22 | 23 | export async function getAnalyticsData(data: { activityId: string }) { 24 | return callWxOpenAPI('getAnalyticsData', data) 25 | } 26 | 27 | export async function getRealtimeAnalyticsData(data: { 28 | activityId: string 29 | startTime: number 30 | endTime: number 31 | channelId: string 32 | }) { 33 | return callWxOpenAPI('getRealtimeAnalyticsData', data) 34 | } 35 | 36 | export async function getSmsTaskResult( 37 | projectId: string, 38 | data: { queryId: string; pageSize: number; page: number } 39 | ) { 40 | return tcbRequest(`/projects/${projectId}/operation/getSmsTaskResult`, { 41 | data, 42 | method: 'POST', 43 | }) 44 | } 45 | 46 | export async function getLowCodeAppInfo(projectId: string) { 47 | return tcbRequest(`/projects/${projectId}/operation/getLowCodeAppInfo`, { 48 | method: 'POST', 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /packages/admin/src/services/project.ts: -------------------------------------------------------------------------------- 1 | import { tcbRequest } from '@/utils' 2 | 3 | export async function getProject(id: string) { 4 | return tcbRequest<{ 5 | data: Project 6 | }>(`/projects/${id}`, { 7 | method: 'GET', 8 | }) 9 | } 10 | 11 | export async function getProjects() { 12 | return tcbRequest<{ 13 | data: Project[] 14 | }>('/projects', { 15 | method: 'GET', 16 | }) 17 | } 18 | 19 | export async function createProject(payload: { name: string; description: string }) { 20 | return tcbRequest<{ 21 | data: Project[] 22 | }>('/projects', { 23 | method: 'POST', 24 | data: payload, 25 | }) 26 | } 27 | 28 | export async function updateProject( 29 | id: string, 30 | payload: Partial & { keepApiPath?: boolean } 31 | ) { 32 | return tcbRequest<{ 33 | data: Project[] 34 | }>(`/projects/${id}`, { 35 | method: 'PATCH', 36 | data: payload, 37 | }) 38 | } 39 | 40 | export async function deleteProject(id: string) { 41 | return tcbRequest<{ 42 | data: Project[] 43 | }>(`/projects/${id}`, { 44 | method: 'DELETE', 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /packages/admin/src/services/role.ts: -------------------------------------------------------------------------------- 1 | import { tcbRequest } from '@/utils' 2 | 3 | export const getUserRoles = async (page = 1, pageSize = 10) => { 4 | return tcbRequest('/roles', { 5 | method: 'GET', 6 | params: { 7 | page, 8 | pageSize, 9 | }, 10 | }) 11 | } 12 | 13 | export const createUserRole = async (role: any) => { 14 | return tcbRequest('/roles', { 15 | method: 'POST', 16 | data: role, 17 | }) 18 | } 19 | 20 | export const updateUserRole = async (id: string, role: any) => { 21 | return tcbRequest(`/roles/${id}`, { 22 | method: 'PATCH', 23 | data: role, 24 | }) 25 | } 26 | 27 | export const deleteUserRole = async (id: string) => { 28 | return tcbRequest(`/roles/${id}`, { 29 | method: 'DELETE', 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /packages/admin/src/services/schema.ts: -------------------------------------------------------------------------------- 1 | import { tcbRequest } from '@/utils' 2 | 3 | export async function getSchemas(projectId?: string): Promise<{ data: Schema[] }> { 4 | return tcbRequest(`/projects/${projectId}/schemas`, { 5 | method: 'GET', 6 | }) 7 | } 8 | 9 | export async function getSchema(projectId: string, schemaId: string): Promise<{ data: Schema }> { 10 | return tcbRequest(`/projects/${projectId}/schemas/${schemaId}`, { 11 | method: 'GET', 12 | }) 13 | } 14 | 15 | export async function createSchema(projectId: string, schema: Partial) { 16 | return tcbRequest(`/projects/${projectId}/schemas`, { 17 | method: 'POST', 18 | data: schema, 19 | }) 20 | } 21 | 22 | export async function updateSchema(projectId: string, schemaId: string, schema: Partial) { 23 | return tcbRequest(`/projects/${projectId}/schemas/${schemaId}`, { 24 | method: 'PATCH', 25 | data: schema, 26 | }) 27 | } 28 | 29 | export async function deleteSchema(projectId: string, schemaId: string, deleteCollection: boolean) { 30 | return tcbRequest(`/projects/${projectId}/schemas/${schemaId}`, { 31 | method: 'DELETE', 32 | data: { 33 | deleteCollection, 34 | }, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /packages/admin/src/services/user.ts: -------------------------------------------------------------------------------- 1 | import { tcbRequest } from '@/utils' 2 | 3 | export const getUsers = async (page = 1, pageSize = 10) => { 4 | return tcbRequest('/user', { 5 | method: 'GET', 6 | params: { 7 | page, 8 | pageSize, 9 | }, 10 | }) 11 | } 12 | 13 | export const createUser = async (user: Record) => { 14 | return tcbRequest('/user', { 15 | method: 'POST', 16 | data: user, 17 | }) 18 | } 19 | 20 | export const updateUser = async (id: string, payload: Record) => { 21 | return tcbRequest(`/user/${id}`, { 22 | method: 'PATCH', 23 | data: payload, 24 | }) 25 | } 26 | 27 | export const deleteUser = async (userId: string) => { 28 | return tcbRequest(`/user/${userId}`, { 29 | method: 'DELETE', 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /packages/admin/src/services/webhook.ts: -------------------------------------------------------------------------------- 1 | import { tcbRequest } from '@/utils' 2 | 3 | export interface Options { 4 | page?: number 5 | pageSize?: number 6 | 7 | filter?: { 8 | _id?: string 9 | ids?: string[] 10 | [key: string]: any 11 | } 12 | 13 | fuzzyFilter?: { 14 | [key: string]: any 15 | } 16 | 17 | sort?: { 18 | [key: string]: 'ascend' | 'descend' | null 19 | } 20 | 21 | payload: Record 22 | } 23 | 24 | export const getWebhooks = async (projectId: string, options?: Partial) => { 25 | return tcbRequest(`/projects/${projectId}/webhooks`, { 26 | method: 'POST', 27 | data: { 28 | options, 29 | action: 'getMany', 30 | }, 31 | }) 32 | } 33 | 34 | export const getWebhookLog = async (projectId: string, options?: Partial) => { 35 | return tcbRequest(`/projects/${projectId}/webhooks/log`, { 36 | method: 'POST', 37 | data: { 38 | options, 39 | action: 'getMany', 40 | }, 41 | }) 42 | } 43 | 44 | export const createWebhook = async (projectId: string, options?: Partial) => { 45 | return tcbRequest(`/projects/${projectId}/webhooks`, { 46 | method: 'POST', 47 | data: { 48 | options, 49 | action: 'createOne', 50 | }, 51 | }) 52 | } 53 | 54 | export const updateWebhook = async (projectId: string, options?: Partial) => { 55 | return tcbRequest(`/projects/${projectId}/webhooks`, { 56 | method: 'POST', 57 | data: { 58 | options, 59 | action: 'updateOne', 60 | }, 61 | }) 62 | } 63 | 64 | export const deleteWebhook = async (projectId: string, options?: Partial) => { 65 | return tcbRequest(`/projects/${projectId}/webhooks`, { 66 | method: 'POST', 67 | data: { 68 | options, 69 | action: 'deleteOne', 70 | }, 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /packages/admin/src/utils/common.ts: -------------------------------------------------------------------------------- 1 | // 生成随机字符串 2 | export const random = (len = 32) => { 3 | const count = Math.ceil(Number(len) / 10) + 1 4 | let ret = '' 5 | for (let i = 0; i < count; i++) { 6 | ret += Math.random().toString(36).substr(2) 7 | } 8 | return ret.substr(0, len) 9 | } 10 | 11 | /** 12 | * 计算字符串的 hash 值 13 | */ 14 | export const hashCode = (str: string) => { 15 | let i = str.length 16 | let hash1 = 5381 17 | let hash2 = 52711 18 | 19 | while (i--) { 20 | const char = str.charCodeAt(i) 21 | hash1 = (hash1 * 33) ^ char 22 | hash2 = (hash2 * 33) ^ char 23 | } 24 | 25 | return (hash1 >>> 0) * 4096 + (hash2 >>> 0) 26 | } 27 | 28 | // 判断是否是开发环境 29 | export const isDevEnv = () => process.env.NODE_ENV === 'development' 30 | 31 | // 延迟 32 | export const sleep = async (interval: number) => { 33 | return new Promise((resolve) => { 34 | setTimeout(() => { 35 | resolve() 36 | }, interval) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /packages/admin/src/utils/config.ts: -------------------------------------------------------------------------------- 1 | const InnerDefaultValue: Partial = { 2 | appName: 'CloudBase', 3 | cmsTitle: 'CloudBase CMS', 4 | cmsLogo: './icon.svg', 5 | cmsDocLink: 'https://docs.cloudbase.net/cms/intro.html', 6 | cmsHelpLink: 'https://support.qq.com/products/148793', 7 | officialSiteLink: 'https://cloudbase.net', 8 | } 9 | 10 | /** 11 | * 获取 CMS 配置,适配小程序 OR 腾讯云 12 | */ 13 | export const getCmsConfig = (key: keyof ITcbCmsConfing, defaultValue?: any) => { 14 | // 获取 CMS 配置 15 | return window.TcbCmsConfig[key] || defaultValue || InnerDefaultValue[key] || '' 16 | } 17 | -------------------------------------------------------------------------------- /packages/admin/src/utils/doc.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash.isequal' 2 | 3 | /** 4 | * 处理内容文档 5 | */ 6 | export const getDocInitialValues = (action: string, schema: Schema, selectedContent: any) => { 7 | const initialValues = 8 | action === 'create' 9 | ? schema?.fields?.reduce((prev, field) => { 10 | let { type, defaultValue } = field 11 | // 布尔值默认为 false 12 | if (type === 'Boolean' && typeof defaultValue !== 'boolean') { 13 | defaultValue = false 14 | } 15 | return { 16 | ...prev, 17 | [field.name]: defaultValue, 18 | } 19 | }, {}) 20 | : selectedContent 21 | 22 | if (action === 'edit') { 23 | schema?.fields?.forEach((field) => { 24 | let { type, name, isMultiple } = field 25 | 26 | const fieldValue = selectedContent[name] 27 | 28 | // 布尔值默认为 false 29 | if (type === 'Boolean' && typeof fieldValue !== 'boolean') { 30 | selectedContent[name] = false 31 | } 32 | 33 | // 如果字段是 multiple 类型,将异常的字符串值,转换为正常的数组 34 | if (isMultiple && typeof fieldValue === 'string') { 35 | selectedContent[name] = [fieldValue] 36 | } 37 | }) 38 | } 39 | 40 | return initialValues 41 | } 42 | 43 | /** 44 | * 比较 doc 获取变更的值 45 | */ 46 | export const getDocChangedValues = (oldDoc: Object, newDoc: Object): any => { 47 | // doc 相等 48 | if (oldDoc === newDoc || isEqual(oldDoc, newDoc)) return newDoc 49 | 50 | // 按 key 比较 51 | const docKeys: string[] = Object.keys(newDoc) 52 | 53 | // 相同的值返回 null,否则返回 key,根据 key 获取变更的值 54 | return docKeys 55 | .map((key) => { 56 | if (isEqual(newDoc[key], oldDoc[key])) { 57 | return null 58 | } else { 59 | return key 60 | } 61 | }) 62 | .filter((_) => _ !== null) 63 | .reduce((obj: any, key: any) => { 64 | obj[key] = newDoc[key] 65 | return obj 66 | }, {}) 67 | } 68 | -------------------------------------------------------------------------------- /packages/admin/src/utils/file.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'antd' 2 | import request from 'umi-request' 3 | 4 | /** 5 | * 将字符串保存为文件并下载 6 | */ 7 | export const saveContentToFile = (content: string, fileName: string, type = 'application/json') => { 8 | if (!/\.json$/.test(fileName) && type === 'application/json') { 9 | throw new Error('文件格式指定错误') 10 | } 11 | return saveFile(new Blob([content], { type }), fileName) 12 | } 13 | 14 | /** 15 | * 浏览器保存文件到本地 16 | */ 17 | export const saveFile = (file: Blob, fileName: string) => { 18 | if (window.navigator.msSaveOrOpenBlob) { 19 | // IE10+ 20 | window.navigator.msSaveOrOpenBlob(file, fileName) 21 | } else { 22 | const url = URL.createObjectURL(file) 23 | downloadFileFromUrl(url, fileName) 24 | } 25 | } 26 | 27 | /** 28 | * 通过链接下载文件 29 | */ 30 | export const downloadFileFromUrl = (url: string, fileName: string) => { 31 | // 创建一个链接 32 | const a = document.createElement('a') 33 | a.href = url 34 | a.download = fileName 35 | document.body.appendChild(a) 36 | a.click() 37 | 38 | setTimeout(function () { 39 | document.body.removeChild(a) 40 | window.URL.revokeObjectURL(url) 41 | }, 0) 42 | } 43 | 44 | /** 45 | * 下载文件 46 | */ 47 | export const downloadAndSaveFile = async (url: string, fileName: string) => { 48 | try { 49 | const data = await request(url, { 50 | responseType: 'blob', 51 | }) 52 | saveFile(data, fileName) 53 | } catch (e) { 54 | message.error(`下载文件失败:${e.message}`) 55 | } 56 | } 57 | 58 | /** 59 | * 读取文件,获取文件内容 60 | */ 61 | export const readFile = (file: Blob): Promise => { 62 | const fileReader = new FileReader() 63 | 64 | return new Promise((resolve, reject) => { 65 | fileReader.onload = (e) => { 66 | resolve(e.target?.result as string) 67 | } 68 | 69 | fileReader.onerror = reject 70 | fileReader.readAsText(file) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /packages/admin/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './route' 2 | export * from './common' 3 | export * from './text' 4 | export * from './cloudbase' 5 | export * from './date' 6 | export * from './file' 7 | export * from './field' 8 | export * from './config' 9 | export * from './qrcode' 10 | export * from './doc' 11 | export * from './templateCompile' 12 | -------------------------------------------------------------------------------- /packages/admin/src/utils/qrcode.ts: -------------------------------------------------------------------------------- 1 | import QRCode from 'qrcode' 2 | 3 | export const generateQRCode = async (text: string) => { 4 | try { 5 | return QRCode.toDataURL(text, { 6 | margin: 0, 7 | }) 8 | } catch (err) { 9 | console.error(err) 10 | return '' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/admin/src/utils/route.ts: -------------------------------------------------------------------------------- 1 | import { getState } from 'concent' 2 | import { history } from 'umi' 3 | import { parse } from 'querystring' 4 | 5 | const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/ 6 | 7 | export const isUrl = (path: string): boolean => reg.test(path) 8 | 9 | /** 10 | * 获取 query 参数 11 | */ 12 | export const getPageQuery = (): Record => { 13 | const { href } = window.location 14 | const qsIndex = href.indexOf('?') 15 | const sharpIndex = href.indexOf('#') 16 | 17 | if (qsIndex !== -1) { 18 | if (qsIndex > sharpIndex) { 19 | return parse(href.split('?')[1]) as Record 20 | } 21 | 22 | return parse(href.slice(qsIndex + 1, sharpIndex)) as Record 23 | } 24 | 25 | return {} 26 | } 27 | 28 | /** 29 | * 从 url 中获取项目 id 30 | */ 31 | export const getProjectId = () => { 32 | // 全局 state 33 | const state: any = getState() 34 | // page query 35 | const query = getPageQuery() 36 | 37 | return query?.pid || state?.global?.currentProject?._id || '' 38 | } 39 | 40 | /** 41 | * 跳转到项目某个路径 42 | */ 43 | export const redirectTo = ( 44 | pathname: string, 45 | options: { 46 | projectId?: string 47 | query?: Record 48 | } = {} 49 | ) => { 50 | const pageQuery = getPageQuery() 51 | const { projectId, query } = options 52 | const pid: string = projectId || (pageQuery?.pid as string) || '' 53 | 54 | const path = pathname[0] === '/' ? pathname.slice(1) : pathname 55 | 56 | history.push({ 57 | pathname: `/project/${path}`, 58 | query: { 59 | pid, 60 | ...query, 61 | }, 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /packages/admin/src/utils/templateCompile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 模板引擎,替换 {} 模版变量,用法: 3 | * templateCompile('The mobile number of {name} is {phone.mobile}', { 4 | * name: 'name', 5 | * phone: { 6 | * mobile: '609 24 363' 7 | * } 8 | * }); 9 | */ 10 | export const templateCompile = (template: string, data: Record) => { 11 | // 匹配字符串 12 | const braceRegex = /{(\d+|[a-z$_][a-z\d$_]*?(?:\.[a-z\d$_]*?)*?)}/gi 13 | 14 | return template.replace(braceRegex, (_, key) => { 15 | let result = data 16 | 17 | for (const property of key.split('.')) { 18 | result = result ? result[property] : '' 19 | } 20 | 21 | return String(result) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /packages/admin/src/utils/text.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 拷贝到剪切板 3 | * @param value 4 | */ 5 | export function copyToClipboard(str: string, execCommand?: Document['execCommand']) { 6 | const el = document.createElement('textarea') 7 | el.value = str 8 | el.setAttribute('readonly', 'false') 9 | el.setAttribute('contenteditable', 'true') 10 | el.style.position = 'absolute' 11 | el.style.left = '-99999px' 12 | document.body.append(el) 13 | 14 | // 保存原有的选择区域 15 | const selected = 16 | (document.getSelection()?.rangeCount || -1) > 0 ? document.getSelection()?.getRangeAt(0) : false 17 | el.select() 18 | el.setSelectionRange(0, el.textLength) // iOS 中使用 select() 函数无效 19 | execCommand?.call(document, 'copy') 20 | document.execCommand('copy') 21 | 22 | document.body.removeChild(el) 23 | 24 | if (selected) { 25 | document.getSelection()?.removeAllRanges() 26 | document.getSelection()?.addRange(selected) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/admin/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = { 4 | important: true, 5 | theme: { 6 | maxWidth: { 7 | 80: '80%', 8 | }, 9 | }, 10 | purge: ['./src/**/*.{js,ts,jsx,tsx}'], 11 | variants: { 12 | extend: {}, 13 | }, 14 | plugins: [], 15 | } 16 | -------------------------------------------------------------------------------- /packages/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "sourceMap": true, 8 | "baseUrl": ".", 9 | "jsx": "react", 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUnusedLocals": true, 16 | "allowJs": true, 17 | "skipLibCheck": true, 18 | "experimentalDecorators": true, 19 | "strict": true, 20 | "paths": { 21 | "@/*": ["./src/*"], 22 | "@@/*": ["./src/.umi/*"], 23 | "@lang": ["./src/locales"] 24 | }, 25 | "resolveJsonModule": true 26 | }, 27 | "exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/admin/typings/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'slash2' 2 | declare module '*.css' 3 | declare module '*.less' 4 | declare module '*.scss' 5 | declare module '*.sass' 6 | declare module '*.svg' 7 | declare module '*.png' 8 | declare module '*.jpg' 9 | declare module '*.jpeg' 10 | declare module '*.gif' 11 | declare module '*.bmp' 12 | declare module '*.tiff' 13 | declare module 'omit.js' 14 | declare module 'lodash.isequal' 15 | declare module 'braft-utils' 16 | 17 | declare const WX_MP: boolean 18 | 19 | declare const SERVER_MODE: boolean 20 | 21 | declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false 22 | -------------------------------------------------------------------------------- /packages/admin/typings/user.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 当前登录用户 3 | */ 4 | interface CurrentUser { 5 | _id: string 6 | 7 | username: string 8 | 9 | password: string 10 | 11 | // 创建时间 12 | createTime: number 13 | 14 | // 用户角色 15 | roles: string[] 16 | 17 | avatar?: string 18 | 19 | // 是否项目管理员 20 | isAdmin: boolean 21 | 22 | // 项目管理员 23 | isProjectAdmin: boolean 24 | 25 | // 所有可访问的服务 26 | accessibleService?: '*' | string[] 27 | } 28 | 29 | /** 30 | * 项目 31 | */ 32 | interface Project { 33 | _id: string 34 | 35 | name: string 36 | 37 | customId: string 38 | 39 | description: string 40 | 41 | // 项目封面图 42 | cover?: string 43 | 44 | // 是否开启 Api 访问 45 | enableApiAccess: boolean 46 | 47 | // api 访问路径 48 | apiAccessPath: string 49 | 50 | // 可读集合 51 | readableCollections: string[] 52 | 53 | // 可修改的集合 54 | modifiableCollections: string[] 55 | 56 | // 可删除的集合 57 | deletableCollections: string[] 58 | 59 | /** 60 | * 分组 61 | */ 62 | group?: string[] 63 | } 64 | 65 | /** 66 | * 用户管理 67 | */ 68 | interface User { 69 | _id: string 70 | 71 | username: string 72 | 73 | // 创建时间 74 | createTime: number 75 | 76 | // 用户角色 77 | roles: UserRole[] 78 | 79 | // cloudbase uuid 80 | uuid: string 81 | 82 | // 是否为 root 用户 83 | root?: boolean 84 | } 85 | 86 | /** 87 | * 用户角色 88 | */ 89 | interface UserRole { 90 | _id: string 91 | 92 | // 角色名 93 | roleName: string 94 | 95 | // 角色描述 96 | description: string 97 | 98 | // 角色绑定的权限描述 99 | permissions: Permission[] 100 | 101 | type: string | 'system' 102 | } 103 | -------------------------------------------------------------------------------- /packages/admin/typings/webhook.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * webhook 定义 3 | */ 4 | interface Webhook { 5 | _id: string 6 | 7 | name: string 8 | 9 | /** 10 | * webhook 类型 11 | */ 12 | type: 'http' | 'function' 13 | 14 | event: string[] 15 | 16 | collections: (Schema & '*')[] 17 | 18 | /** 19 | * http webhook 属性 20 | */ 21 | url: string 22 | 23 | method: string 24 | 25 | headers: { key: string; value: string }[] 26 | 27 | /** 28 | * function webhook 属性 29 | */ 30 | functionName: string 31 | } 32 | -------------------------------------------------------------------------------- /packages/cms-api/.env: -------------------------------------------------------------------------------- 1 | # 服务配置 2 | SERVER_PORT=5001 3 | 4 | # 超时配置 5 | RES_TIMEOUT=15000 6 | 7 | # Webhook 请求处理超时 8 | WEBHOOK_TIMEOUT=10000 9 | -------------------------------------------------------------------------------- /packages/cms-api/.env.example: -------------------------------------------------------------------------------- 1 | # 云开发环境 Id 2 | TCB_ENVID= 3 | 4 | # 腾讯云 API 秘钥 5 | SECRETID= 6 | SECRETKEY= 7 | 8 | -------------------------------------------------------------------------------- /packages/cms-api/.eslintignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /packages/cms-api/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /packages/cms-api/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const main = require('./dist/main') 3 | 4 | exports.tcbGetApp = main.bootstrap 5 | -------------------------------------------------------------------------------- /packages/cms-api/dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.15.0-alpine 2 | 3 | RUN apk --update add tzdata \ 4 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 5 | && echo "Asia/Shanghai" > /etc/timezone \ 6 | && apk del tzdata 7 | 8 | RUN mkdir -p /usr/src/app 9 | 10 | WORKDIR /usr/src/app 11 | 12 | # add npm package 13 | COPY package.json /usr/src/app/package.json 14 | COPY yarn.lock /usr/src/app/yarn.lock 15 | 16 | # RUN npm i --registry=https://registry.npm.taobao.org 17 | RUN yarn --registry=https://registry.npm.taobao.org 18 | 19 | # copy code 20 | COPY . /usr/src/app 21 | 22 | EXPOSE 5000 23 | 24 | CMD npm start 25 | -------------------------------------------------------------------------------- /packages/cms-api/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const serverless = require('serverless-http') 3 | const entry = require('./app.js') 4 | 5 | module.exports.main = async (event, context) => { 6 | context.callbackWaitsForEmptyEventLoop = false 7 | let app = entry 8 | 9 | // support for async load app 10 | if (entry && entry.tcbGetApp && typeof entry.tcbGetApp === 'function') { 11 | app = await entry.tcbGetApp() 12 | } 13 | 14 | return serverless(app)(event, context) 15 | } 16 | -------------------------------------------------------------------------------- /packages/cms-api/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /packages/cms-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudbase-cms-api", 3 | "version": "2.13.19", 4 | "description": "CloudBase content manager system service restful api", 5 | "author": "cwuyiqing@gmail.com", 6 | "private": true, 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "start": "nest start", 11 | "dev": "cross-env NODE_ENV=development DEBUG=cms:* nest start --watch", 12 | "debug": "nest start --debug --watch" 13 | }, 14 | "dependencies": { 15 | "@cloudbase/node-sdk": "2.4.6-beta", 16 | "@cloudbase/signature-nodejs": "^1.1.0", 17 | "@nestjs/common": "^7.3.2", 18 | "@nestjs/config": "^0.5.0", 19 | "@nestjs/core": "^7.3.2", 20 | "@nestjs/platform-express": "^7.3.2", 21 | "axios": "^0.21.1", 22 | "bson": "^4.2.0", 23 | "class-transformer": "^0.3.1", 24 | "class-validator": "^0.12.2", 25 | "debug": "^4.1.1", 26 | "helmet": "^4.1.1", 27 | "lodash": "^4.17.19", 28 | "nanoid": "^3.1.10", 29 | "reflect-metadata": "^0.1.13", 30 | "rxjs": "^6.6.0", 31 | "serverless-http": "^2.5.0" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "^7.4.1", 35 | "@nestjs/schematics": "^7.0.1", 36 | "@types/debug": "^4.1.5", 37 | "@types/express": "^4.17.7", 38 | "@types/lodash": "^4.14.158", 39 | "@types/node": "^14.0.23", 40 | "@typescript-eslint/eslint-plugin": "^3.6.1", 41 | "@typescript-eslint/parser": "^3.6.1", 42 | "cross-env": "^7.0.2", 43 | "eslint": "^7.4.0", 44 | "eslint-config-alloy": "^3.7.3", 45 | "rimraf": "^3.0.2", 46 | "ts-loader": "^8.0.0", 47 | "ts-node": "^8.10.2", 48 | "tsconfig-paths": "^3.9.0", 49 | "typescript": "^3.9.6" 50 | }, 51 | "engines": { 52 | "node": ">=10.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/cms-api/src/api/api.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ApiController } from './api.controller' 3 | import { ApiService } from './api.service' 4 | 5 | @Module({ 6 | controllers: [ApiController], 7 | providers: [ApiService], 8 | }) 9 | export class ApiModule {} 10 | -------------------------------------------------------------------------------- /packages/cms-api/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post } from '@nestjs/common' 2 | import { AppService } from './app.service' 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | async getHello(): Promise { 10 | return this.appService.getHello() 11 | } 12 | 13 | @Post() 14 | getStatus(): Promise { 15 | return this.appService.getHello() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cms-api/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, MiddlewareConsumer, NestModule, Scope } from '@nestjs/common' 2 | import { ConfigModule } from '@nestjs/config' 3 | import { AppController } from '@/app.controller' 4 | import { AppService } from '@/app.service' 5 | import { BodyConverter } from '@/middlewares/converter.middleware' 6 | import { ApiModule } from './api/api.module' 7 | import { GlobalModule } from './global.module' 8 | 9 | @Module({ 10 | imports: [ 11 | ApiModule, 12 | GlobalModule, 13 | ConfigModule.forRoot({ 14 | isGlobal: true, 15 | envFilePath: process.env.NODE_ENV === 'development' ? ['.env', '.env.local'] : '.env', 16 | }), 17 | ], 18 | controllers: [AppController], 19 | providers: [AppService], 20 | }) 21 | export class AppModule implements NestModule { 22 | configure(consumer: MiddlewareConsumer) { 23 | consumer.apply(BodyConverter).forRoutes('*') 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cms-api/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class AppService { 5 | async getHello(): Promise { 6 | return 'Hello World! Powered by Nest & CloudBase!' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/cms-api/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error' 2 | -------------------------------------------------------------------------------- /packages/cms-api/src/constants.ts: -------------------------------------------------------------------------------- 1 | // 云函数、数据库等资源命名前缀 2 | export const RESOURCE_PREFIX = process.env.CMS_RESOURCE_PREFIX || 'tcb-ext-cms' 3 | 4 | /** 5 | * 数据库 6 | */ 7 | export const Collection = { 8 | // 项目集合 9 | Projects: `${RESOURCE_PREFIX}-projects`, 10 | 11 | // 内容模型集合 12 | Schemas: `${RESOURCE_PREFIX}-schemas`, 13 | 14 | // Webhooks 集合 15 | Webhooks: `${RESOURCE_PREFIX}-webhooks`, 16 | 17 | // 系统设置 18 | Settings: `${RESOURCE_PREFIX}-settings`, 19 | 20 | // 用户集合 21 | Users: `${RESOURCE_PREFIX}-users`, 22 | 23 | // 定义的角色集合 24 | CustomUserRoles: `${RESOURCE_PREFIX}-user-roles`, 25 | 26 | // 数据导入导出的记录 27 | DataMigrateTasks: `${RESOURCE_PREFIX}-data-migrate`, 28 | 29 | // 短信活动 30 | MessageActivity: `${RESOURCE_PREFIX}-sms-activities`, 31 | 32 | // 发送短信记录 33 | MessageTasks: `${RESOURCE_PREFIX}-sms-tasks`, 34 | } 35 | -------------------------------------------------------------------------------- /packages/cms-api/src/global.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common' 2 | import { CloudBaseService, LocalCacheService } from './services' 3 | 4 | @Global() 5 | @Module({ 6 | providers: [CloudBaseService, LocalCacheService], 7 | exports: [CloudBaseService, LocalCacheService], 8 | }) 9 | export class GlobalModule {} 10 | -------------------------------------------------------------------------------- /packages/cms-api/src/guards/action.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, Injectable, ExecutionContext, mixin, Inject } from '@nestjs/common' 2 | import { CmsException, ErrorCode, UnauthorizedOperation } from '@/common' 3 | import { LocalCacheService } from '@/services' 4 | 5 | // 映射 action 和对应的权限控制字段 6 | const ACTION_MAP = { 7 | read: 'readableCollections', 8 | modify: 'modifiableCollections', 9 | delete: 'deletableCollections', 10 | } 11 | 12 | @Injectable() 13 | export class MixinActionGuard implements CanActivate { 14 | // 操作 15 | protected readonly action: 'read' | 'modify' | 'delete' 16 | 17 | constructor(@Inject('LocalCacheService') private readonly cacheService: LocalCacheService) {} 18 | 19 | async canActivate(context: ExecutionContext): Promise { 20 | const req = context.switchToHttp().getRequest() 21 | // const res = context.switchToHttp().getResponse() 22 | 23 | // 数据库集合名 24 | const collectionName = req.params?.collectionName 25 | 26 | // 从缓存中读取 project 27 | const project = this.cacheService.get('project') 28 | 29 | if (!this.action) { 30 | throw new CmsException(ErrorCode.ServerError, 'Missing Action') 31 | } 32 | 33 | // 校验 action 是否允许 34 | // 兼容原项目中的设置,2.12.0+ 35 | if ( 36 | project?.[ACTION_MAP[this.action]]?.includes(collectionName) || 37 | req.accessToken?.permissions?.includes(this.action) 38 | ) { 39 | return true 40 | } 41 | 42 | throw new UnauthorizedOperation() 43 | } 44 | } 45 | 46 | export const ActionGuard = (action: 'read' | 'modify' | 'delete') => { 47 | const guard = mixin( 48 | class extends MixinActionGuard { 49 | protected readonly action = action 50 | } 51 | ) 52 | return guard 53 | } 54 | -------------------------------------------------------------------------------- /packages/cms-api/src/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard' 2 | export * from './action.guard' 3 | -------------------------------------------------------------------------------- /packages/cms-api/src/interceptors/timecost.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { tap } from 'rxjs/operators' 3 | import { Response } from 'express' 4 | import { Injectable, ExecutionContext, CallHandler, NestInterceptor } from '@nestjs/common' 5 | 6 | @Injectable() 7 | export class TimeCost implements NestInterceptor { 8 | intercept(context: ExecutionContext, next: CallHandler): Observable { 9 | return next.handle().pipe( 10 | tap(() => { 11 | const res = context.switchToHttp().getResponse() as Response 12 | // 计算请求耗时,并添加到 header 13 | const timeCost = Date.now() - res.locals.cost 14 | res.header('X-Request-Cost', `${timeCost}`) 15 | 16 | console.log(`> 请求处理耗时: ${timeCost} ms`) 17 | }) 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/cms-api/src/interceptors/timeout.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { timeout } from 'rxjs/operators' 3 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common' 4 | 5 | @Injectable() 6 | export class TimeoutInterceptor implements NestInterceptor { 7 | timeout: number 8 | 9 | constructor(timeout = 15000) { 10 | this.timeout = timeout 11 | } 12 | 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | // 超时处理 15 | return next.handle().pipe(timeout(this.timeout)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cms-api/src/middlewares/converter.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from '@nestjs/common' 2 | import { Request, Response } from 'express' 3 | 4 | @Injectable() 5 | export class BodyConverter implements NestMiddleware { 6 | use(req: Request, res: Response, next: Function) { 7 | // 记录请求开始时间 8 | res.locals.cost = Date.now() 9 | 10 | // 打印请求信息 11 | console.log('\n> 请求', req.path, req.params, req.body) 12 | 13 | // serverless-http 框架会将 string 类型的字符串转换成 stream 14 | // 将被转换成 stream 的 event.body 转换成对象 15 | if (Buffer.isBuffer(req.body)) { 16 | const body = req.body.toString() 17 | try { 18 | req.body = JSON.parse(body) 19 | } catch (error) { 20 | // ignore error 21 | } 22 | } 23 | 24 | // 打印请求信息 25 | console.log('请求', req.path, req.params, req.body) 26 | 27 | next() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/cms-api/src/services/cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Scope } from '@nestjs/common' 2 | 3 | interface CacheMap { 4 | currentSchema: Schema 5 | 6 | project: Project 7 | 8 | schemas: Schema[] 9 | 10 | connectTraverseCollections: string[] 11 | } 12 | 13 | // 针对单个请求的缓存 14 | @Injectable({ 15 | scope: Scope.REQUEST, 16 | }) 17 | export class LocalCacheService { 18 | private readonly cache: Map 19 | 20 | constructor() { 21 | this.cache = new Map() 22 | } 23 | 24 | public set(key: keyof CacheMap, value: CacheMap[T]) { 25 | this.cache.set(key, value) 26 | } 27 | 28 | public get(key: T): CacheMap[T] { 29 | return this.cache.get(key) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/cms-api/src/services/cloudbase.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { CloudBase } from '@cloudbase/node-sdk/lib/cloudbase' 3 | import { CollectionReference } from '@cloudbase/database' 4 | import { getCloudBaseApp } from '@/utils' 5 | 6 | @Injectable() 7 | export class CloudBaseService { 8 | app: CloudBase 9 | 10 | constructor() { 11 | this.app = getCloudBaseApp() 12 | } 13 | 14 | get db() { 15 | return this.app.database() 16 | } 17 | 18 | collection(collection: string): CollectionReference { 19 | return this.app.database().collection(collection) 20 | } 21 | 22 | auth() { 23 | return this.app.auth() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cms-api/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cloudbase.service' 2 | export * from './cache.service' 3 | -------------------------------------------------------------------------------- /packages/cms-api/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from '@/constants' 2 | import { getCloudBaseApp } from './cloudbase' 3 | 4 | export const isDateType = (type: string): boolean => type === 'Date' || type === 'DateTime' 5 | 6 | // 格式化 data 中的时间类型 7 | export const formatPayloadDate = async ( 8 | payload: Object | Object[], 9 | collectionName: string 10 | ): Promise => { 11 | const app = getCloudBaseApp() 12 | const { 13 | data: [schema], 14 | }: { data: Schema[] } = await app 15 | .database() 16 | .collection(Collection.Schemas) 17 | .where({ 18 | collectionName, 19 | }) 20 | .get() 21 | 22 | // Webhook 直接返回 23 | if (!schema) return payload 24 | 25 | const dateFields = schema.fields.filter( 26 | (field) => isDateType(field.type) && field.dateFormatType === 'date' 27 | ) 28 | 29 | // 不存在需要格式化的时间字段 30 | if (!dateFields.length) return payload 31 | 32 | if (Array.isArray(payload)) { 33 | return payload.map((record) => { 34 | dateFields.forEach((field) => { 35 | record[field.name] = new Date(record[field.name]) 36 | }) 37 | return record 38 | }) 39 | } 40 | 41 | dateFields.forEach((field) => { 42 | payload[field.name] = new Date(payload[field.name]) 43 | }) 44 | 45 | return payload 46 | } 47 | -------------------------------------------------------------------------------- /packages/cms-api/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cloudbase' 2 | export * from './tools' 3 | export * from './date' 4 | -------------------------------------------------------------------------------- /packages/cms-api/src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from 'nanoid' 2 | 3 | export const isDevEnv = () => 4 | process.env.NODE_ENV === 'development' && !process.env.TENCENTCLOUD_RUNENV 5 | 6 | export const nanoid = customAlphabet( 7 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-', 8 | 32 9 | ) 10 | -------------------------------------------------------------------------------- /packages/cms-api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/cms-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "outDir": "dist", 12 | "baseUrl": ".", 13 | "incremental": true, 14 | "paths": { 15 | "@/*": ["src/*"], 16 | "@modules/*": ["src/modules/*"], 17 | "@utils": ["src/utils"] 18 | } 19 | }, 20 | "include": ["src/**/*", "typings"], 21 | "exclude": ["node_modules", "dist"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/cms-api/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | 3 | declare global { 4 | export interface IResponse extends Response { 5 | locals: { 6 | currentSchema: Schema 7 | project: Project 8 | } 9 | } 10 | 11 | interface IRequest extends Request { 12 | handleService: string 13 | 14 | cmsUser: RequestUser 15 | 16 | accessToken: ApiAccessToken 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/.env: -------------------------------------------------------------------------------- 1 | # 服务配置 2 | SERVER_PORT=5003 3 | 4 | # Webhook 请求处理超时 5 | WEBHOOK_TIMEOUT=10000 6 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/.env.example: -------------------------------------------------------------------------------- 1 | # 云开发环境 Id 2 | TCB_ENVID= 3 | 4 | # 腾讯云 API 秘钥 5 | SECRETID= 6 | SECRETKEY= 7 | 8 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/.eslintignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /packages/cms-fx-openapi/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /packages/cms-fx-openapi/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const main = require('./dist/main') 3 | 4 | exports.tcbGetApp = main.bootstrap 5 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.15.0-alpine 2 | 3 | RUN apk --update add tzdata \ 4 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 5 | && echo "Asia/Shanghai" > /etc/timezone \ 6 | && apk del tzdata 7 | 8 | RUN mkdir -p /usr/src/app 9 | 10 | WORKDIR /usr/src/app 11 | 12 | # add npm package 13 | COPY package.json /usr/src/app/package.json 14 | COPY yarn.lock /usr/src/app/yarn.lock 15 | 16 | # RUN npm i --registry=https://registry.npm.taobao.org 17 | RUN yarn --registry=https://registry.npm.taobao.org 18 | 19 | # copy code 20 | COPY . /usr/src/app 21 | 22 | EXPOSE 5000 23 | 24 | CMD npm start 25 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const serverless = require('serverless-http') 3 | const entry = require('./app.js') 4 | 5 | module.exports.main = async (event, context) => { 6 | const { action, data, version } = event 7 | let app = entry 8 | 9 | if (!version) { 10 | return { 11 | error: { 12 | code: 'INVALID_PARAMS', 13 | message: 'The version param is required', 14 | }, 15 | } 16 | } 17 | 18 | // support for async load app 19 | if (entry && entry.tcbGetApp && typeof entry.tcbGetApp === 'function') { 20 | app = await entry.tcbGetApp() 21 | } 22 | 23 | // mock http call 24 | let res = await serverless(app, { 25 | // 无需判断返回值的类型 26 | binary: false, 27 | })( 28 | { 29 | body: data, 30 | headers: {}, 31 | httpMethod: 'POST', 32 | queryStringParameters: '', 33 | path: `/api/${action}`, 34 | }, 35 | context 36 | ) 37 | 38 | try { 39 | res = JSON.parse(res.body) 40 | } catch (error) { 41 | // ignore error 42 | console.log(error) 43 | } 44 | 45 | return res 46 | } 47 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudbase-cms-fx-openapi", 3 | "version": "2.13.19", 4 | "description": "CloudBase CMS open api service", 5 | "author": "cwuyiqing@gmail.com", 6 | "private": true, 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "start": "nest start", 11 | "dev": "cross-env NODE_ENV=development DEBUG=cms:* nest start --watch", 12 | "dev:wx": "cross-env CMS_RESOURCE_PREFIX=wx-ext-cms NODE_ENV=development nest start --watch", 13 | "debug": "nest start --debug --watch" 14 | }, 15 | "dependencies": { 16 | "@cloudbase/manager-node": "^3.9.0", 17 | "@cloudbase/node-sdk": "^2.5.0", 18 | "@nestjs/common": "^7.6.13", 19 | "@nestjs/config": "^0.6.3", 20 | "@nestjs/core": "^7.6.13", 21 | "@nestjs/platform-express": "^7.6.13", 22 | "axios": "^0.21.1", 23 | "class-transformer": "^0.4.0", 24 | "class-validator": "^0.13.1", 25 | "dayjs": "^1.10.4", 26 | "helmet": "^4.4.1", 27 | "lodash": "^4.17.21", 28 | "nanoid": "^3.1.20", 29 | "papaparse": "^5.3.0", 30 | "reflect-metadata": "^0.1.13", 31 | "rxjs": "^6.6.3", 32 | "serverless-http": "^2.7.0", 33 | "wx-server-sdk": "^2.7.2" 34 | }, 35 | "devDependencies": { 36 | "@nestjs/cli": "^7.5.4", 37 | "@nestjs/schematics": "^7.2.7", 38 | "@types/debug": "^4.1.5", 39 | "@types/express": "^4.17.11", 40 | "@types/lodash": "^4.14.168", 41 | "@types/node": "^14.14.31", 42 | "cross-env": "^7.0.3", 43 | "rimraf": "^3.0.2", 44 | "ts-loader": "^8.0.17", 45 | "ts-node": "^9.1.1", 46 | "tsconfig-paths": "^3.9.0", 47 | "typescript": "^4.1.5" 48 | }, 49 | "engines": { 50 | "node": ">=12.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/api/api.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ApiController } from './api.controller' 3 | import { ApiService } from './api.service' 4 | 5 | @Module({ 6 | controllers: [ApiController], 7 | providers: [ApiService], 8 | }) 9 | export class ApiModule {} 10 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post } from '@nestjs/common' 2 | import { AppService } from './app.service' 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | async getHello(): Promise { 10 | return this.appService.getHello() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, MiddlewareConsumer, NestModule, Scope } from '@nestjs/common' 2 | import { ConfigModule } from '@nestjs/config' 3 | import { AppController } from '@/app.controller' 4 | import { AppService } from '@/app.service' 5 | import { BodyConverter } from '@/middlewares/converter.middleware' 6 | import { ApiModule } from './api/api.module' 7 | import { GlobalModule } from './global.module' 8 | 9 | @Module({ 10 | imports: [ 11 | ApiModule, 12 | GlobalModule, 13 | ConfigModule.forRoot({ 14 | isGlobal: true, 15 | envFilePath: process.env.NODE_ENV === 'development' ? ['.env', '.env.local'] : '.env', 16 | }), 17 | ], 18 | controllers: [AppController], 19 | providers: [AppService], 20 | }) 21 | export class AppModule implements NestModule { 22 | configure(consumer: MiddlewareConsumer) { 23 | consumer.apply(BodyConverter).forRoutes('*') 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class AppService { 5 | async getHello(): Promise { 6 | return 'Hello World! Powered by Nest & CloudBase!' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error' 2 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/constants.ts: -------------------------------------------------------------------------------- 1 | // 云函数、数据库等资源命名前缀 2 | export const RESOURCE_PREFIX = process.env.CMS_RESOURCE_PREFIX || 'tcb-ext-cms' 3 | 4 | /** 5 | * 数据库 6 | */ 7 | export const Collection = { 8 | // 项目集合 9 | Projects: `${RESOURCE_PREFIX}-projects`, 10 | 11 | // 内容模型集合 12 | Schemas: `${RESOURCE_PREFIX}-schemas`, 13 | 14 | // Webhooks 集合 15 | Webhooks: `${RESOURCE_PREFIX}-webhooks`, 16 | 17 | // 系统设置 18 | Settings: `${RESOURCE_PREFIX}-settings`, 19 | 20 | // 用户集合 21 | Users: `${RESOURCE_PREFIX}-users`, 22 | 23 | // 定义的角色集合 24 | CustomUserRoles: `${RESOURCE_PREFIX}-user-roles`, 25 | 26 | // 数据导入导出的记录 27 | DataMigrateTasks: `${RESOURCE_PREFIX}-data-migrate`, 28 | 29 | // 短信活动 30 | MessageActivity: `${RESOURCE_PREFIX}-sms-activities`, 31 | 32 | // 发送短信记录 33 | MessageTasks: `${RESOURCE_PREFIX}-sms-tasks`, 34 | } 35 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/global.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common' 2 | import { CloudBaseService } from './services' 3 | 4 | @Global() 5 | @Module({ 6 | providers: [CloudBaseService], 7 | exports: [CloudBaseService], 8 | }) 9 | export class GlobalModule {} 10 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, Injectable } from '@nestjs/common' 2 | import { getWxCloudApp, isDevEnv } from '@/utils' 3 | 4 | // 校验用户是否登录,是否存在 5 | @Injectable() 6 | export class GlobalAuthGuard implements CanActivate { 7 | async canActivate(): Promise { 8 | if (isDevEnv()) { 9 | return true 10 | } 11 | 12 | const wxCloud = getWxCloudApp() 13 | const { SOURCE } = wxCloud.getWXContext() 14 | 15 | // 仅支持微信 HTTP API 调用 16 | if (SOURCE === 'wx_http') { 17 | return true 18 | } 19 | 20 | return false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard' 2 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/interceptors/timecost.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { tap } from 'rxjs/operators' 3 | import { Response } from 'express' 4 | import { Injectable, ExecutionContext, CallHandler, NestInterceptor } from '@nestjs/common' 5 | 6 | @Injectable() 7 | export class TimeCost implements NestInterceptor { 8 | intercept(context: ExecutionContext, next: CallHandler): Observable { 9 | return next.handle().pipe( 10 | tap(() => { 11 | const res = context.switchToHttp().getResponse() as Response 12 | // 计算请求耗时,并添加到 header 13 | const timeCost = Date.now() - res.locals.cost 14 | res.header('X-Request-Cost', `${timeCost}`) 15 | 16 | console.log(`> 请求处理耗时: ${timeCost} ms`) 17 | }) 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/interceptors/timeout.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { timeout } from 'rxjs/operators' 3 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common' 4 | 5 | @Injectable() 6 | export class TimeoutInterceptor implements NestInterceptor { 7 | timeout: number 8 | 9 | constructor(timeout = 600000) { 10 | this.timeout = timeout 11 | } 12 | 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | // 超时处理 15 | return next.handle().pipe(timeout(this.timeout)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/middlewares/converter.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from '@nestjs/common' 2 | import { Request, Response } from 'express' 3 | 4 | @Injectable() 5 | export class BodyConverter implements NestMiddleware { 6 | use(req: Request, res: Response, next: Function) { 7 | // 记录请求开始时间 8 | res.locals.cost = Date.now() 9 | 10 | // 打印请求信息 11 | console.log('\n> 请求', req.path, req.params, req.body) 12 | 13 | // serverless-http 框架会将 string 类型的字符串转换成 stream 14 | // 将被转换成 stream 的 event.body 转换成对象 15 | if (Buffer.isBuffer(req.body)) { 16 | const body = req.body.toString() 17 | try { 18 | req.body = JSON.parse(body) 19 | } catch (error) { 20 | // ignore error 21 | } 22 | } 23 | 24 | // 打印请求信息 25 | console.log('请求', req.path, req.params, req.body) 26 | 27 | next() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/services/cloudbase.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { CloudBase } from '@cloudbase/node-sdk' 3 | import { getCloudBaseApp } from '@/utils' 4 | 5 | @Injectable() 6 | export class CloudBaseService { 7 | app: CloudBase 8 | 9 | constructor() { 10 | this.app = getCloudBaseApp() 11 | } 12 | 13 | get db() { 14 | return this.app.database() 15 | } 16 | 17 | collection(collection: string) { 18 | return this.app.database().collection(collection) 19 | } 20 | 21 | auth() { 22 | return this.app.auth() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cloudbase.service' 2 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import 'dayjs/locale/zh-cn' 3 | dayjs.locale('zh-cn') 4 | 5 | /** 6 | * 获取当前时间的 unix timestamp 形式 7 | */ 8 | export const getUnixTimestamp = () => dayjs().unix() 9 | 10 | /** 11 | * 将时间转换成毫秒级的 unix timestamp Date.now() 12 | */ 13 | export const dateToUnixTimestampInMs = (date?: string) => { 14 | // 毫秒 15 | const unixTime = dayjs(date).valueOf() 16 | 17 | if (isNaN(unixTime)) { 18 | throw new Error(`Invalid Date Type: ${date}`) 19 | } 20 | 21 | return unixTime 22 | } 23 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cloudbase' 2 | export * from './tools' 3 | export * from './date' 4 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | export const isDevEnv = () => 4 | process.env.NODE_ENV === 'development' && !process.env.TENCENTCLOUD_RUNENV 5 | 6 | export const md5Base64 = (text: string) => crypto.createHash('md5').update(text).digest('base64') 7 | 8 | export const base64 = (text: string) => Buffer.from(text).toString('base64') 9 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "outDir": "dist", 12 | "baseUrl": ".", 13 | "incremental": true, 14 | "paths": { 15 | "@/*": ["src/*"], 16 | "@modules/*": ["src/modules/*"], 17 | "@utils": ["src/utils"] 18 | } 19 | }, 20 | "include": ["src/**/*", "typings"], 21 | "exclude": ["node_modules", "dist"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/cms-fx-openapi/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | NODE_ENV: 'development' | 'production' 4 | TCB_ENVID: string 5 | SECRETID: string 6 | SECRETKEY: string 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/cms-init/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # OS 12 | .DS_Store 13 | 14 | # Tests 15 | /coverage 16 | /.nyc_output 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | 34 | -------------------------------------------------------------------------------- /packages/cms-init/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudbase-cms-init", 3 | "version": "2.13.19", 4 | "description": "The function to init CloudBase content manager system", 5 | "main": "index.js", 6 | "private": true, 7 | "author": "cwuyiqing@gmail.com", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "dependencies": { 12 | "@cloudbase/manager-node": "3.11.0", 13 | "@cloudbase/node-sdk": "^2.4.0-beta", 14 | "lodash": "^4.17.20", 15 | "nanoid": "^3.1.12" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cms-openapi/.env: -------------------------------------------------------------------------------- 1 | # 服务配置 2 | SERVER_PORT=5003 3 | 4 | # 超时配置 5 | RES_TIMEOUT=60000 6 | 7 | # Webhook 请求处理超时 8 | WEBHOOK_TIMEOUT=10000 9 | -------------------------------------------------------------------------------- /packages/cms-openapi/.env.example: -------------------------------------------------------------------------------- 1 | # 云开发环境 Id 2 | TCB_ENVID= 3 | 4 | # 腾讯云 API 秘钥 5 | SECRETID= 6 | SECRETKEY= 7 | 8 | -------------------------------------------------------------------------------- /packages/cms-openapi/.eslintignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /packages/cms-openapi/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /packages/cms-openapi/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const main = require('./dist/main') 3 | 4 | exports.tcbGetApp = main.bootstrap 5 | -------------------------------------------------------------------------------- /packages/cms-openapi/dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.15.0-alpine 2 | 3 | RUN apk --update add tzdata \ 4 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 5 | && echo "Asia/Shanghai" > /etc/timezone \ 6 | && apk del tzdata 7 | 8 | RUN mkdir -p /usr/src/app 9 | 10 | WORKDIR /usr/src/app 11 | 12 | # add npm package 13 | COPY package.json /usr/src/app/package.json 14 | COPY yarn.lock /usr/src/app/yarn.lock 15 | 16 | # RUN npm i --registry=https://registry.npm.taobao.org 17 | RUN yarn --registry=https://registry.npm.taobao.org 18 | 19 | # copy code 20 | COPY . /usr/src/app 21 | 22 | EXPOSE 5000 23 | 24 | CMD npm start 25 | -------------------------------------------------------------------------------- /packages/cms-openapi/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const serverless = require('serverless-http') 3 | const entry = require('./app.js') 4 | 5 | module.exports.main = async (event, context) => { 6 | context.callbackWaitsForEmptyEventLoop = false 7 | let app = entry 8 | 9 | // 本次请求的 requestId 10 | // 云函数当前的一个并发实例在同一时刻仅处理一个事件 11 | process.env.CMS_REQUEST_ID = context.request_id 12 | 13 | // support for async load app 14 | if (entry && entry.tcbGetApp && typeof entry.tcbGetApp === 'function') { 15 | app = await entry.tcbGetApp() 16 | } 17 | 18 | const res = await serverless(app, { 19 | // 无需判断返回值的类型 20 | binary: false, 21 | })(event, context) 22 | 23 | // 使用 SDK 调用时,格式化返回的 Body,方便在浏览器中查看返回值 24 | if (process.env.TCB_SOURCE === 'web_client') { 25 | try { 26 | res.body = JSON.parse(res.body) 27 | } catch (error) { 28 | // ignore error 29 | } 30 | } 31 | 32 | return res 33 | } 34 | -------------------------------------------------------------------------------- /packages/cms-openapi/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /packages/cms-openapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudbase-cms-openapi", 3 | "version": "2.13.19", 4 | "description": "CloudBase content manager system service restful api", 5 | "author": "cwuyiqing@gmail.com", 6 | "private": true, 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "start": "nest start", 11 | "dev": "cross-env NODE_ENV=development DEBUG=cms:* nest start --watch", 12 | "debug": "nest start --debug --watch" 13 | }, 14 | "dependencies": { 15 | "@cloudbase/manager-node": "^3.9.0", 16 | "@cloudbase/node-sdk": "^2.5.0", 17 | "@nestjs/common": "^7.6.5", 18 | "@nestjs/config": "^0.6.2", 19 | "@nestjs/core": "^7.6.5", 20 | "@nestjs/platform-express": "^7.6.5", 21 | "axios": "^0.21.1", 22 | "class-transformer": "^0.3.2", 23 | "class-validator": "^0.13.1", 24 | "dayjs": "^1.10.4", 25 | "debug": "^4.3.1", 26 | "helmet": "^4.4.1", 27 | "lodash": "^4.17.20", 28 | "nanoid": "^3.1.20", 29 | "papaparse": "^5.3.0", 30 | "reflect-metadata": "^0.1.13", 31 | "rxjs": "^6.6.3", 32 | "serverless-http": "^2.7.0", 33 | "wx-server-sdk": "^2.7.2" 34 | }, 35 | "devDependencies": { 36 | "@nestjs/cli": "^7.5.4", 37 | "@nestjs/schematics": "^7.2.7", 38 | "@types/debug": "^4.1.5", 39 | "@types/express": "^4.17.11", 40 | "@types/lodash": "^4.14.168", 41 | "@types/node": "^14.14.22", 42 | "@typescript-eslint/eslint-plugin": "^4.14.0", 43 | "@typescript-eslint/parser": "^4.14.0", 44 | "cross-env": "^7.0.3", 45 | "eslint": "^7.18.0", 46 | "eslint-config-alloy": "^3.10.0", 47 | "rimraf": "^3.0.2", 48 | "ts-loader": "^8.0.14", 49 | "ts-node": "^9.1.1", 50 | "tsconfig-paths": "^3.9.0", 51 | "typescript": "^4.1.3" 52 | }, 53 | "engines": { 54 | "node": ">=12.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/api/api.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ApiController } from './api.controller' 3 | import { ApiService } from './api.service' 4 | 5 | @Module({ 6 | controllers: [ApiController], 7 | providers: [ApiService], 8 | }) 9 | export class ApiModule {} 10 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post } from '@nestjs/common' 2 | import { AppService } from './app.service' 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | async getHello(): Promise { 10 | return this.appService.getHello() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, MiddlewareConsumer, NestModule, Scope } from '@nestjs/common' 2 | import { ConfigModule } from '@nestjs/config' 3 | import { AppController } from '@/app.controller' 4 | import { AppService } from '@/app.service' 5 | import { BodyConverter } from '@/middlewares/converter.middleware' 6 | import { ApiModule } from './api/api.module' 7 | import { GlobalModule } from './global.module' 8 | 9 | @Module({ 10 | imports: [ 11 | ApiModule, 12 | GlobalModule, 13 | ConfigModule.forRoot({ 14 | isGlobal: true, 15 | envFilePath: process.env.NODE_ENV === 'development' ? ['.env', '.env.local'] : '.env', 16 | }), 17 | ], 18 | controllers: [AppController], 19 | providers: [AppService], 20 | }) 21 | export class AppModule implements NestModule { 22 | configure(consumer: MiddlewareConsumer) { 23 | consumer.apply(BodyConverter).forRoutes('*') 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class AppService { 5 | async getHello(): Promise { 6 | return 'Hello World! Powered by Nest & CloudBase!' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error' 2 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/global.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common' 2 | import { CloudBaseService, LocalCacheService } from './services' 3 | 4 | @Global() 5 | @Module({ 6 | providers: [CloudBaseService, LocalCacheService], 7 | exports: [CloudBaseService, LocalCacheService], 8 | }) 9 | export class GlobalModule {} 10 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Collection, SystemUserRoles, SYSTEM_ROLE_IDS } from '@/constants' 2 | import { getCloudBaseApp, getUserFromCredential, isDevEnv } from '@/utils' 3 | import { 4 | CanActivate, 5 | ExecutionContext, 6 | HttpException, 7 | HttpStatus, 8 | Injectable, 9 | } from '@nestjs/common' 10 | 11 | // 校验用户是否登录,是否存在 12 | @Injectable() 13 | export class GlobalAuthGuard implements CanActivate { 14 | async canActivate(context: ExecutionContext): Promise { 15 | const request = context.switchToHttp().getRequest() 16 | 17 | if (isDevEnv()) { 18 | return true 19 | } 20 | 21 | // 获取用户信息 22 | const app = getCloudBaseApp() 23 | let userInfo 24 | 25 | // 根据 credential 信息获取用户身份 26 | const { headers } = request 27 | const credentials = headers['x-cloudbase-credentials'] as string 28 | if (credentials) { 29 | // headers.origin 可能为空 30 | const origin = headers.origin || headers.host || 'http://127.0.0.1:8000' 31 | const user = await getUserFromCredential(credentials, origin) 32 | if (user) { 33 | userInfo = user 34 | } 35 | } 36 | 37 | // 未登录用户 38 | if (!userInfo?.username && !userInfo?.openId) { 39 | throw new HttpException( 40 | { 41 | code: 'NO_AUTH', 42 | message: '未登录用户', 43 | }, 44 | HttpStatus.FORBIDDEN 45 | ) 46 | } 47 | 48 | const { 49 | data: [userRecord], 50 | } = await app 51 | .database() 52 | .collection(Collection.Users) 53 | .where({ 54 | username: userInfo.username, 55 | }) 56 | .get() 57 | 58 | // 用户信息不存在 59 | if (!userRecord) { 60 | throw new HttpException( 61 | { 62 | error: { 63 | code: 'AUTH_EXPIRED', 64 | message: '用户不存在,请确认登录信息!', 65 | }, 66 | }, 67 | HttpStatus.FORBIDDEN 68 | ) 69 | } 70 | 71 | request.cmsUser = userRecord 72 | 73 | return true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard' 2 | export * from './role.guard' 3 | export * from './permission.guard' 4 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/interceptors/timecost.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { tap } from 'rxjs/operators' 3 | import { Response } from 'express' 4 | import { Injectable, ExecutionContext, CallHandler, NestInterceptor } from '@nestjs/common' 5 | 6 | @Injectable() 7 | export class TimeCost implements NestInterceptor { 8 | intercept(context: ExecutionContext, next: CallHandler): Observable { 9 | return next.handle().pipe( 10 | tap(() => { 11 | const res = context.switchToHttp().getResponse() as Response 12 | // 计算请求耗时,并添加到 header 13 | const timeCost = Date.now() - res.locals.cost 14 | res.header('X-Request-Cost', `${timeCost}`) 15 | 16 | console.log(`> 请求处理耗时: ${timeCost} ms`) 17 | }) 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/interceptors/timeout.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { timeout } from 'rxjs/operators' 3 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common' 4 | 5 | @Injectable() 6 | export class TimeoutInterceptor implements NestInterceptor { 7 | timeout: number 8 | 9 | constructor(timeout = 600000) { 10 | this.timeout = timeout 11 | } 12 | 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | // 超时处理 15 | return next.handle().pipe(timeout(this.timeout)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/middlewares/converter.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from '@nestjs/common' 2 | import { Request, Response } from 'express' 3 | 4 | @Injectable() 5 | export class BodyConverter implements NestMiddleware { 6 | use(req: Request, res: Response, next: Function) { 7 | // 记录请求开始时间 8 | res.locals.cost = Date.now() 9 | 10 | // 打印请求信息 11 | console.log('\n> 请求', req.path, req.params, req.body) 12 | 13 | // serverless-http 框架会将 string 类型的字符串转换成 stream 14 | // 将被转换成 stream 的 event.body 转换成对象 15 | if (Buffer.isBuffer(req.body)) { 16 | const body = req.body.toString() 17 | try { 18 | req.body = JSON.parse(body) 19 | } catch (error) { 20 | // ignore error 21 | } 22 | } 23 | 24 | // 打印请求信息 25 | console.log('请求', req.path, req.params, req.body) 26 | 27 | next() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/services/cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Scope } from '@nestjs/common' 2 | 3 | interface CacheMap { 4 | currentSchema: Schema 5 | 6 | project: Project 7 | 8 | schemas: Schema[] 9 | 10 | connectTraverseCollections: string[] 11 | } 12 | 13 | // 针对单个请求的缓存 14 | @Injectable({ 15 | scope: Scope.REQUEST, 16 | }) 17 | export class LocalCacheService { 18 | private readonly cache: Map 19 | 20 | constructor() { 21 | this.cache = new Map() 22 | } 23 | 24 | public set(key: keyof CacheMap, value: CacheMap[T]) { 25 | this.cache.set(key, value) 26 | } 27 | 28 | public get(key: T): CacheMap[T] { 29 | return this.cache.get(key) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/services/cloudbase.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { CloudBase } from '@cloudbase/node-sdk/lib/cloudbase' 3 | import { CollectionReference } from '@cloudbase/database' 4 | import { getCloudBaseApp } from '@/utils' 5 | 6 | @Injectable() 7 | export class CloudBaseService { 8 | app: CloudBase 9 | 10 | constructor() { 11 | this.app = getCloudBaseApp() 12 | } 13 | 14 | get db() { 15 | return this.app.database() 16 | } 17 | 18 | collection(collection: string): CollectionReference { 19 | return this.app.database().collection(collection) 20 | } 21 | 22 | auth() { 23 | return this.app.auth() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cloudbase.service' 2 | export * from './cache.service' 3 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import 'dayjs/locale/zh-cn' 3 | import utc from 'dayjs/plugin/utc' 4 | import timezone from 'dayjs/plugin/timezone' 5 | 6 | dayjs.extend(utc) 7 | dayjs.extend(timezone) 8 | 9 | dayjs.locale('zh-cn') 10 | dayjs.tz.setDefault('Asia/Shanghai') 11 | 12 | export const unixToDateString = (date: string | number) => 13 | dayjs(Number(date) * 1000) 14 | .tz('Asia/Shanghai') 15 | .format('YYYY-MM-DD HH:mm:ss') 16 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cloudbase' 2 | export * from './tools' 3 | export * from './date' 4 | -------------------------------------------------------------------------------- /packages/cms-openapi/src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import { customAlphabet } from 'nanoid' 3 | 4 | export const isDevEnv = () => 5 | process.env.NODE_ENV === 'development' && !process.env.TENCENTCLOUD_RUNENV 6 | 7 | export const nanoid = customAlphabet( 8 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-', 9 | 32 10 | ) 11 | 12 | export const md5Base64 = (text: string) => crypto.createHash('md5').update(text).digest('base64') 13 | 14 | export const base64 = (text: string) => Buffer.from(text).toString('base64') 15 | -------------------------------------------------------------------------------- /packages/cms-openapi/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/cms-openapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "outDir": "dist", 12 | "baseUrl": ".", 13 | "incremental": true, 14 | "paths": { 15 | "@/*": ["src/*"], 16 | "@modules/*": ["src/modules/*"], 17 | "@utils": ["src/utils"] 18 | } 19 | }, 20 | "include": ["src/**/*", "typings"], 21 | "exclude": ["node_modules", "dist"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/cms-openapi/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | 3 | declare global { 4 | export interface IResponse extends Response { 5 | locals: { 6 | currentSchema: Schema 7 | project: Project 8 | } 9 | } 10 | 11 | interface IRequest extends Request { 12 | handleService: string 13 | 14 | cmsUser: RequestUser 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/cms-openapi/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | NODE_ENV: 'development' | 'production' 4 | TCB_ENVID: string 5 | SECRETID: string 6 | SECRETKEY: string 7 | SMS_TEMPLATE_ID: string 8 | } 9 | } 10 | 11 | interface User { 12 | _id: string 13 | 14 | username: string 15 | 16 | // 创建时间 17 | createTime: number 18 | 19 | // 用户角色 20 | roles: string[] 21 | 22 | // cloudbase uuid 23 | uuid: string 24 | } 25 | 26 | interface UserRole { 27 | _id: string 28 | 29 | // 角色名 30 | roleName: string 31 | 32 | // 角色描述 33 | description: string 34 | 35 | // 角色绑定的权限描述 36 | permissions: Permission[] 37 | 38 | type: string | 'system' 39 | } 40 | 41 | /** 42 | * 限制 43 | * 项目 ID 为 * 时,资源必然为 * 44 | * 服务未 * 时,资源必然为 * 45 | */ 46 | interface Permission { 47 | // 项目 48 | projectId: '*' | string 49 | 50 | // 行为 51 | action: string[] | ['*'] 52 | 53 | // TODO: 允许访问/拒绝访问 54 | effect: 'allow' | 'deny' 55 | 56 | // 服务 57 | // 一个权限规则仅支持一个 service 58 | service: string | '*' 59 | 60 | // 具体资源 61 | resource: string[] | ['*'] 62 | } 63 | 64 | interface RequestUser extends User { 65 | // 用户可以访问的资源 66 | projectResource?: { 67 | [key: string]: '*' | string[] 68 | } 69 | 70 | // 所有可访问的服务 71 | accessibleService?: '*' | string[] 72 | 73 | // 系统管理员 74 | isAdmin?: boolean 75 | 76 | // 项目管理员 77 | isProjectAdmin?: boolean 78 | 79 | // 用户关联的角色信息 80 | userRoles?: UserRole[] 81 | } 82 | -------------------------------------------------------------------------------- /packages/cms-sms-page/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.2% 2 | last 4 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /packages/cms-sms-page/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /packages/cms-sms-page/README.md: -------------------------------------------------------------------------------- 1 | # 短信跳转页面 2 | -------------------------------------------------------------------------------- /packages/cms-sms-page/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/cms-sms-page/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sms-page", 3 | "version": "2.13.19", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service serve", 7 | "dev:wx": "WX_MP=true vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "build:wx": "WX_MP=true vue-cli-service build" 10 | }, 11 | "dependencies": { 12 | "core-js": "^3.12.1", 13 | "vue": "^2.6.12" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "^4.5.13", 17 | "@vue/cli-service": "^4.5.13", 18 | "less": "^3.13.1", 19 | "less-loader": "^5.0.0", 20 | "vue-template-compiler": "^2.6.12" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cms-sms-page/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 打开{{APPNAME}} 7 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 35 | 36 | 37 | 38 | 45 |
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /packages/cms-sms-page/src/components/DesktopWeb.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/cms-sms-page/src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | 18 | 51 | -------------------------------------------------------------------------------- /packages/cms-sms-page/src/components/PublicWeb.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 45 | 46 | 70 | -------------------------------------------------------------------------------- /packages/cms-sms-page/src/components/WeDialog.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /packages/cms-sms-page/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | Vue.config.ignoredElements = [/^wx/] 6 | 7 | console.log(process.env.NODE_ENV) 8 | 9 | // 仅在开发模式下替换 cloudResource 10 | if (process.env.VUE_APP_CLOUDRESOURCE && process.env.NODE_ENV === 'development') { 11 | try { 12 | const cloudResource = JSON.parse(process.env.VUE_APP_CLOUDRESOURCE) 13 | window.cloudResource = cloudResource 14 | } catch (error) {} 15 | } 16 | 17 | new Vue({ 18 | render: (h) => h(App), 19 | }).$mount('#app') 20 | -------------------------------------------------------------------------------- /packages/cms-sms-page/vue.config.js: -------------------------------------------------------------------------------- 1 | const { DefinePlugin } = require('webpack') 2 | 3 | // vue.config.js 4 | const isDev = process.env.NODE_ENV === 'development' 5 | 6 | const activityPath = process.env.WX_MP ? 'cms-activities' : 'tcb-cms-activities' 7 | 8 | module.exports = { 9 | publicPath: isDev ? '/' : `/${activityPath}/`, 10 | css: { 11 | extract: false, 12 | }, 13 | // 打包时不生成.map文件 14 | productionSourceMap: false, 15 | configureWebpack: { 16 | plugins: [ 17 | new DefinePlugin({ 18 | WX_MP: JSON.stringify(process.env.WX_MP), 19 | }), 20 | ], 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /packages/cms-sms/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const cloud = require('wx-server-sdk') 3 | 4 | /** 5 | * 获取 app 基本信息 6 | */ 7 | async function getAppBasicInfo() { 8 | try { 9 | const res = await cloud.openapi.auth.getBasicInfo() 10 | console.log('小程序信息', res) 11 | return res 12 | } catch (e) { 13 | return { 14 | error: { 15 | message: e.message, 16 | code: e.errCode, 17 | }, 18 | } 19 | } 20 | } 21 | 22 | module.exports = { 23 | getAppBasicInfo, 24 | } 25 | -------------------------------------------------------------------------------- /packages/cms-sms/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const cloud = require('wx-server-sdk') 3 | 4 | const { getUrlScheme } = require('./url') 5 | 6 | exports.main = async (event = {}, context) => { 7 | const { taskId, action } = event 8 | const { ENV } = cloud.getWXContext() 9 | 10 | cloud.init({ 11 | env: ENV, 12 | }) 13 | 14 | // 生成 url schema 15 | if (action === 'getUrlScheme') { 16 | return getUrlScheme(event) 17 | } 18 | 19 | return { 20 | code: 'DEPRECATED', 21 | message: '服务未找到', 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/cms-sms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudbase-cms-sms", 3 | "version": "2.13.19", 4 | "description": "短信下发模块", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "dependencies": { 12 | "wx-server-sdk": "^2.7.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/cms-sms/report.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const crypto = require('crypto') 3 | const cloud = require('wx-server-sdk') 4 | 5 | /** 6 | * 上报短信下发任务 7 | */ 8 | async function reportMessageTask(event = {}) { 9 | const { taskId, phoneCount, activityId } = event 10 | 11 | const { ENV } = cloud.getWXContext() 12 | await cloud.openapi({ convertCase: false }).cloudbase.report({ 13 | reportAction: 'sendSmsTask', // 下发短信上报 14 | activityId, // 活动 ID 15 | taskId, // 任务 ID 16 | phoneCount, // 手机数量 17 | envId: ENV, // 环境 ID 18 | }) 19 | } 20 | 21 | const hashNode = (val) => crypto.createHash('sha256').update(val).digest('hex') 22 | 23 | const base64 = (v) => Buffer.from(v).toString('base64') 24 | 25 | /** 26 | * 上报 H5 页面访问数据 27 | */ 28 | async function reportUserView(event = {}) { 29 | const { ENV } = cloud.getWXContext() 30 | let { activityId, channelId, sessionId, referer } = event 31 | const clientIP = process.env.WX_CLIENTIP || process.env.WX_CLIENTIPV6 || '127.0.0.1' 32 | 33 | if (!sessionId) { 34 | sessionId = hashNode(clientIP).slice(0, 36) 35 | } 36 | 37 | // 记录 IP 地址 38 | sessionId += `-${base64(clientIP)}` 39 | console.log('IP 地址', clientIP, sessionId) 40 | 41 | const result = await cloud.openapi({ convertCase: false }).cloudbase.report({ 42 | referer, // 访问 referer 43 | activityId, // 活动 ID 44 | channelId, // 渠道 ID 45 | sessionId, // 用户访问ID 46 | envId: ENV, // 环境 ID 47 | reportAction: 'openH5', // 开发 H5 上报 48 | }) 49 | 50 | console.log(result) 51 | } 52 | 53 | module.exports = { 54 | reportUserView, 55 | reportMessageTask, 56 | } 57 | -------------------------------------------------------------------------------- /packages/service/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | dist 3 | node_modules 4 | npm-debug.log 5 | .env.local -------------------------------------------------------------------------------- /packages/service/.env.example: -------------------------------------------------------------------------------- 1 | TCB_ENVID= 2 | SECRETID= 3 | SECRETKEY= 4 | -------------------------------------------------------------------------------- /packages/service/.eslintignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /packages/service/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /packages/service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.15.0-alpine 2 | 3 | RUN apk --update add tzdata \ 4 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 5 | && echo "Asia/Shanghai" > /etc/timezone \ 6 | && apk del tzdata 7 | 8 | RUN mkdir -p /usr/src/app 9 | 10 | WORKDIR /usr/src/app 11 | 12 | COPY package.json /usr/src/app/package.json 13 | COPY yarn.lock /usr/src/app/yarn.lock 14 | 15 | RUN yarn --registry=https://mirrors.cloud.tencent.com/npm/ && yarn 16 | 17 | COPY . . 18 | 19 | ENV NODE_ENV production 20 | 21 | RUN yarn build 22 | 23 | EXPOSE 5000 24 | 25 | CMD npm start 26 | -------------------------------------------------------------------------------- /packages/service/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const main = require('./dist/main') 3 | 4 | exports.tcbGetApp = main.bootstrap 5 | -------------------------------------------------------------------------------- /packages/service/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": ["cloudbase.sendSms"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/service/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const serverless = require('serverless-http') 3 | const entry = require('./app.js') 4 | 5 | 6 | module.exports.main = async (event, context) => { 7 | context.callbackWaitsForEmptyEventLoop = false 8 | let app = entry 9 | 10 | // 本次请求的 requestId 11 | 12 | // support for async load app 13 | if (entry && entry.tcbGetApp && typeof entry.tcbGetApp === 'function') { 14 | app = await entry.tcbGetApp() 15 | } 16 | 17 | const res = await serverless(app, { 18 | // 无需判断返回值的类型 19 | binary: false, 20 | })(event, context) 21 | 22 | // 使用 SDK 调用时,格式化返回的 Body,方便在浏览器中查看返回值 23 | if (process.env.TCB_SOURCE === 'web_client') { 24 | try { 25 | res.body = JSON.parse(res.body) 26 | } catch (error) { 27 | // ignore error 28 | } 29 | } 30 | 31 | return res 32 | } 33 | -------------------------------------------------------------------------------- /packages/service/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["**/*.html"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudbase-cms-service", 3 | "version": "2.13.19", 4 | "description": "CloudBase content manager system service", 5 | "author": "cwuyiqing@gmail.com", 6 | "private": true, 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "start": "node dist/main.js", 11 | "nest:start": "nest start", 12 | "dev": "cross-env NODE_ENV=development DEBUG=cms:* nest start --watch", 13 | "dev:wx": "cross-env WX=true CMS_RESOURCE_PREFIX=wx-ext-cms NODE_ENV=development nest start --watch", 14 | "debug": "nest start --debug --watch" 15 | }, 16 | "dependencies": { 17 | "@cloudbase/cloud-api": "^0.4.0", 18 | "@cloudbase/manager-node": "^3.8.0", 19 | "@cloudbase/node-sdk": "^2.4.6-beta", 20 | "@nestjs/common": "^7.6.15", 21 | "@nestjs/config": "^0.5.0", 22 | "@nestjs/core": "^7.3.2", 23 | "@nestjs/platform-express": "^7.3.2", 24 | "axios": "^0.21.1", 25 | "class-transformer": "^0.3.1", 26 | "class-validator": "^0.12.2", 27 | "cos-nodejs-sdk-v5": "2.8.6", 28 | "dayjs": "^1.8.31", 29 | "extract-zip": "^2.0.1", 30 | "helmet": "^3.23.3", 31 | "lodash": "^4.17.21", 32 | "nanoid": "^3.1.10", 33 | "ramda": "^0.27.1", 34 | "reflect-metadata": "^0.1.13", 35 | "rxjs": "^6.6.0", 36 | "serverless-http": "^2.5.0" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/cli": "^7.6.0", 40 | "@nestjs/schematics": "^7.0.1", 41 | "@types/express": "^4.17.7", 42 | "@types/lodash": "^4.14.168", 43 | "@types/node": "^14.0.23", 44 | "@types/ramda": "^0.27.32", 45 | "@typescript-eslint/eslint-plugin": "^3.6.1", 46 | "@typescript-eslint/parser": "^3.6.1", 47 | "cross-env": "^7.0.2", 48 | "eslint": "^7.4.0", 49 | "eslint-config-alloy": "^3.7.3", 50 | "rimraf": "^3.0.2", 51 | "ts-loader": "^8.0.0", 52 | "ts-node": "^8.10.2", 53 | "tsconfig-paths": "^3.9.0", 54 | "typescript": "^3.9.6" 55 | }, 56 | "engines": { 57 | "node": ">=10.0.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/service/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common' 2 | import { ConfigModule } from '@nestjs/config' 3 | import { BodySerialize } from '@/middlewares/converter.middleware' 4 | import { ProjectsModule } from './modules/projects/projects.module' 5 | import { UserModule } from './modules/user/user.module' 6 | import { RoleModule } from './modules/role/role.module' 7 | import { SettingModule } from './modules/setting/setting.module' 8 | import { ApisModule } from './modules/apis/apis.module' 9 | import { GlobalModule } from './global.module' 10 | 11 | @Module({ 12 | imports: [ 13 | ApisModule, 14 | GlobalModule, 15 | UserModule, 16 | ProjectsModule, 17 | ConfigModule.forRoot({ 18 | isGlobal: true, 19 | envFilePath: 'WX' in process.env ? '.env.wx.local' : '.env.local', 20 | }), 21 | RoleModule, 22 | SettingModule, 23 | ], 24 | controllers: [], 25 | providers: [], 26 | }) 27 | export class AppModule implements NestModule { 28 | configure(consumer: MiddlewareConsumer) { 29 | consumer.apply(BodySerialize).forRoutes('*') 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/service/src/common/decorators.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common' 2 | 3 | export const Roles = (...roles: string[]) => SetMetadata('roles', roles) 4 | -------------------------------------------------------------------------------- /packages/service/src/common/field.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 系统默认字段 3 | */ 4 | export const SYSTEM_FIELDS: any[] = [ 5 | { 6 | displayName: '创建时间', 7 | id: '_createTime', 8 | name: '_createTime', 9 | type: 'DateTime', 10 | isSystem: true, 11 | dateFormatType: 'timestamp-ms', 12 | description: 'CMS 系统字段,请勿随意修改。通过 CMS 系统录入的数据会默认添加该字段', 13 | }, 14 | { 15 | displayName: '修改时间', 16 | id: '_updateTime', 17 | name: '_updateTime', 18 | type: 'DateTime', 19 | isSystem: true, 20 | dateFormatType: 'timestamp-ms', 21 | description: 'CMS 系统字段,请勿随意修改。通过 CMS 系统录入的数据会默认添加该字段', 22 | }, 23 | ] 24 | 25 | // 字段排序,数字越大,越靠后 26 | const SYSTEM_FIELD_ORDER = { 27 | _createTime: 1, 28 | _updateTime: 2, 29 | } 30 | 31 | const fieldOrder = (field: SchemaField) => { 32 | return SYSTEM_FIELD_ORDER[field.name] || 0 33 | } 34 | 35 | const SchemaCustomFieldKeys = ['docCreateTimeField', 'docUpdateTimeField'] 36 | 37 | // 获取 Schema 中的系统字段,并排序 38 | export const getSchemaSystemFields = (schema: Schema) => { 39 | const fields = schema?.fields 40 | if (!fields?.length) return SYSTEM_FIELDS 41 | 42 | // schema 中包含的系统字段 43 | const systemFieldsInSchema = fields.filter((_) => _.isSystem) 44 | 45 | SYSTEM_FIELDS.forEach((field) => { 46 | if ( 47 | !systemFieldsInSchema.find( 48 | (_) => _.name === field.name || SchemaCustomFieldKeys.some((key) => _.name === schema[key]) 49 | ) 50 | ) { 51 | systemFieldsInSchema.push(field) 52 | } 53 | }) 54 | 55 | return systemFieldsInSchema.sort((prev, next) => { 56 | return fieldOrder(prev) - fieldOrder(next) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /packages/service/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error' 2 | export * from './field' 3 | -------------------------------------------------------------------------------- /packages/service/src/config/index.ts: -------------------------------------------------------------------------------- 1 | // system config 2 | export default { 3 | globalPrefix: '/api/v1.0', 4 | 5 | // 请求处理超时时间 6 | timeout: 15000, 7 | 8 | // Webhook 请求处理超时时间 9 | webhookTimeout: 10000, 10 | } 11 | -------------------------------------------------------------------------------- /packages/service/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './permission.decorator' 2 | -------------------------------------------------------------------------------- /packages/service/src/decorators/permission.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common' 2 | 3 | export const Roles = (...roles: string[]) => SetMetadata('Roles', roles) 4 | 5 | /** 6 | * 通用 API Service 权限 7 | * modules apis 8 | */ 9 | export const API_METADATA_KEY = 'API_SERVICE_ROLE' 10 | 11 | export const ApiServiceRole = (...roles: string[]) => SetMetadata('API_SERVICE_ROLE', roles) 12 | -------------------------------------------------------------------------------- /packages/service/src/exceptions.filter.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express' 2 | import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common' 3 | 4 | interface NestResponse { 5 | statusCode: number 6 | message: string 7 | error: string 8 | } 9 | 10 | interface SystemResponse { 11 | error: { 12 | code: string 13 | message: string 14 | } 15 | } 16 | 17 | @Catch() 18 | export class AllExceptionsFilter implements ExceptionFilter { 19 | catch(exception: HttpException, host: ArgumentsHost) { 20 | const ctx = host.switchToHttp() 21 | const response = ctx.getResponse() 22 | const request = ctx.getRequest() 23 | 24 | console.error(exception) 25 | 26 | try { 27 | const httpRes = exception?.getResponse?.() as NestResponse & SystemResponse 28 | 29 | const status = 30 | exception instanceof HttpException 31 | ? exception.getStatus() 32 | : HttpStatus.INTERNAL_SERVER_ERROR 33 | 34 | let error = { 35 | code: '', 36 | message: '', 37 | } 38 | 39 | if (httpRes?.statusCode) { 40 | error.code = httpRes.statusCode.toString() 41 | error.message = `[${httpRes.error}] ${httpRes.message}` 42 | } else if (httpRes?.error) { 43 | error = httpRes.error 44 | } else { 45 | console.error('服务异常,响应:', httpRes || {}) 46 | 47 | error = { 48 | code: 'SYS_ERR', 49 | message: exception?.message || '服务异常', 50 | } 51 | } 52 | 53 | response.status(status).json({ 54 | error: { 55 | ...error, 56 | path: request.url, 57 | }, 58 | helpText: '异常', 59 | }) 60 | } catch (e) { 61 | // 解析错误异常 62 | console.error('系统错误', e || {}) 63 | 64 | response.status(500).json({ 65 | error: { 66 | code: 'SYS_ERR', 67 | message: '服务异常', 68 | path: request.url, 69 | }, 70 | helpText: '异常', 71 | }) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/service/src/global.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common' 2 | import { CloudBaseService, LocalCacheService, SchemaCacheService } from './services' 3 | 4 | @Global() 5 | @Module({ 6 | providers: [CloudBaseService, LocalCacheService, SchemaCacheService], 7 | exports: [CloudBaseService, LocalCacheService, SchemaCacheService], 8 | }) 9 | export class GlobalModule {} 10 | -------------------------------------------------------------------------------- /packages/service/src/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard' 2 | export * from './permission.guard' 3 | -------------------------------------------------------------------------------- /packages/service/src/interceptors/timecost.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { tap } from 'rxjs/operators' 3 | import { Response } from 'express' 4 | import { Injectable, ExecutionContext, CallHandler, NestInterceptor } from '@nestjs/common' 5 | 6 | @Injectable() 7 | export class TimeCost implements NestInterceptor { 8 | intercept(context: ExecutionContext, next: CallHandler): Observable { 9 | return next.handle().pipe( 10 | tap(() => { 11 | const res = context.switchToHttp().getResponse() as Response 12 | // 计算请求耗时,并添加到 header 13 | const timeCost = Date.now() - res.locals.cost 14 | res.header('x-request-cost', `${timeCost}`) 15 | 16 | console.info(`请求处理耗时: ${timeCost} ms`) 17 | }) 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/service/src/interceptors/timeout.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { timeout } from 'rxjs/operators' 3 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common' 4 | import config from '@/config' 5 | 6 | @Injectable() 7 | export class TimeoutInterceptor implements NestInterceptor { 8 | timeout: number 9 | 10 | constructor(timeout = config.timeout) { 11 | this.timeout = timeout 12 | } 13 | 14 | intercept(context: ExecutionContext, next: CallHandler): Observable { 15 | // 超时处理 16 | return next.handle().pipe(timeout(this.timeout)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/service/src/middlewares/converter.middleware.ts: -------------------------------------------------------------------------------- 1 | import { randomId } from '@/utils' 2 | import { Request, Response } from 'express' 3 | import { Injectable, NestMiddleware } from '@nestjs/common' 4 | 5 | @Injectable() 6 | export class BodySerialize implements NestMiddleware { 7 | use(req: Request, res: Response, next: Function) { 8 | // 将 seqId 添加到 header 中 9 | res.header('x-seqid', randomId(16).toString()) 10 | // 记录请求开始时间 11 | res.locals.cost = Date.now() 12 | 13 | // serverless-http 框架会将 string 类型的字符串转换成 stream 14 | // 将被转换成 stream 的 event.body 转换成对象 15 | if (Buffer.isBuffer(req.body)) { 16 | const body = req.body.toString() 17 | try { 18 | req.body = JSON.parse(body) 19 | } catch (error) { 20 | // ignore error 21 | } 22 | } 23 | 24 | // 打印请求信息 25 | console.info(`${req.method} ${req.originalUrl}`) 26 | console.info('请求 Body', req.body) 27 | 28 | next() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/service/src/modules/apis/apis.controller.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { Reflector } from '@nestjs/core' 3 | import { Get, Post, Body, Controller, Req } from '@nestjs/common' 4 | import { IsNotEmpty } from 'class-validator' 5 | import { API_METADATA_KEY } from '@/decorators' 6 | import { CmsException, UnauthorizedOperation } from '@/common' 7 | import { checkRole } from '@/utils' 8 | import { UtilService } from './util.service' 9 | import { AuthService } from './auth.service' 10 | 11 | type Service = 'util' | 'auth' 12 | 13 | type Action = keyof UtilService | keyof AuthService 14 | 15 | class RequestBody { 16 | // 合法的 service 17 | service: Service 18 | 19 | // 操作 20 | @IsNotEmpty() 21 | action: Action 22 | } 23 | 24 | @Controller() 25 | export class ApisController { 26 | constructor( 27 | private reflector: Reflector, 28 | private readonly util: UtilService, 29 | private readonly auth: AuthService 30 | ) {} 31 | 32 | @Get() 33 | async getHello(): Promise { 34 | return 'Hello World! Powered by Nest & CloudBase!' 35 | } 36 | 37 | @Post() 38 | async handleServiceActions(@Req() request: IRequest, @Body() body: RequestBody) { 39 | const { service, action } = body 40 | 41 | console.log('Service 处理', service, action) 42 | 43 | const validServices = Object.keys(this) 44 | 45 | if (!validServices.includes(service)) { 46 | throw new CmsException('INVALID_SERVICE', '非法的 Service') 47 | } 48 | 49 | const data = _.omit(body, 'service', 'action') 50 | 51 | // 通过 service 和 action 调用方法 52 | const needRoles = this.reflector.get(API_METADATA_KEY, this[service][action]) 53 | 54 | const allow = checkRole(request, needRoles || []) 55 | 56 | if (!allow) { 57 | throw new UnauthorizedOperation() 58 | } 59 | 60 | return this[service][action](data) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/service/src/modules/apis/apis.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ApisController } from './apis.controller' 3 | import { ApisService } from './apis.service' 4 | import { FileModule } from '../file/file.module' 5 | import { UtilService } from './util.service' 6 | import { AuthService } from './auth.service' 7 | 8 | @Module({ 9 | controllers: [ApisController], 10 | providers: [ApisService, UtilService, AuthService], 11 | imports: [FileModule], 12 | }) 13 | export class ApisModule {} 14 | -------------------------------------------------------------------------------- /packages/service/src/modules/apis/apis.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class ApisService {} 5 | -------------------------------------------------------------------------------- /packages/service/src/modules/apis/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Scope } from '@nestjs/common' 2 | import { REQUEST } from '@nestjs/core' 3 | 4 | @Injectable({ scope: Scope.REQUEST }) 5 | export class AuthService { 6 | constructor(@Inject(REQUEST) private request: IRequest) {} 7 | 8 | async getCurrentUser() { 9 | const { cmsUser } = this.request 10 | return cmsUser 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/service/src/modules/apis/util.service.ts: -------------------------------------------------------------------------------- 1 | import { RecordNotExistException } from '@/common' 2 | import { Collection } from '@/constants' 3 | import { CloudBaseService } from '@/services' 4 | import { getCollectionSchema } from '@/utils' 5 | import { Injectable } from '@nestjs/common' 6 | 7 | @Injectable() 8 | export class UtilService { 9 | constructor(private readonly cloudbaseService: CloudBaseService) {} 10 | 11 | // 根据 collectionName 查询 collection 信息 12 | async getCollectionInfo(body: any) { 13 | const { collectionName, customId } = body 14 | 15 | // 查询项目信息 16 | const { 17 | data: [project], 18 | } = await this.cloudbaseService 19 | .collection(Collection.Projects) 20 | .where({ 21 | customId, 22 | }) 23 | .get() 24 | 25 | let schema 26 | 27 | // 如果有 collectionName,也查询集合信息 28 | if (collectionName) { 29 | schema = await getCollectionSchema(collectionName) 30 | if (!schema) { 31 | throw new RecordNotExistException('数据集合不存在') 32 | } 33 | } 34 | 35 | return { 36 | data: { 37 | project, 38 | schema, 39 | }, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/service/src/modules/file/file.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | UseInterceptors, 4 | Post, 5 | UploadedFiles, 6 | HttpCode, 7 | UseGuards, 8 | Body, 9 | } from '@nestjs/common' 10 | import { AnyFilesInterceptor } from '@nestjs/platform-express' 11 | import { PermissionGuard } from '@/guards' 12 | import { FileService } from './file.service' 13 | import { IsNotEmpty } from 'class-validator' 14 | 15 | class UploadFile { 16 | @IsNotEmpty() 17 | filePath: string 18 | } 19 | 20 | @UseGuards(PermissionGuard('content')) 21 | @Controller('upload') 22 | export class FileController { 23 | constructor(private fileService: FileService) {} 24 | 25 | // 上传文件 26 | // 返回 HTTP Code 要为 200,POST 默认的 201 前端不识别 27 | @Post() 28 | @HttpCode(200) 29 | @UseInterceptors(AnyFilesInterceptor()) 30 | async createFile(@UploadedFiles() _files: IFile[]) { 31 | let files = _files 32 | 33 | // 上传文件 34 | const jobs = files.map((file) => { 35 | return this.fileService.upload(file) 36 | }) 37 | const data = await Promise.all(jobs) 38 | 39 | // 返回链接 40 | const result = await this.fileService.getUrl(data) 41 | return result 42 | } 43 | 44 | /** 45 | * 上传文件到静态托管 46 | */ 47 | @Post('hosting') 48 | @HttpCode(200) 49 | @UseInterceptors(AnyFilesInterceptor()) 50 | async uploadFile(@UploadedFiles() files: IFile[], @Body() payload: UploadFile) { 51 | // 处理多个文件 52 | // 处理多个文件 53 | const jobs = files.map((file) => { 54 | return this.fileService.uploadFileToHosting(file, payload.filePath) 55 | }) 56 | 57 | const data = await Promise.all(jobs) 58 | 59 | return data 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/service/src/modules/file/file.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { FileController } from './file.controller' 3 | import { FileService } from './file.service' 4 | 5 | @Module({ 6 | controllers: [FileController], 7 | providers: [FileService], 8 | }) 9 | export class FileModule {} 10 | -------------------------------------------------------------------------------- /packages/service/src/modules/projects/operation/template/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TencentCloudBase/cloudbase-extension-cms/56c83a246b68561683839ea529423b70fe32764f/packages/service/src/modules/projects/operation/template/.gitkeep -------------------------------------------------------------------------------- /packages/service/src/modules/projects/operation/template/raw.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 打开小程序 7 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 35 | 36 | 37 | 38 | 45 |
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /packages/service/src/modules/projects/projects.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, Module } from '@nestjs/common' 2 | import { ProjectsController } from './projects.controller' 3 | import { SchemasService } from './schemas/schema.service' 4 | import { WebhooksService } from './webhooks/webhooks.service' 5 | import { SchemasController } from './schemas/schema.controller' 6 | import { WebhooksController } from './webhooks/webhooks.controller' 7 | import { ContentsService } from './contents/contents.service' 8 | import { ContentsController } from './contents/contents.controller' 9 | import { MigrateController } from './migrate/migrate.controller' 10 | import { ProjectsService } from './projects.service' 11 | import { OperationController } from './operation/operation.controller' 12 | import { OperationService } from './operation/operation.service' 13 | 14 | @Module({ 15 | imports: [HttpModule], 16 | controllers: [ 17 | SchemasController, 18 | WebhooksController, 19 | ContentsController, 20 | ProjectsController, 21 | MigrateController, 22 | OperationController, 23 | ], 24 | providers: [SchemasService, ContentsService, OperationService, WebhooksService, ProjectsService], 25 | }) 26 | export class ProjectsModule {} 27 | -------------------------------------------------------------------------------- /packages/service/src/modules/projects/projects.service.ts: -------------------------------------------------------------------------------- 1 | import { RecordExistException } from '@/common' 2 | import { Functions } from '@/constants' 3 | import { getCloudBaseManager, isRunInContainer, isRunInServerMode } from '@/utils' 4 | import { Injectable } from '@nestjs/common' 5 | 6 | @Injectable() 7 | export class ProjectsService { 8 | async deleteApiAccessPath(path: string) { 9 | const manager = await getCloudBaseManager() 10 | // 查询 apiId 11 | const { 12 | APISet: [accessPath], 13 | } = await manager.access.getAccessList({ 14 | path, 15 | }) 16 | 17 | // API 可能已被删除 18 | if (!accessPath) return 19 | 20 | // 根据 apiId 删除 21 | await manager.access.deleteAccess({ 22 | apiId: accessPath.APIId, 23 | }) 24 | } 25 | 26 | async createApiAccessPath(path: string) { 27 | const manager = await getCloudBaseManager() 28 | 29 | // 查询 path 是否已经绑定了其他的云函数/云托管服务 30 | const { 31 | APISet: [accessPath], 32 | } = await manager.access.getAccessList({ 33 | path, 34 | }) 35 | 36 | if (accessPath && accessPath.Name !== Functions.API) { 37 | throw new RecordExistException('此路径已被其他服务绑定,请更换路径后重试') 38 | } 39 | 40 | // 路径未被占用 41 | try { 42 | await manager.access.createAccess({ 43 | path, 44 | name: Functions.API, 45 | }) 46 | } catch (e) { 47 | if (e.code === 'InvalidParameter.APICreated') { 48 | // ignore 49 | } else { 50 | throw e 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/service/src/modules/projects/schemas/schema.pipe.ts: -------------------------------------------------------------------------------- 1 | import { dateToUnixTimestampInMs, randomId } from '@/utils' 2 | import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common' 3 | 4 | @Injectable() 5 | export class SchemaTransfromPipe implements PipeTransform { 6 | constructor(private readonly action: 'create' | 'update') {} 7 | 8 | transform(value: any, metadata: ArgumentMetadata) { 9 | const _createTime = dateToUnixTimestampInMs() 10 | const _updateTime = _createTime 11 | 12 | // 为 field 添加 id 13 | if (this.action === 'create') { 14 | value.fields = 15 | value?.fields?.map((v) => { 16 | const id = v.id || randomId() 17 | return { 18 | ...v, 19 | id, 20 | } 21 | }) || [] 22 | 23 | return { 24 | ...value, 25 | _createTime, 26 | _updateTime, 27 | } 28 | } 29 | 30 | if (this.action === 'update') { 31 | if (value.fields?.length) { 32 | // 为 field 添加 id 33 | value.fields = value?.fields?.map((v) => { 34 | const id = v.id || randomId() 35 | return { 36 | ...v, 37 | id, 38 | } 39 | }) 40 | } 41 | 42 | return { 43 | ...value, 44 | _updateTime, 45 | } 46 | } 47 | 48 | return value 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/service/src/modules/projects/schemas/schema.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { getCloudBaseManager } from '@/utils' 3 | 4 | @Injectable() 5 | export class SchemasService { 6 | // 创建集合 7 | async createCollection(name: string) { 8 | const manager = await getCloudBaseManager() 9 | 10 | try { 11 | const res = await manager.database.createCollectionIfNotExists(name) 12 | 13 | // 集合创建失败 14 | if (!res?.IsCreated && !res.ExistsResult?.Exists) { 15 | return `Create Collection Fail: ${res.RequestId}` 16 | } 17 | } catch (e) { 18 | console.error(e) 19 | return e.code 20 | } 21 | } 22 | 23 | // 删除集合 24 | async deleteCollection(name: string) { 25 | const manager = await getCloudBaseManager() 26 | 27 | try { 28 | await manager.database.deleteCollection(name) 29 | } catch (e) { 30 | return e.code 31 | } 32 | } 33 | 34 | // 重命名集合 35 | async renameCollection(oldName: string, newName: string) { 36 | const manager = await getCloudBaseManager() 37 | 38 | try { 39 | // 获取数据库实例ID 40 | const { EnvInfo } = await manager.env.getEnvInfo() 41 | const { Databases } = EnvInfo 42 | 43 | await manager.commonService('flexdb').call({ 44 | Action: 'ModifyNameSpace', 45 | Param: { 46 | Tag: Databases[0].InstanceId, 47 | ModifyTableInfo: [ 48 | { 49 | OldTableName: oldName, 50 | NewTableName: newName, 51 | }, 52 | ], 53 | }, 54 | }) 55 | } catch (e) { 56 | return e.code 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/service/src/modules/projects/schemas/types.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator' 2 | 3 | export class SchemaField { 4 | id: string 5 | 6 | // 字段类型 7 | @IsNotEmpty() 8 | type: string 9 | 10 | // 展示标题 11 | @IsNotEmpty() 12 | displayName: string 13 | 14 | // 在数据库中的字段名 15 | @IsNotEmpty() 16 | name: string 17 | 18 | // 字段顺序 19 | order: number 20 | 21 | // 字段描述 22 | description: string 23 | 24 | // 是否隐藏 25 | isHidden: boolean 26 | 27 | // 是否必需字段 28 | isRequired: boolean 29 | 30 | // 排序字段 31 | isOrderField: boolean 32 | orderDirection: 'asc' | 'desc' 33 | 34 | // 是否唯一 35 | isUnique: boolean 36 | 37 | // 在 API 返回结果中隐藏 38 | isHiddenInApi: boolean 39 | 40 | // 是否加密 41 | isEncrypted: boolean 42 | 43 | // 默认值 44 | defaultValue: any 45 | 46 | // 最小长度/值 47 | min: number 48 | 49 | // 最大长度/值 50 | max: number 51 | 52 | // 校验 53 | validator: string 54 | 55 | // 样式属性 56 | style: {} 57 | 58 | // 连接字段 59 | connectField: string 60 | 61 | // 连接资源 Id 62 | connectResource: string 63 | 64 | // 关联多个 65 | connectMany: boolean 66 | 67 | // 枚举类型 68 | enumElements: { label: string; value: string }[] 69 | } 70 | 71 | export class Schema { 72 | _id: string 73 | 74 | displayName: string 75 | 76 | collectionName: string 77 | 78 | projectId: string 79 | 80 | fields: SchemaField[] 81 | 82 | description: string 83 | 84 | _creatTime: number 85 | 86 | _updateTime: number 87 | } 88 | -------------------------------------------------------------------------------- /packages/service/src/modules/projects/webhooks/type.ts: -------------------------------------------------------------------------------- 1 | import { Method } from 'axios' 2 | import { Schema } from '../schemas/types' 3 | 4 | /** 5 | * webhook 定义 6 | */ 7 | export interface Webhook { 8 | _id: string 9 | 10 | name: string 11 | 12 | /** 13 | * webhook 类型 14 | */ 15 | type: 'http' | 'function' 16 | 17 | event: string[] 18 | 19 | collections: (Schema & '*')[] 20 | 21 | /** 22 | * http webhook 属性 23 | */ 24 | url: string 25 | 26 | method: Method 27 | 28 | headers: { key: string; value: string }[] 29 | 30 | /** 31 | * function webhook 属性 32 | */ 33 | functionName: string 34 | } 35 | -------------------------------------------------------------------------------- /packages/service/src/modules/role/role.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator' 2 | 3 | export class UserRole { 4 | // 角色名 5 | @IsNotEmpty() 6 | roleName: string 7 | 8 | // 角色描述 9 | @IsNotEmpty() 10 | description: string 11 | 12 | // 角色绑定的权限描述 13 | @IsNotEmpty() 14 | permissions: Permission[] 15 | } 16 | 17 | export class Permission { 18 | // 项目 19 | @IsNotEmpty() 20 | projectId: '*' | string 21 | 22 | // 行为 23 | @IsNotEmpty() 24 | action: string[] | ['*'] 25 | 26 | // TODO: 允许访问/拒绝访问 27 | effect: 'allow' | 'deny' 28 | 29 | // 服务 30 | @IsNotEmpty() 31 | service: string | '*' 32 | 33 | // 具体资源 34 | @IsNotEmpty() 35 | resource: string[] | ['*'] 36 | } 37 | -------------------------------------------------------------------------------- /packages/service/src/modules/role/role.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { RoleController } from './role.controller' 3 | 4 | @Module({ 5 | controllers: [RoleController], 6 | }) 7 | export class RoleModule {} 8 | -------------------------------------------------------------------------------- /packages/service/src/modules/setting/setting.module.ts: -------------------------------------------------------------------------------- 1 | import { CloudBaseService } from '@/services' 2 | 3 | import { Module } from '@nestjs/common' 4 | import { SettingController } from './setting.controller' 5 | import { SettingService } from './setting.service' 6 | 7 | @Module({ 8 | controllers: [SettingController], 9 | providers: [SettingService], 10 | }) 11 | export class SettingModule { 12 | constructor(private readonly cloudbaseService: CloudBaseService) {} 13 | } 14 | -------------------------------------------------------------------------------- /packages/service/src/modules/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator' 2 | 3 | export class User { 4 | @IsNotEmpty() 5 | username: string 6 | 7 | @IsNotEmpty() 8 | password: string 9 | 10 | @IsNotEmpty() 11 | roles: string[] 12 | 13 | // 创建时间 14 | createTime: number 15 | 16 | // 登陆失败次数 17 | failedLogins?: Record[] 18 | } 19 | -------------------------------------------------------------------------------- /packages/service/src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { UserController } from './user.controller' 3 | import { UserService } from './user.service' 4 | 5 | @Module({ 6 | controllers: [UserController], 7 | providers: [UserService], 8 | }) 9 | export class UserModule {} 10 | -------------------------------------------------------------------------------- /packages/service/src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { getCloudBaseManager } from '@/utils' 3 | import { EndUserInfo } from '@cloudbase/manager-node/types/interfaces' 4 | 5 | @Injectable() 6 | export class UserService { 7 | // 用户名密码登录 => 注册用户信息 8 | async createUser(username: string, password: string): Promise { 9 | const manager = await getCloudBaseManager() 10 | 11 | console.info('注册用户信息', username) 12 | 13 | const { User } = await manager.user.createEndUser({ 14 | username, 15 | password, 16 | }) 17 | 18 | console.info(User) 19 | 20 | return User 21 | } 22 | 23 | async deleteUser(uuid: string) { 24 | const manager = await getCloudBaseManager() 25 | return manager.user.deleteEndUsers({ 26 | userList: [uuid], 27 | }) 28 | } 29 | 30 | async updateUserInfo(uuid: string, data: { username: string; password: string }) { 31 | const { username, password } = data 32 | const manager = await getCloudBaseManager() 33 | return manager.user.modifyEndUser({ 34 | uuid, 35 | username, 36 | password, 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/service/src/services/cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Scope } from '@nestjs/common' 2 | 3 | interface KV { 4 | [key: string]: any 5 | [key: number]: any 6 | } 7 | 8 | interface CacheMap extends KV { 9 | // 模型数据 10 | schemas: Schema[] 11 | 12 | // 关联遍历的集合名 13 | connectTraverseCollections: string[] 14 | } 15 | 16 | // 针对单个请求的缓存 17 | @Injectable({ 18 | scope: Scope.REQUEST, 19 | }) 20 | export class LocalCacheService { 21 | private readonly cache: Map 22 | 23 | constructor() { 24 | this.cache = new Map() 25 | } 26 | 27 | public set(key: keyof CacheMap, value: CacheMap[T]) { 28 | this.cache.set(key, value) 29 | } 30 | 31 | public get(key: T): CacheMap[T] { 32 | return this.cache.get(key) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/service/src/services/cloudbase.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { CloudBase, Database } from '@cloudbase/node-sdk' 3 | import { getCloudBaseApp } from '@/utils' 4 | 5 | @Injectable() 6 | export class CloudBaseService { 7 | app: CloudBase 8 | 9 | constructor() { 10 | this.app = getCloudBaseApp() 11 | } 12 | 13 | get db() { 14 | return this.app.database() 15 | } 16 | 17 | collection(collection: string): Database.CollectionReference { 18 | return this.app.database().collection(collection) 19 | } 20 | 21 | auth() { 22 | return this.app.auth() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/service/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cache.service' 2 | export * from './cloudbase.service' 3 | export * from './schema-cache.service' 4 | -------------------------------------------------------------------------------- /packages/service/src/services/schema-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from '@/constants' 2 | import { getCloudBaseApp } from '@/utils' 3 | import { Injectable } from '@nestjs/common' 4 | import { LocalCacheService } from './cache.service' 5 | 6 | /** 7 | * 请求级缓存 schema 8 | */ 9 | @Injectable() 10 | export class SchemaCacheService { 11 | constructor(private readonly cacheService: LocalCacheService) {} 12 | 13 | getCollectionSchema(collection: string): Promise 14 | getCollectionSchema(): Promise 15 | 16 | async getCollectionSchema(collection?: string) { 17 | const { cacheService } = this 18 | 19 | // 全部 schemas 使用 SCHEMAS 作为 key 缓存 20 | const cacheSchema = collection ? cacheService.get(collection) : cacheService.get('SCHEMAS') 21 | if (cacheSchema) return cacheSchema 22 | 23 | const app = getCloudBaseApp() 24 | 25 | const query = collection 26 | ? { 27 | collectionName: collection, 28 | } 29 | : {} 30 | 31 | const { data }: { data: Schema[] } = await app 32 | .database() 33 | .collection(Collection.Schemas) 34 | .where(query) 35 | .limit(1000) 36 | .get() 37 | 38 | if (collection) { 39 | cacheService.set(collection, data[0]) 40 | } else { 41 | cacheService.set('SCHEMAS', data) 42 | } 43 | 44 | return collection ? data[0] : data 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/service/src/utils/cache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 内存缓存 3 | */ 4 | export class MemoryCache { 5 | cache: Map 6 | 7 | constructor() { 8 | this.cache = new Map() 9 | } 10 | 11 | public set(key: string, value: any) { 12 | this.cache.set(key, value) 13 | } 14 | 15 | public get(key: string): any { 16 | return this.cache.get(key) 17 | } 18 | 19 | public del(key: string) { 20 | this.cache.delete(key) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/service/src/utils/cos.ts: -------------------------------------------------------------------------------- 1 | import COS from 'cos-nodejs-sdk-v5' 2 | import SecretManager, { isRunInContainer } from './cloudbase' 3 | import { getUnixTimestamp } from './date' 4 | import { getCredential, Credential } from './env' 5 | 6 | let cos 7 | let secretExpire: number 8 | let secretManager: SecretManager 9 | 10 | /** 11 | * 获取 COS SDK 实例 12 | */ 13 | export const getCosApp = async (parallel = 20) => { 14 | let credential: Credential 15 | 16 | const now = getUnixTimestamp() + 120 17 | // 秘钥没有过期,可以继续使用,否则,需要重新获取秘钥 18 | if (cos && now < secretExpire) { 19 | return cos 20 | } 21 | 22 | console.log('运行在云托管中', isRunInContainer()) 23 | 24 | // 云托管中 25 | if (isRunInContainer()) { 26 | secretManager = new SecretManager() 27 | const { expire, ...tmpCredential } = await secretManager.getTmpSecret() 28 | secretExpire = expire 29 | credential = tmpCredential 30 | } else { 31 | credential = getCredential() 32 | } 33 | 34 | const { secretId, secretKey, token } = credential 35 | 36 | console.log('密钥', credential) 37 | 38 | cos = new COS({ 39 | FileParallelLimit: parallel, 40 | SecretId: secretId, 41 | SecretKey: secretKey, 42 | XCosSecurityToken: token, 43 | }) 44 | 45 | return cos 46 | } 47 | -------------------------------------------------------------------------------- /packages/service/src/utils/db.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 根据分页条件计算 skip 3 | * @param page 4 | * @param pageSize 5 | * @param offset 6 | */ 7 | export const getSkip = (page = 1, pageSize = 10, offset = 0) => { 8 | const skip = (Number(page) - 1) * Number(pageSize) - offset 9 | return skip >= 0 ? skip : 0 10 | } 11 | 12 | /** 13 | * 根据分页条件计算 limit 14 | * @param page 15 | * @param pageSize 16 | * @param offset 17 | */ 18 | export const getLimit = (page = 1, pageSize = 10, offset = 0) => { 19 | // 转换成 number 20 | const pageNum = Number(page) 21 | const pageSizeNum = Number(pageSize) 22 | 23 | // 当前分页情况,offset 到达的页数 24 | const splitPage = offset > 0 ? Math.ceil(offset / pageSizeNum) : 1 25 | // page 大于 splitPage,说明不需要从 offset 中补充,直接返回 26 | // 反之,返回需要从数据库中查询的条数 27 | return pageNum > splitPage ? pageSizeNum : pageSizeNum - (offset % pageSizeNum) 28 | } 29 | -------------------------------------------------------------------------------- /packages/service/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | export interface Credential { 2 | secretId: string 3 | secretKey: string 4 | token?: string 5 | region?: string 6 | } 7 | 8 | /** 9 | * 从环境变量中解析密钥信息 10 | */ 11 | export const getCredential = (): Credential => { 12 | const { 13 | SECRETID, 14 | SECRETKEY, 15 | TENCENTCLOUD_REGION = 'ap-shanghai', 16 | TENCENTCLOUD_SECRETID, 17 | TENCENTCLOUD_SECRETKEY, 18 | TENCENTCLOUD_SESSIONTOKEN, 19 | } = process.env 20 | 21 | const secretId = SECRETID || TENCENTCLOUD_SECRETID 22 | const secretKey = SECRETKEY || TENCENTCLOUD_SECRETKEY 23 | 24 | return { 25 | secretId, 26 | secretKey, 27 | region: TENCENTCLOUD_REGION, 28 | token: TENCENTCLOUD_SESSIONTOKEN, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/service/src/utils/field.ts: -------------------------------------------------------------------------------- 1 | // 是否为 date 类型 2 | export const isDateType = (type: string): boolean => type === 'Date' || type === 'DateTime' 3 | -------------------------------------------------------------------------------- /packages/service/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './db' 2 | export * from './date' 3 | export * from './cloudbase' 4 | export * from './tools' 5 | export * from './permission' 6 | export * from './lowcode' 7 | -------------------------------------------------------------------------------- /packages/service/src/utils/lowcode.ts: -------------------------------------------------------------------------------- 1 | import { CloudApiService } from '@cloudbase/cloud-api' 2 | import { getCredential } from './env' 3 | 4 | /** 5 | * 获取低码应用已发布页面列表 6 | */ 7 | export const getLowCodeAppInfo = async (appId: string) => { 8 | const credential = getCredential() 9 | const apiService = new CloudApiService({ 10 | credential, 11 | service: 'lowcode', 12 | }) 13 | 14 | try { 15 | const res = await apiService.request('ListAppPages', { 16 | projectId: appId, 17 | }) 18 | return res 19 | } catch (error) { 20 | console.log(error) 21 | return { 22 | data: { 23 | pages: [], 24 | }, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/service/src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { customAlphabet } from 'nanoid' 3 | 4 | export const isDevEnv = () => 5 | process.env.NODE_ENV === 'development' && !process.env.TENCENTCLOUD_RUNENV 6 | 7 | /** 8 | * 生成随机 id 9 | */ 10 | export const randomId = (len = 32) => 11 | customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-', len)() 12 | 13 | /** 14 | * 检查 ele 不为 null,undefined,空数组,空对象,仅包含 null、undefined 的数组 15 | */ 16 | export const isNotEmpty = (ele: any | any[]) => { 17 | if (Array.isArray(ele)) { 18 | return !_.isEmpty(ele) && !_.isEmpty(ele.filter((_) => _)) 19 | } 20 | 21 | return !_.isEmpty(ele) 22 | } 23 | 24 | /** 25 | * 休眠 tick 时间 26 | */ 27 | export const sleep = (tick: number) => { 28 | return new Promise((resolve) => { 29 | setTimeout(() => { 30 | resolve() 31 | }, tick) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /packages/service/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": false, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": false, 11 | "esModuleInterop": true, 12 | "outDir": "dist", 13 | "baseUrl": ".", 14 | "incremental": true, 15 | "paths": { 16 | "@/*": ["src/*"], 17 | "@modules/*": ["src/modules/*"], 18 | "@utils": ["src/utils"] 19 | } 20 | }, 21 | "include": ["src/**/*", "typings/**/*"], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/service/typings/file.d.ts: -------------------------------------------------------------------------------- 1 | interface IFile { 2 | fieldname: string 3 | originalname: string 4 | encoding: string 5 | mimetype: string 6 | buffer: Buffer 7 | size: number 8 | } 9 | -------------------------------------------------------------------------------- /packages/service/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | NODE_ENV: 'development' | 'production' 4 | TCB_ENVID: string 5 | SECRETID: string 6 | SECRETKEY: string 7 | TCB_CMS: string 8 | WX_MP: string 9 | FROM_LOWCODE: string 10 | } 11 | } 12 | 13 | interface User { 14 | _id: string 15 | 16 | username: string 17 | 18 | // 创建时间 19 | createTime: number 20 | 21 | // 用户角色 22 | roles: string[] 23 | 24 | // cloudbase uuid 25 | uuid: string 26 | 27 | // 是否为 root 用户 28 | root?: boolean 29 | } 30 | 31 | interface UserRole { 32 | _id: string 33 | 34 | // 角色名 35 | roleName: string 36 | 37 | // 角色描述 38 | description: string 39 | 40 | // 角色绑定的权限描述 41 | permissions: Permission[] 42 | 43 | type: string | 'system' 44 | } 45 | 46 | /** 47 | * 限制 48 | * 项目 ID 为 * 时,资源必然为 * 49 | * 服务为 * 时,资源必然为 * 50 | */ 51 | interface Permission { 52 | // 项目 53 | projectId: '*' | string 54 | 55 | // 行为 56 | action: string[] | ['*'] 57 | 58 | // TODO: 允许访问/拒绝访问 59 | effect: 'allow' | 'deny' 60 | 61 | // 服务 62 | // 一个权限规则仅支持一个 service 63 | service: string | '*' 64 | 65 | // 具体资源 66 | resource: string[] | ['*'] 67 | } 68 | 69 | interface RequestUser extends User { 70 | // 用户可以访问的项目资源 71 | projectResource?: { 72 | [key: string]: '*' | string[] 73 | } 74 | 75 | // 所有可访问的服务 76 | accessibleService?: '*' | string[] 77 | 78 | // 系统管理员 79 | isAdmin?: boolean 80 | 81 | // 项目管理员 82 | isProjectAdmin?: boolean 83 | 84 | // 用户关联的角色信息 85 | userRoles?: UserRole[] 86 | } 87 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo $JOB_ID 4 | echo "您当前正在使用云托管模式部署 CMS" 5 | echo "如果你想继续使用云函数部署 CMS,请将 cloudbaserc-fx.json 文件重命名为 cloudbaserc.json 并替换现有的 cloudbaserc.json 文件,再运行 npm run deploy 命令" 6 | echo "" 7 | 8 | # 云端构建 9 | if [ $JOB_ID ]; then 10 | # 安装依赖 11 | echo "云端构建,安装开发依赖" 12 | yarn 13 | echo "安装开发依赖完成" 14 | echo "安装项目依赖" 15 | npm run setup 16 | echo "安装项目依赖完成" 17 | else 18 | echo "本地,跳过安装依赖" 19 | fi 20 | 21 | echo "" 22 | -------------------------------------------------------------------------------- /scripts/zip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 根路径 4 | __ABS_PATH__="$(pwd)" 5 | 6 | # 将静态托管网站 Build 的代码拷贝到 cms-init 函数中 7 | # 使用 cms-init 函数上传 8 | cd "$__ABS_PATH__/packages/cms-init" 9 | rm -rf build 10 | rm -rf sms-dist 11 | mkdir build 12 | mkdir sms-dist 13 | 14 | cd - 15 | 16 | # 拷贝管理端代码 17 | cp -R ./packages/admin/dist/* ./packages/cms-init/build 18 | # 拷贝 sms 跳转页代码 19 | cp -R ./packages/cms-sms-page/dist/* ./packages/cms-init/sms-dist 20 | # 添加到 service 服务 21 | cp -R ./packages/cms-sms-page/dist/* ./packages/service/dist/modules/projects/operation/template 22 | 23 | cd $__ABS_PATH__ 24 | 25 | # 打包函数代码 26 | zipFunction() { 27 | echo "zip $1" 28 | cd "packages/$1" 29 | DEST_FILE="$__ABS_PATH__/build/$1.zip" 30 | 31 | rm -rf $DEST_FILE 32 | zip -r $DEST_FILE . -x 'node_modules/*' -x '.DS_Store' -x 'src/*' -x yarn.lock -x .env.local -x .env.wx.local 33 | cd - 34 | } 35 | 36 | zipFunction service 37 | zipFunction cms-init 38 | zipFunction cms-api 39 | zipFunction cms-sms 40 | zipFunction cms-openapi 41 | zipFunction cms-fx-openapi 42 | 43 | cd $__ABS_PATH__ 44 | rm -rf packages/cms-init/build 45 | rm -rf packages/cms-init/sms-dist 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "outDir": "dist", 12 | "baseUrl": ".", 13 | "incremental": true, 14 | "paths": {} 15 | } 16 | } 17 | --------------------------------------------------------------------------------