Compare commits

...

16 Commits

Author SHA1 Message Date
xiaojunnuo 90ba55c043 chore: 1 2026-05-14 19:05:49 +08:00
xiaojunnuo 9f878a353c fix: 修复clogin多选类型登录失败的bug 2026-05-14 19:04:20 +08:00
xiaojunnuo af7297d671 chore: 1 2026-05-14 18:47:28 +08:00
xiaojunnuo 2f172b56e9 fix: 修复群晖授权没有显示设备id输入框的bug 2026-05-14 18:42:35 +08:00
xiaojunnuo 9076c8b20e Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-05-14 18:31:34 +08:00
xiaojunnuo 639756dfcd chore: sdk 2026-05-14 18:31:25 +08:00
xiaojunnuo 7aa0c7e491 feat: 彩虹登录支持选择多种登录方式 2026-05-14 01:39:22 +08:00
xiaojunnuo 45dedf5bc7 fix: 修复第三方登录丢失state时无法在用户信息页面绑定第三方账号的bug 2026-05-13 23:47:31 +08:00
xiaojunnuo 4681ec9008 fix: 修复自动注册后没有跳转到控制台的bug 2026-05-13 22:47:46 +08:00
xiaojunnuo b91826c6e6 chore: 1 2026-05-13 17:29:49 +08:00
xiaojunnuo 686856d0ae build: release 2026-05-13 17:29:19 +08:00
xiaojunnuo 9b09d2578d chore: 1 2026-05-13 17:14:14 +08:00
xiaojunnuo f8f51adf88 build: publish 2026-05-13 14:22:46 +08:00
xiaojunnuo f8ce639717 build: trigger build image 2026-05-13 14:22:33 +08:00
xiaojunnuo 1c6dc169ac v1.39.16 2026-05-13 14:21:21 +08:00
xiaojunnuo 3e5366c74e build: prepare to build 2026-05-13 14:13:01 +08:00
59 changed files with 1424 additions and 261 deletions
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package root
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13) ## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Bug Fixes ### Bug Fixes
+17
View File
@@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package root
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Bug Fixes
* 修复第三方登录彩虹登录不上的bug ([bae4f8e](https://github.com/certd/certd/commit/bae4f8e3209d9f9869ecbd7c01655383bac2fe21))
### Performance Improvements
* 优化申请时报错日志增加对应域名打印 ([d6e9e59](https://github.com/certd/certd/commit/d6e9e5987bd52ea12ee18745615486eadd4c87ff))
* icon选择器增加一套logo集 ([fdd5848](https://github.com/certd/certd/commit/fdd5848df4055a6ee07dc5eabaaf6b718672882d))
* **monitor/site:** 新增站点监控页面禁用启用、检查状态两个筛选条件 ([118c15d](https://github.com/certd/certd/commit/118c15d04633a6ef06f2d9e7a7849d20f596e02c))
* **network:** 新增全局公共http请求 headers设置 ([aad9045](https://github.com/certd/certd/commit/aad9045de55e76cb2ad09cac74a7bd60a4b47124))
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11) ## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
### Bug Fixes ### Bug Fixes
+1 -1
View File
@@ -9,5 +9,5 @@
} }
}, },
"npmClient": "pnpm", "npmClient": "pnpm",
"version": "1.39.15" "version": "1.39.16"
} }
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/publishlab/node-acme-client/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/acme-client
## [1.39.15](https://github.com/publishlab/node-acme-client/compare/v1.39.14...v1.39.15) (2026-05-13) ## [1.39.15](https://github.com/publishlab/node-acme-client/compare/v1.39.14...v1.39.15) (2026-05-13)
### Performance Improvements ### Performance Improvements
+3 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client", "description": "Simple and unopinionated ACME client",
"private": false, "private": false,
"author": "nmorsman", "author": "nmorsman",
"version": "1.39.15", "version": "1.39.16",
"type": "module", "type": "module",
"module": "./dist/index.js", "module": "./dist/index.js",
"main": "./dist/index.js", "main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types" "types"
], ],
"dependencies": { "dependencies": {
"@certd/basic": "^1.39.15", "@certd/basic": "^1.39.16",
"@peculiar/x509": "^1.11.0", "@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.5",
"axios": "^1.9.0", "axios": "^1.9.0",
@@ -76,5 +76,5 @@
"bugs": { "bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues" "url": "https://github.com/publishlab/node-acme-client/issues"
}, },
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0" "gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
} }
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/basic
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13) ## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Performance Improvements ### Performance Improvements
+1 -1
View File
@@ -1 +1 @@
13:33 14:13
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/basic", "name": "@certd/basic",
"private": false, "private": false,
"version": "1.39.15", "version": "1.39.16",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -52,5 +52,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0" "gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
} }
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/pipeline
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13) ## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/pipeline **Note:** Version bump only for package @certd/pipeline
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/pipeline", "name": "@certd/pipeline",
"private": false, "private": false,
"version": "1.39.15", "version": "1.39.16",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -19,8 +19,8 @@
"compile": "tsc --skipLibCheck --watch" "compile": "tsc --skipLibCheck --watch"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.39.15", "@certd/basic": "^1.39.16",
"@certd/plus-core": "^1.39.15", "@certd/plus-core": "^1.39.16",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13" "reflect-metadata": "^0.1.13"
@@ -49,5 +49,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0" "gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-huawei", "name": "@certd/lib-huawei",
"private": false, "private": false,
"version": "1.39.15", "version": "1.39.16",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts", "types": "./dist/d/index.d.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-iframe", "name": "@certd/lib-iframe",
"private": false, "private": false,
"version": "1.39.15", "version": "1.39.16",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/jdcloud", "name": "@certd/jdcloud",
"version": "1.39.15", "version": "1.39.16",
"description": "jdcloud openApi sdk", "description": "jdcloud openApi sdk",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/lib-k8s
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13) ## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/lib-k8s **Note:** Version bump only for package @certd/lib-k8s
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-k8s", "name": "@certd/lib-k8s",
"private": false, "private": false,
"version": "1.39.15", "version": "1.39.16",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -19,7 +19,7 @@
"compile": "tsc --skipLibCheck --watch" "compile": "tsc --skipLibCheck --watch"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.39.15", "@certd/basic": "^1.39.16",
"@kubernetes/client-node": "0.21.0" "@kubernetes/client-node": "0.21.0"
}, },
"devDependencies": { "devDependencies": {
@@ -36,5 +36,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0" "gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
} }
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/lib-server
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13) ## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Performance Improvements ### Performance Improvements
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/lib-server", "name": "@certd/lib-server",
"version": "1.39.15", "version": "1.39.16",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -29,11 +29,11 @@
], ],
"license": "AGPL", "license": "AGPL",
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.39.15", "@certd/acme-client": "^1.39.16",
"@certd/basic": "^1.39.15", "@certd/basic": "^1.39.16",
"@certd/pipeline": "^1.39.15", "@certd/pipeline": "^1.39.16",
"@certd/plugin-lib": "^1.39.15", "@certd/plugin-lib": "^1.39.16",
"@certd/plus-core": "^1.39.15", "@certd/plus-core": "^1.39.16",
"@midwayjs/cache": "3.14.0", "@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11", "@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13", "@midwayjs/i18n": "3.20.13",
@@ -69,5 +69,5 @@
"typeorm": "^0.3.11", "typeorm": "^0.3.11",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0" "gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
} }
@@ -64,6 +64,7 @@ export class SysPublicSettings extends BaseSettings {
type: string; type: string;
title: string; title: string;
addonId: number; addonId: number;
icon?: string;
}> = {}; }> = {};
notice?: string; notice?: string;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/midway-flyway-js", "name": "@certd/midway-flyway-js",
"version": "1.39.15", "version": "1.39.16",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/plugin-cert
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13) ## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/plugin-cert **Note:** Version bump only for package @certd/plugin-cert
+6 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-cert", "name": "@certd/plugin-cert",
"private": false, "private": false,
"version": "1.39.15", "version": "1.39.16",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -18,10 +18,10 @@
"compile": "tsc --skipLibCheck --watch" "compile": "tsc --skipLibCheck --watch"
}, },
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.39.15", "@certd/acme-client": "^1.39.16",
"@certd/basic": "^1.39.15", "@certd/basic": "^1.39.16",
"@certd/pipeline": "^1.39.15", "@certd/pipeline": "^1.39.16",
"@certd/plugin-lib": "^1.39.15", "@certd/plugin-lib": "^1.39.16",
"psl": "^1.9.0", "psl": "^1.9.0",
"punycode.js": "^2.3.1" "punycode.js": "^2.3.1"
}, },
@@ -41,5 +41,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0" "gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
} }
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/plugin-lib
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13) ## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/plugin-lib **Note:** Version bump only for package @certd/plugin-lib
+6 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-lib", "name": "@certd/plugin-lib",
"private": false, "private": false,
"version": "1.39.15", "version": "1.39.16",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -23,10 +23,10 @@
"@alicloud/pop-core": "^1.7.10", "@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.11", "@alicloud/tea-util": "^1.4.11",
"@aws-sdk/client-s3": "^3.964.0", "@aws-sdk/client-s3": "^3.964.0",
"@certd/acme-client": "^1.39.15", "@certd/acme-client": "^1.39.16",
"@certd/basic": "^1.39.15", "@certd/basic": "^1.39.16",
"@certd/pipeline": "^1.39.15", "@certd/pipeline": "^1.39.16",
"@certd/plus-core": "^1.39.15", "@certd/plus-core": "^1.39.16",
"@kubernetes/client-node": "0.21.0", "@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0", "ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5", "basic-ftp": "^5.0.5",
@@ -61,5 +61,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0" "gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
} }
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/ui-client
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13) ## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Performance Improvements ### Performance Improvements
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/ui-client", "name": "@certd/ui-client",
"version": "1.39.15", "version": "1.39.16",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite --open", "dev": "vite --open",
@@ -106,8 +106,8 @@
"zod-defaults": "^0.1.3" "zod-defaults": "^0.1.3"
}, },
"devDependencies": { "devDependencies": {
"@certd/lib-iframe": "^1.39.15", "@certd/lib-iframe": "^1.39.16",
"@certd/pipeline": "^1.39.15", "@certd/pipeline": "^1.39.16",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12", "@types/chai": "^4.3.12",
@@ -85,6 +85,7 @@ export type SysPublicSetting = {
type: string; type: string;
title: string; title: string;
addonId: number; addonId: number;
icon?: string;
} }
>; >;
// 系统通知 // 系统通知
@@ -38,6 +38,9 @@ export const useUserStore = defineStore({
getToken(): string { getToken(): string {
return this.token || LocalStorage.get(TOKEN_KEY); return this.token || LocalStorage.get(TOKEN_KEY);
}, },
isLogined(): boolean {
return !!this.getToken;
},
isAdmin(): boolean { isAdmin(): boolean {
return this.getUserInfo.roleIds?.includes(1) || this.getUserInfo.id === 1; return this.getUserInfo.roleIds?.includes(1) || this.getUserInfo.id === 1;
}, },
@@ -13,8 +13,9 @@ export function useReference(formItem: any) {
const ctx = { const ctx = {
compute: (opts: any) => { compute: (opts: any) => {
const func = (context: any) => { const func = (context: any) => {
debugger;
let form = context.form || {}; let form = context.form || {};
form = form.input || form.body || form.access || form; form = form.input || form.body || form; // form.access去掉,历史原因,access的mergeScript会处理form.access
return opts({ return opts({
...context, ...context,
form, form,
@@ -68,20 +68,23 @@ export async function GetOauthProviders() {
}); });
} }
export async function UnbindOauth(type: string) { export async function UnbindOauth(type: string, subtype?: string) {
return await request({ return await request({
url: "/oauth/unbind", url: "/oauth/unbind",
method: "POST", method: "POST",
data: { type }, data: {
type: subtype ? `${type}:${subtype}` : type,
},
}); });
} }
export async function OauthBoundUrl(type: string) { export async function OauthBoundUrl(type: string, subtype?: string) {
return await request({ return await request({
url: "/oauth/login", url: "/oauth/login",
method: "POST", method: "POST",
data: { data: {
type, type,
subtype,
forType: "bind", forType: "bind",
}, },
}); });
@@ -78,11 +78,11 @@
<a-tag v-else color="red" class="bound-tag1">未绑定</a-tag> <a-tag v-else color="red" class="bound-tag1">未绑定</a-tag>
</span> </span>
</div> </div>
<a-button v-if="item.bound" type="primary" danger class="action-btn" @click="unbind(item.name)"> <a-button v-if="item.bound" type="primary" danger class="action-btn" @click="unbind(item)">
<template #icon><fs-icon icon="ion:unlink-outline" /></template> <template #icon><fs-icon icon="ion:unlink-outline" /></template>
解绑 解绑
</a-button> </a-button>
<a-button v-else type="primary" class="action-btn" @click="bind(item.name)"> <a-button v-else type="primary" class="action-btn" @click="bind(item)">
<template #icon><fs-icon icon="ion:link-outline" /></template> <template #icon><fs-icon icon="ion:link-outline" /></template>
绑定 绑定
</a-button> </a-button>
@@ -214,7 +214,7 @@ async function loadOauthProviders() {
const computedOauthBounds = computed(() => { const computedOauthBounds = computed(() => {
const list = oauthProviders.value.map(item => { const list = oauthProviders.value.map(item => {
const bound = oauthBounds.value.find(bound => bound.type === item.name); const bound = oauthBounds.value.find(bound => bound.type === buildOauthBoundType(item));
return { return {
...item, ...item,
bound, bound,
@@ -223,20 +223,24 @@ const computedOauthBounds = computed(() => {
return list; return list;
}); });
async function unbind(type: string) { function buildOauthBoundType(item: any) {
return item.subtype ? `${item.name}:${item.subtype}` : item.name;
}
async function unbind(item: any) {
Modal.confirm({ Modal.confirm({
title: "确认解绑吗?", title: "确认解绑吗?",
okText: "确认", okText: "确认",
okType: "danger", okType: "danger",
onOk: async () => { onOk: async () => {
await api.UnbindOauth(type); await api.UnbindOauth(item.name, item.subtype);
await loadOauthBounds(); await loadOauthBounds();
}, },
}); });
} }
async function bind(type: string) { async function bind(item: any) {
const res = await api.OauthBoundUrl(type); const res = await api.OauthBoundUrl(item.name, item.subtype);
const loginUrl = res.loginUrl; const loginUrl = res.loginUrl;
window.location.href = loginUrl; window.location.href = loginUrl;
} }
@@ -2,7 +2,7 @@ import { request } from "/src/api/service";
const apiPrefix = "/oauth"; const apiPrefix = "/oauth";
export async function OauthLogin(type: string, forType?: string, from?: string) { export async function OauthLogin(type: string, forType?: string, from?: string, subtype?: string) {
return await request({ return await request({
url: apiPrefix + `/login`, url: apiPrefix + `/login`,
method: "post", method: "post",
@@ -10,6 +10,7 @@ export async function OauthLogin(type: string, forType?: string, from?: string)
type, type,
forType: forType || "login", forType: forType || "login",
from: from || "web", from: from || "web",
subtype,
}, },
}); });
} }
@@ -9,8 +9,9 @@
<div>第三方{{ oauthType }}登录成功您还未绑定账号请选择</div> <div>第三方{{ oauthType }}登录成功您还未绑定账号请选择</div>
<div class="mt-10"> <div class="mt-10">
<a-button class="w-full mt-10" type="primary" @click="goBindUser">绑定已有账号</a-button> <a-button v-if="!userStore.isLogined" class="w-full mt-10" type="primary" @click="goBindUser">绑定已有账号</a-button>
<a-button v-if="settingStore.sysPublic.registerEnabled" class="w-full mt-10" type="primary" @click="autoRegister">创建新账号</a-button> <a-button v-else class="w-full mt-10" type="primary" @click="doBindCurrent">绑定当前登录账号({{ userStore.getUserInfo.username }} - {{ userStore.getUserInfo.nickName }})</a-button>
<a-button v-if="settingStore.sysPublic.registerEnabled" class="w-full mt-10" type="primary" @click="autoRegister">创建新账号绑定</a-button>
</div> </div>
<div class="w-full mt-10"> <div class="w-full mt-10">
@@ -63,6 +64,15 @@ async function handleOauthToken() {
} }
} }
async function doBindCurrent() {
await api.BindUser(validationCode);
notification.success({
message: "绑定成功",
});
//
router.replace("/certd/mine/user-profile");
}
onMounted(async () => { onMounted(async () => {
if (error.value) { if (error.value) {
return; return;
@@ -70,12 +80,7 @@ onMounted(async () => {
if (forType === "bind") { if (forType === "bind") {
// //
await api.BindUser(validationCode); await doBindCurrent();
notification.success({
message: "绑定成功",
});
//
router.replace("/certd/mine/user-profile");
return; return;
} }
@@ -98,7 +103,7 @@ async function autoRegister() {
// //
userStore.onLoginSuccess(res); userStore.onLoginSuccess(res);
// //
router.replace("/"); router.replace("/index");
} }
</script> </script>
<style lang="less"> <style lang="less">
@@ -5,8 +5,8 @@
</div> </div>
<div class="flex justify-center items-center gap-4 flex-wrap md:flex-nowrap"> <div class="flex justify-center items-center gap-4 flex-wrap md:flex-nowrap">
<passkey-login></passkey-login> <passkey-login></passkey-login>
<template v-for="item in oauthProviderList" :key="item.type"> <template v-for="item in oauthProviderList" :key="buildProviderKey(item)">
<div v-if="item.addonId" class="oauth-icon-button pointer" @click="goOauthLogin(item.name)"> <div v-if="item.addonId" class="oauth-icon-button pointer" @click="goOauthLogin(item)">
<div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div> <div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div>
<div class="ellipsis title" :title="item.addonTitle || item.title">{{ item.addonTitle || item.title }}</div> <div class="ellipsis title" :title="item.addonTitle || item.title">{{ item.addonTitle || item.title }}</div>
</div> </div>
@@ -22,7 +22,17 @@ import { useSettingStore } from "/@/store/settings";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import PasskeyLogin from "../login/passkey-login.vue"; import PasskeyLogin from "../login/passkey-login.vue";
const oauthProviderList = ref([]); type OauthProviderItem = {
name: string;
type?: string;
subtype?: string;
title: string;
addonTitle?: string;
icon: string;
addonId?: number;
};
const oauthProviderList = ref<OauthProviderItem[]>([]);
const props = defineProps<{ const props = defineProps<{
oauthOnly?: boolean; oauthOnly?: boolean;
}>(); }>();
@@ -42,15 +52,19 @@ onMounted(async () => {
if (settingStore.sysPublic.oauthAutoRedirect && queryOauthOnly !== "false") { if (settingStore.sysPublic.oauthAutoRedirect && queryOauthOnly !== "false") {
const firstOauth = oauthProviderList.value.find(item => item.addonId > 0); const firstOauth = oauthProviderList.value.find(item => item.addonId > 0);
if (firstOauth) { if (firstOauth) {
goOauthLogin(firstOauth.name); goOauthLogin(firstOauth);
} }
} }
}); });
async function goOauthLogin(type: string) { function buildProviderKey(item: OauthProviderItem) {
return `${item.name}:${item.subtype || ""}`;
}
async function goOauthLogin(item: OauthProviderItem) {
//URL //URL
const from = "web"; const from = "web";
const res = await api.OauthLogin(type, from); const res = await api.OauthLogin(item.name, "login", from, item.subtype);
const loginUrl = res.loginUrl; const loginUrl = res.loginUrl;
window.location.href = loginUrl; window.location.href = loginUrl;
} }
@@ -114,7 +114,7 @@ export async function GetSmsTypeDefine(type: string) {
export async function GetOauthProviders() { export async function GetOauthProviders() {
return await request({ return await request({
url: "/oauth/providers", url: apiPrefix + "/oauth/providers",
method: "post", method: "post",
}); });
} }
@@ -109,6 +109,7 @@ const formState = reactive<Partial<SysSettings>>({
const oauthProviders = ref([]); const oauthProviders = ref([]);
async function loadOauthProviders() { async function loadOauthProviders() {
oauthProviders.value = await api.GetOauthProviders(); oauthProviders.value = await api.GetOauthProviders();
mergeOauthProviderSettings();
} }
const bindDomain = computed(() => { const bindDomain = computed(() => {
@@ -164,6 +165,16 @@ const onFinish = async (form: any) => {
function buildCallbackUrl(type: string) { function buildCallbackUrl(type: string) {
return `${window.location.origin}/api/oauth/callback/${type}`; return `${window.location.origin}/api/oauth/callback/${type}`;
} }
function mergeOauthProviderSettings() {
const savedProviders = formState.public?.oauthProviders || {};
for (const item of oauthProviders.value) {
const saved = savedProviders[item.name];
if (saved) {
item.addonId = saved.addonId;
}
}
}
</script> </script>
<style lang="less"> <style lang="less">
.sys-settings-oauth { .sys-settings-oauth {
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/ui-server
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13) ## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Bug Fixes ### Bug Fixes
@@ -14,55 +14,54 @@ input:
loginType: loginType:
title: 登录类型 title: 登录类型
component: component:
name: a-auto-complete name: a-select
vModel: value
mode: tags
multiple: true
options: options:
- label: QQ - label: QQ
value: qq value: qq
icon: logos:tencent-qq
- label: 微信 - label: 微信
value: wx value: wx
icon: logos:wechat-icon
- label: 支付宝 - label: 支付宝
value: alipay value: alipay
icon: logos:alipay
- label: 微博 - label: 微博
value: sina value: sina
icon: logos:sina-weibo
- label: 百度 - label: 百度
value: baidu value: baidu
icon: logos:baidu
- label: 华为 - label: 华为
value: huawei value: huawei
icon: simple-icons:huawei:#ff0000
- label: 小米 - label: 小米
value: xiaomi value: xiaomi
icon: logos:xiaomi-icon
- label: 谷歌 - label: 谷歌
value: google value: google
icon: logos:google-icon
- label: 微软 - label: 微软
value: microsoft value: microsoft
icon: logos:microsoft-icon
- label: Facebook - label: Facebook
value: facebook value: facebook
icon: logos:facebook
- label: Twitter - label: Twitter
value: twitter value: twitter
icon: logos:twitter
- label: 钉钉 - label: 钉钉
value: dingtalk value: dingtalk
icon: logos:dingtalk
- label: Gitee - label: Gitee
value: gitee value: gitee
icon: simple-icons:gitee:#c71d23
- label: Github - label: Github
value: github value: github
icon: logos:github-icon
required: true required: true
icon:
title: 自定义图标
component:
name: fs-icon-selector
vModel: modelValue
iconSets:
- streamline-logos
- logos
- fa-brands
- fa-solid
- fa-regular
- carbon
- ion
- ant-design
- mdi
- twemoji
- svg-spinners
required: false
appId: appId:
title: AppId title: AppId
helper: 彩虹聚合登录->应用列表->创建应用 获取 helper: 彩虹聚合登录->应用列表->创建应用 获取
+14 -14
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/ui-server", "name": "@certd/ui-server",
"version": "1.39.15", "version": "1.39.16",
"description": "fast-server base midway", "description": "fast-server base midway",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -53,20 +53,20 @@
"@aws-sdk/client-sts": "^3.990.0", "@aws-sdk/client-sts": "^3.990.0",
"@azure/arm-dns": "^5.1.0", "@azure/arm-dns": "^5.1.0",
"@azure/identity": "^4.13.1", "@azure/identity": "^4.13.1",
"@certd/acme-client": "^1.39.15", "@certd/acme-client": "^1.39.16",
"@certd/basic": "^1.39.15", "@certd/basic": "^1.39.16",
"@certd/commercial-core": "^1.39.15", "@certd/commercial-core": "^1.39.16",
"@certd/cv4pve-api-javascript": "^8.4.2", "@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.39.15", "@certd/jdcloud": "^1.39.16",
"@certd/lib-huawei": "^1.39.15", "@certd/lib-huawei": "^1.39.16",
"@certd/lib-k8s": "^1.39.15", "@certd/lib-k8s": "^1.39.16",
"@certd/lib-server": "^1.39.15", "@certd/lib-server": "^1.39.16",
"@certd/midway-flyway-js": "^1.39.15", "@certd/midway-flyway-js": "^1.39.16",
"@certd/pipeline": "^1.39.15", "@certd/pipeline": "^1.39.16",
"@certd/plugin-cert": "^1.39.15", "@certd/plugin-cert": "^1.39.16",
"@certd/plugin-lib": "^1.39.15", "@certd/plugin-lib": "^1.39.16",
"@certd/plugin-plus": "^1.39.15", "@certd/plugin-plus": "^1.39.16",
"@certd/plus-core": "^1.39.15", "@certd/plus-core": "^1.39.16",
"@google-cloud/dns": "^5.3.1", "@google-cloud/dns": "^5.3.1",
"@google-cloud/publicca": "^1.3.0", "@google-cloud/publicca": "^1.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.185", "@huaweicloud/huaweicloud-sdk-cdn": "^3.1.185",
@@ -11,6 +11,24 @@ import { UserEntity } from "../../../modules/sys/authority/entity/user.js";
import { UserService } from "../../../modules/sys/authority/service/user-service.js"; import { UserService } from "../../../modules/sys/authority/service/user-service.js";
import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js"; import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js";
type OauthProviderSetting = {
type: string;
title: string;
icon?: string;
addonId: number;
types?: OauthProviderType[];
};
type OauthProviderType = {
type: string;
name: string;
icon?: string;
};
function getOauthBoundType(type: string, subtype?: string) {
return subtype ? `${type}:${subtype}` : type;
}
/** /**
*/ */
@Provide() @Provide()
@@ -41,7 +59,7 @@ export class ConnectController extends BaseController {
if (!publicSettings?.oauthEnabled) { if (!publicSettings?.oauthEnabled) {
throw new Error("OAuth功能未启用"); throw new Error("OAuth功能未启用");
} }
const setting = publicSettings?.oauthProviders?.[type || ""] const setting = publicSettings?.oauthProviders?.[type || ""] as OauthProviderSetting | undefined;
if (!setting) { if (!setting) {
throw new Error(`未配置该OAuth类型:${type}`); throw new Error(`未配置该OAuth类型:${type}`);
} }
@@ -50,19 +68,30 @@ export class ConnectController extends BaseController {
if (!addon) { if (!addon) {
throw new Error("初始化OAuth插件失败"); throw new Error("初始化OAuth插件失败");
} }
return addon as IOauthProvider; return {
addon: addon as IOauthProvider,
setting,
};
} }
@Post('/login', { description: Constants.per.guest }) @Post('/login', { description: Constants.per.guest })
public async login(@Body(ALL) body: { type: string, forType?:string ,from?:string }) { public async login(@Body(ALL) body: { type: string, subtype?: string, forType?:string ,from?:string }) {
const addon = await this.getOauthProvider(body.type); const oauthProvider = await this.getOauthProvider(body.type);
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo); const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
const bindUrl = installInfo?.bindUrl || ""; const bindUrl = installInfo?.bindUrl || "";
//构造登录url //构造登录url
const redirectUrl = `${bindUrl}api/oauth/callback/${body.type}`; const redirectUrl = `${bindUrl}api/oauth/callback/${body.type}`;
const { loginUrl, ticketValue } = await addon.buildLoginUrl({ redirectUri: redirectUrl, forType: body.forType ,from: body.from || "web" }); const { loginUrl, ticketValue } = await oauthProvider.addon.buildLoginUrl({
const ticket = this.codeService.setValidationValue(ticketValue) redirectUri: redirectUrl,
forType: body.forType,
from: body.from || "web",
subtype: body.subtype,
});
const ticket = this.codeService.setValidationValue({
...ticketValue,
subtype: body.subtype,
})
this.ctx.cookies.set("oauth_ticket", ticket, { this.ctx.cookies.set("oauth_ticket", ticket, {
httpOnly: true, httpOnly: true,
// secure: true, // secure: true,
@@ -78,7 +107,7 @@ export class ConnectController extends BaseController {
checkPlus() checkPlus()
//处理登录回调 //处理登录回调
const addon = await this.getOauthProvider(type); const oauthProvider = await this.getOauthProvider(type);
const request = this.ctx.request; const request = this.ctx.request;
// const ticketValue = this.codeService.getValidationValue(ticket); // const ticketValue = this.codeService.getValidationValue(ticket);
// if (!ticketValue) { // if (!ticketValue) {
@@ -98,7 +127,7 @@ export class ConnectController extends BaseController {
const bindUrl = installInfo?.bindUrl || ""; const bindUrl = installInfo?.bindUrl || "";
const currentUrl = `${bindUrl}api/oauth/callback/${type}?${request.querystring}` const currentUrl = `${bindUrl}api/oauth/callback/${type}?${request.querystring}`
try { try {
const tokenRes = await addon.onCallback({ const tokenRes = await oauthProvider.addon.onCallback({
code: query.code, code: query.code,
state: query.state, state: query.state,
ticketValue, ticketValue,
@@ -108,7 +137,7 @@ export class ConnectController extends BaseController {
const userInfo = tokenRes.userInfo; const userInfo = tokenRes.userInfo;
const validationCode = await this.codeService.setValidationValue({ const validationCode = await this.codeService.setValidationValue({
type, type: getOauthBoundType(type, ticketValue.subtype),
userInfo, userInfo,
}); });
@@ -129,8 +158,10 @@ export class ConnectController extends BaseController {
@Post('/getLogoutUrl', { description: Constants.per.guest }) @Post('/getLogoutUrl', { description: Constants.per.guest })
public async logout(@Body(ALL) body: any) { public async logout(@Body(ALL) body: any) {
checkPlus() checkPlus()
const addon = await this.getOauthProvider(body.type); const oauthProvider = await this.getOauthProvider(body.type);
const { logoutUrl } = await addon.buildLogoutUrl(body); const { logoutUrl } = await oauthProvider.addon.buildLogoutUrl({
...body,
});
return this.ok({ logoutUrl }); return this.ok({ logoutUrl });
} }
@@ -144,7 +175,7 @@ export class ConnectController extends BaseController {
} }
const type = validationValue.type; const type = validationValue.type;
if (type !== body.type) { if (type !== body.type && !type.startsWith(`${body.type}:`)) {
throw new Error("校验码错误"); throw new Error("校验码错误");
} }
const userInfo = validationValue.userInfo; const userInfo = validationValue.userInfo;
@@ -262,16 +293,32 @@ export class ConnectController extends BaseController {
provider.addonId = conf.addonId; provider.addonId = conf.addonId;
provider.addonTitle = addonEntity.name; provider.addonTitle = addonEntity.name;
const addon = await this.addonGetterService.getAddonById(conf.addonId,true,0,null); const addon = await this.addonGetterService.getAddonById(conf.addonId,true,0,null) as IOauthProvider & { icon?: string; types?: OauthProviderType[] };
const {logoutUrl} = await addon.buildLogoutUrl(); const {logoutUrl} = await addon.buildLogoutUrl({});
if (logoutUrl){ if (logoutUrl){
provider.logoutUrl = logoutUrl; provider.logoutUrl = logoutUrl;
} }
if(addon.icon){ if(addon.icon){
provider.icon = addon.icon; provider.icon = addon.icon;
} }
if(addon.types?.length){
provider.types = addon.types;
}
} }
} }
if (provider.addonId && provider.types?.length) {
for (const subtype of provider.types) {
list.push({
...provider,
name: type,
subtype: subtype.type,
title: subtype.name,
icon: subtype.icon || provider.icon,
addonTitle: subtype.name,
});
}
continue;
}
list.push(provider); list.push(provider);
} }
@@ -135,6 +135,7 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
await this.service.savePrivateSettings(privateSettings); await this.service.savePrivateSettings(privateSettings);
return this.ok({}); return this.ok({});
} }
@Post('/stopOtherUserTimer', { description: 'sys:settings:edit' }) @Post('/stopOtherUserTimer', { description: 'sys:settings:edit' })
async stopOtherUserTimer(@Body(ALL) body) { async stopOtherUserTimer(@Body(ALL) body) {
await this.pipelineService.stopOtherUserPipeline(1); await this.pipelineService.stopOtherUserPipeline(1);
@@ -1,12 +1,14 @@
import assert from "assert"; import assert from "assert";
import esmock from "esmock"; import esmock from "esmock";
import { AutoFix, buildEabAccountKeyValue, buildLegacyGoogleAccountConfigWhere, parseStorageValue } from "./auto-fix.js"; import { AutoFix, buildEabAccountKeyValue, buildLegacyGoogleAccountConfigWhere, buildOauthBoundType, parseStorageValue } from "./auto-fix.js";
function createAutoFix(options: { pluginConfigService?: any; accessService?: any; storageService?: any }) { function createAutoFix(options: { pluginConfigService?: any; accessService?: any; storageService?: any; sysSettingsService?: any; oauthBoundService?: any }) {
const autoFix = new AutoFix(); const autoFix = new AutoFix();
autoFix.pluginConfigService = options.pluginConfigService; autoFix.pluginConfigService = options.pluginConfigService;
autoFix.accessService = options.accessService; autoFix.accessService = options.accessService;
autoFix.storageService = options.storageService; autoFix.storageService = options.storageService;
autoFix.sysSettingsService = options.sysSettingsService;
autoFix.oauthBoundService = options.oauthBoundService;
return autoFix; return autoFix;
} }
@@ -42,6 +44,11 @@ describe("AutoFix", () => {
}); });
}); });
it("builds OAuth subtype bound type", () => {
assert.equal(buildOauthBoundType("clogin", "alipay"), "clogin:alipay");
assert.equal(buildOauthBoundType("github"), "github");
});
it("finds legacy Google account config by exact email key only", async () => { it("finds legacy Google account config by exact email key only", async () => {
let findOneWhere: any; let findOneWhere: any;
let findCalled = false; let findCalled = false;
@@ -107,6 +114,25 @@ describe("AutoFix", () => {
} as any, } as any,
accessService: null as any, accessService: null as any,
storageService: null as any, storageService: null as any,
sysSettingsService: {
async getPublicSettings() {
return {
oauthProviders: {},
};
},
},
oauthBoundService: {
async transaction(callback: any) {
return await callback({
async findOne() {
return null;
},
async update() {
return { affected: 0 };
},
});
},
},
}); });
await autoFix.init(); await autoFix.init();
@@ -179,4 +205,97 @@ describe("AutoFix", () => {
}); });
}); });
it("fixes legacy OAuth bound type from string addon loginType and converts loginType to array", async () => {
const updates: any[] = [];
const autoFix = createAutoFix({
pluginConfigService: null as any,
accessService: null as any,
storageService: null as any,
sysSettingsService: {
async getPublicSettings() {
return {
oauthProviders: {
clogin: {
addonId: 1,
},
},
};
},
},
oauthBoundService: {
async transaction(callback: any) {
return await callback({
async findOne(entity: any, options: any) {
assert.equal(entity.name, "AddonEntity");
assert.deepEqual(options, { where: { id: 1 } });
return {
id: 1,
setting: JSON.stringify({
loginType: "alipay",
}),
};
},
async update(entity: any, where: any, value: any) {
updates.push({ entity: entity.name, where, value });
return { affected: entity.name === "OauthBoundEntity" ? 1 : 0 };
},
});
},
},
});
await autoFix.fixOauthSubtypeBoundType();
assert.deepEqual(updates[0], {
entity: "OauthBoundEntity",
where: { type: "clogin" },
value: { type: "clogin:alipay" },
});
assert.equal(updates[1].entity, "AddonEntity");
assert.deepEqual(updates[1].where, { id: 1 });
assert.deepEqual(JSON.parse(updates[1].value.setting).loginType, ["alipay"]);
});
it("skips OAuth subtype fix when addon loginType is already not legacy string", async () => {
let updateCalled = false;
const autoFix = createAutoFix({
pluginConfigService: null as any,
accessService: null as any,
storageService: null as any,
sysSettingsService: {
async getPublicSettings() {
return {
oauthProviders: {
clogin: {
addonId: 1,
},
},
};
},
},
oauthBoundService: {
async transaction(callback: any) {
return await callback({
async findOne() {
return {
id: 1,
setting: JSON.stringify({
loginType: ["alipay", "github"],
}),
};
},
async update() {
updateCalled = true;
return { affected: 0 };
},
});
},
},
});
await autoFix.fixOauthSubtypeBoundType();
assert.equal(updateCalled, false);
});
}); });
@@ -1,9 +1,11 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic"; import { logger } from "@certd/basic";
import { AccessService } from "@certd/lib-server"; import { AccessService, AddonEntity, SysSettingsService } from "@certd/lib-server";
import { isComm } from "@certd/plus-core"; import { isComm } from "@certd/plus-core";
import { PluginConfigService } from "../plugin/service/plugin-config-service.js"; import { PluginConfigService } from "../plugin/service/plugin-config-service.js";
import { StorageService } from "../pipeline/service/storage-service.js"; import { StorageService } from "../pipeline/service/storage-service.js";
import { OauthBoundService } from "../login/service/oauth-bound-service.js";
import { OauthBoundEntity } from "../login/entity/oauth-bound.js";
export function parseStorageValue(value?: string) { export function parseStorageValue(value?: string) {
if (!value) { if (!value) {
@@ -33,6 +35,10 @@ export function buildLegacyGoogleAccountConfigWhere(email: string) {
}; };
} }
export function buildOauthBoundType(type: string, subtype?: string) {
return subtype ? `${type}:${subtype}` : type;
}
@Provide() @Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoFix { export class AutoFix {
@@ -45,9 +51,66 @@ export class AutoFix {
@Inject() @Inject()
storageService: StorageService; storageService: StorageService;
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
oauthBoundService: OauthBoundService;
async init() { async init() {
await this.fixGoogleCommonEabAccountKey(); await this.fixGoogleCommonEabAccountKey();
await this.fixOauthSubtypeBoundType();
} }
async fixOauthSubtypeBoundType() {
try {
const publicSettings = await this.sysSettingsService.getPublicSettings();
const oauthProviders = publicSettings.oauthProviders || {};
await this.oauthBoundService.transaction(async manager => {
for (const [type, provider] of Object.entries(oauthProviders)) {
if (!provider.addonId) {
continue;
}
const addonEntity = await manager.findOne(AddonEntity, { where: { id: provider.addonId } });
const legacyLoginType = this.getLegacyAddonLoginType(addonEntity?.setting);
if (!legacyLoginType) {
continue;
}
const newType = buildOauthBoundType(type, legacyLoginType);
const res = await manager.update(OauthBoundEntity, { type }, { type: newType });
if (res.affected) {
logger.info(`已修复OAuth绑定历史数据,${type} -> ${newType},数量=${res.affected}`);
}
await this.convertLegacyAddonLoginTypeToArray(addonEntity, legacyLoginType, manager);
}
});
} catch (e: any) {
logger.error("修复OAuth subtype绑定历史数据失败", e);
}
}
private getLegacyAddonLoginType(settingValue?: string) {
if (!settingValue) {
return null;
}
const setting = JSON.parse(settingValue);
return typeof setting.loginType === "string" && setting.loginType ? setting.loginType : null;
}
private async convertLegacyAddonLoginTypeToArray(addonEntity: AddonEntity | null, loginType: string, manager: any) {
if (!addonEntity?.setting) {
return;
}
const setting = JSON.parse(addonEntity.setting);
if (typeof setting.loginType !== "string") {
return;
}
setting.loginType = [loginType];
await manager.update(AddonEntity, { id: addonEntity.id }, { setting: JSON.stringify(setting) });
}
async fixGoogleCommonEabAccountKey() { async fixGoogleCommonEabAccountKey() {
if (!isComm()) { if (!isComm()) {
return; return;
@@ -41,9 +41,11 @@ export type BuildLoginUrlReq = {
redirectUri: string; redirectUri: string;
forType?: string; forType?: string;
from?:string; from?:string;
subtype?: string;
} }
export type BuildLogoutUrlReq = { export type BuildLogoutUrlReq = {
subtype?: string;
} }
export type LogoutUrlReply = { export type LogoutUrlReply = {
@@ -1,6 +1,31 @@
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server"; import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
import { BuildLoginUrlReq, BuildLogoutUrlReq, IOauthProvider, OnCallbackReq } from "../api.js"; import { BuildLoginUrlReq, BuildLogoutUrlReq, IOauthProvider, OnCallbackReq } from "../api.js";
import { IconSets } from "../iconsets.js";
const CLOGIN_TYPES = [
{ label: "QQ", value: "qq", icon: "cib:tencent-qq:#007AFF" },
{ label: "微信", value: "wx", icon: "simple-icons:wechat:#34C759" },
{ label: "支付宝", value: "alipay", icon: "simple-icons:alipay:#0099ff" },
{ label: "微博", value: "sina", icon: "uiw:weibo:#FF3B30" },
{ label: "百度", value: "baidu", icon: "simple-icons:baidu:#007AFF" },
{ label: "华为", value: "huawei", icon: "simple-icons:huawei:#ff0000" },
{ label: "小米", value: "xiaomi", icon: "simple-icons:xiaomi:#FF9500" },
{ label: "谷歌", value: "google", icon: "flat-color-icons:google" },
{ label: "微软", value: "microsoft", icon: "logos:microsoft-icon" },
{ label: "Facebook", value: "facebook", icon: "logos:facebook" },
{ label: "Twitter", value: "twitter", icon: "logos:twitter" },
{ label: "钉钉", value: "dingtalk", icon: "ant-design:dingding-outlined:#007AFF" },
{ label: "Gitee", value: "gitee", icon: "simple-icons:gitee:#c71d23" },
{ label: "Github", value: "github", icon: "logos:github-icon" },
];
function getCloginType(subtype?: string, loginType?: string | string[]) {
const types = Array.isArray(loginType) ? loginType : [loginType];
const type = subtype || types.find(item => !!item);
if (!type) {
throw new Error("请选择彩虹聚合登录类型");
}
return type;
}
@IsAddon({ @IsAddon({
addonType: "oauth", addonType: "oauth",
@@ -22,38 +47,27 @@ export class CloginOauthProvider extends BaseAddon implements IOauthProvider {
@AddonInput({ @AddonInput({
title: "登录类型", title: "登录类型",
component: { component: {
name: "a-auto-complete", name: "a-select",
options: [ vModel: "value",
{ label: "QQ", value: "qq" }, mode: "tags",
{ label: "微信", value: "wx" }, multiple: true,
{ label: "支付宝", value: "alipay" }, options: CLOGIN_TYPES,
{ label: "微博", value: "sina" },
{ label: "百度", value: "baidu" },
{ label: "华为", value: "huawei" },
{ label: "小米", value: "xiaomi" },
{ label: "谷歌", value: "google" },
{ label: "微软", value: "microsoft" },
{ label: "Facebook", value: "facebook" },
{ label: "Twitter", value: "twitter" },
{ label: "钉钉", value: "dingtalk" },
{ label: "Gitee", value: "gitee" },
{ label: "Github", value: "github" },
]
}, },
required: true, required: true,
}) })
loginType = ""; loginType: string[] | string = [];
@AddonInput({ get types() {
title: "自定义图标", const loginTypes = Array.isArray(this.loginType) ? this.loginType : [this.loginType].filter(Boolean);
component: { return loginTypes.map(type => {
name:"fs-icon-selector", const option = CLOGIN_TYPES.find(item => item.value === type);
vModel:"modelValue", return {
iconSets: IconSets, type,
}, name: option?.label || type,
required: false, icon: option?.icon,
}) };
icon = ""; });
}
@AddonInput({ @AddonInput({
title: "AppId", title: "AppId",
@@ -75,11 +89,12 @@ export class CloginOauthProvider extends BaseAddon implements IOauthProvider {
async buildLoginUrl(params: BuildLoginUrlReq) { async buildLoginUrl(params: BuildLoginUrlReq) {
let redirectUri = params.redirectUri || "" let redirectUri = params.redirectUri || ""
const loginType = getCloginType(params.subtype, this.loginType);
// if(redirectUri.indexOf("localhost:3008")>=0){ // if(redirectUri.indexOf("localhost:3008")>=0){
// redirectUri = redirectUri.replace("localhost:3008", "certd.handfree.work") // redirectUri = redirectUri.replace("localhost:3008", "certd.handfree.work")
// } // }
const res = await this.ctx.http.request({ const res = await this.ctx.http.request({
url: `${this.endpoint}/connect.php?act=login&appid=${this.appId}&appkey=${this.appKey}&type=${this.loginType}&redirect_uri=${redirectUri}` url: `${this.endpoint}/connect.php?act=login&appid=${this.appId}&appkey=${this.appKey}&type=${loginType}&redirect_uri=${redirectUri}`
}) })
this.checkRes(res) this.checkRes(res)
@@ -103,8 +118,9 @@ export class CloginOauthProvider extends BaseAddon implements IOauthProvider {
//校验state //校验state
const code = req.code || "" const code = req.code || ""
const loginType = getCloginType(req.ticketValue?.subtype, this.loginType);
const tokenEndpoint = `${this.endpoint}/connect.php?act=callback&appid=${this.appId}&appkey=${this.appKey}&type=${this.loginType}&code=${code}` const tokenEndpoint = `${this.endpoint}/connect.php?act=callback&appid=${this.appId}&appkey=${this.appKey}&type=${loginType}&code=${code}`
const res = await this.ctx.utils.http.request({ const res = await this.ctx.utils.http.request({
url: tokenEndpoint, url: tokenEndpoint,
method: "post", method: "post",
@@ -63,6 +63,7 @@ export class SynologyAccess extends BaseAccess {
name: "a-switch", name: "a-switch",
vModel: "checked", vModel: "checked",
}, },
col:{span:24},
helper: "是否启用了双重认证", helper: "是否启用了双重认证",
required: true, required: true,
}) })
@@ -76,6 +77,7 @@ export class SynologyAccess extends BaseAccess {
type: "access", type: "access",
typeName: "synology", typeName: "synology",
}, },
col:{span:24},
mergeScript: ` mergeScript: `
return { return {
component:{ component:{
+47 -108
View File
@@ -52,7 +52,7 @@ importers:
packages/core/acme-client: packages/core/acme-client:
dependencies: dependencies:
'@certd/basic': '@certd/basic':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../basic version: link:../basic
'@peculiar/x509': '@peculiar/x509':
specifier: ^1.11.0 specifier: ^1.11.0
@@ -234,11 +234,11 @@ importers:
packages/core/pipeline: packages/core/pipeline:
dependencies: dependencies:
'@certd/basic': '@certd/basic':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../basic version: link:../basic
'@certd/plus-core': '@certd/plus-core':
specifier: ^1.39.14 specifier: ^1.39.16
version: 1.39.14 version: link:../../pro/plus-core
dayjs: dayjs:
specifier: ^1.11.7 specifier: ^1.11.7
version: 1.11.13 version: 1.11.13
@@ -457,7 +457,7 @@ importers:
packages/libs/lib-k8s: packages/libs/lib-k8s:
dependencies: dependencies:
'@certd/basic': '@certd/basic':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/basic version: link:../../core/basic
'@kubernetes/client-node': '@kubernetes/client-node':
specifier: 0.21.0 specifier: 0.21.0
@@ -503,20 +503,20 @@ importers:
packages/libs/lib-server: packages/libs/lib-server:
dependencies: dependencies:
'@certd/acme-client': '@certd/acme-client':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/acme-client version: link:../../core/acme-client
'@certd/basic': '@certd/basic':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/basic version: link:../../core/basic
'@certd/pipeline': '@certd/pipeline':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/pipeline version: link:../../core/pipeline
'@certd/plugin-lib': '@certd/plugin-lib':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../plugins/plugin-lib version: link:../../plugins/plugin-lib
'@certd/plus-core': '@certd/plus-core':
specifier: ^1.39.14 specifier: ^1.39.16
version: 1.39.14 version: link:../../pro/plus-core
'@midwayjs/cache': '@midwayjs/cache':
specifier: 3.14.0 specifier: 3.14.0
version: 3.14.0 version: 3.14.0
@@ -679,16 +679,16 @@ importers:
packages/plugins/plugin-cert: packages/plugins/plugin-cert:
dependencies: dependencies:
'@certd/acme-client': '@certd/acme-client':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/acme-client version: link:../../core/acme-client
'@certd/basic': '@certd/basic':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/basic version: link:../../core/basic
'@certd/pipeline': '@certd/pipeline':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/pipeline version: link:../../core/pipeline
'@certd/plugin-lib': '@certd/plugin-lib':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../plugin-lib version: link:../plugin-lib
psl: psl:
specifier: ^1.9.0 specifier: ^1.9.0
@@ -758,17 +758,17 @@ importers:
specifier: ^3.964.0 specifier: ^3.964.0
version: 3.964.0(aws-crt@1.26.2) version: 3.964.0(aws-crt@1.26.2)
'@certd/acme-client': '@certd/acme-client':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/acme-client version: link:../../core/acme-client
'@certd/basic': '@certd/basic':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/basic version: link:../../core/basic
'@certd/pipeline': '@certd/pipeline':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/pipeline version: link:../../core/pipeline
'@certd/plus-core': '@certd/plus-core':
specifier: ^1.39.14 specifier: ^1.39.16
version: 1.39.14 version: link:../../pro/plus-core
'@kubernetes/client-node': '@kubernetes/client-node':
specifier: 0.21.0 specifier: 0.21.0
version: 0.21.0 version: 0.21.0
@@ -867,16 +867,16 @@ importers:
packages/pro/commercial-core: packages/pro/commercial-core:
dependencies: dependencies:
'@certd/basic': '@certd/basic':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/basic version: link:../../core/basic
'@certd/lib-server': '@certd/lib-server':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../libs/lib-server version: link:../../libs/lib-server
'@certd/pipeline': '@certd/pipeline':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/pipeline version: link:../../core/pipeline
'@certd/plus-core': '@certd/plus-core':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../plus-core version: link:../plus-core
'@midwayjs/core': '@midwayjs/core':
specifier: 3.20.11 specifier: 3.20.11
@@ -967,16 +967,16 @@ importers:
packages/pro/plugin-plus: packages/pro/plugin-plus:
dependencies: dependencies:
'@certd/basic': '@certd/basic':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/basic version: link:../../core/basic
'@certd/pipeline': '@certd/pipeline':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/pipeline version: link:../../core/pipeline
'@certd/plugin-lib': '@certd/plugin-lib':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../plugins/plugin-lib version: link:../../plugins/plugin-lib
'@certd/plus-core': '@certd/plus-core':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../plus-core version: link:../plus-core
crypto-js: crypto-js:
specifier: ^4.2.0 specifier: ^4.2.0
@@ -1061,7 +1061,7 @@ importers:
packages/pro/plus-core: packages/pro/plus-core:
dependencies: dependencies:
'@certd/basic': '@certd/basic':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/basic version: link:../../core/basic
dayjs: dayjs:
specifier: ^1.11.7 specifier: ^1.11.7
@@ -1363,10 +1363,10 @@ importers:
version: 0.1.3(zod@3.24.4) version: 0.1.3(zod@3.24.4)
devDependencies: devDependencies:
'@certd/lib-iframe': '@certd/lib-iframe':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../libs/lib-iframe version: link:../../libs/lib-iframe
'@certd/pipeline': '@certd/pipeline':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/pipeline version: link:../../core/pipeline
'@rollup/plugin-commonjs': '@rollup/plugin-commonjs':
specifier: ^25.0.7 specifier: ^25.0.7
@@ -1573,47 +1573,47 @@ importers:
specifier: ^4.13.1 specifier: ^4.13.1
version: 4.13.1 version: 4.13.1
'@certd/acme-client': '@certd/acme-client':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/acme-client version: link:../../core/acme-client
'@certd/basic': '@certd/basic':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/basic version: link:../../core/basic
'@certd/commercial-core': '@certd/commercial-core':
specifier: ^1.39.14 specifier: ^1.39.16
version: 1.39.14(better-sqlite3@11.10.0)(mysql2@3.14.1)(pg@8.16.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@18.19.100)(typescript@5.9.3)) version: link:../../pro/commercial-core
'@certd/cv4pve-api-javascript': '@certd/cv4pve-api-javascript':
specifier: ^8.4.2 specifier: ^8.4.2
version: 8.4.2 version: 8.4.2
'@certd/jdcloud': '@certd/jdcloud':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../libs/lib-jdcloud version: link:../../libs/lib-jdcloud
'@certd/lib-huawei': '@certd/lib-huawei':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../libs/lib-huawei version: link:../../libs/lib-huawei
'@certd/lib-k8s': '@certd/lib-k8s':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../libs/lib-k8s version: link:../../libs/lib-k8s
'@certd/lib-server': '@certd/lib-server':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../libs/lib-server version: link:../../libs/lib-server
'@certd/midway-flyway-js': '@certd/midway-flyway-js':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../libs/midway-flyway-js version: link:../../libs/midway-flyway-js
'@certd/pipeline': '@certd/pipeline':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../core/pipeline version: link:../../core/pipeline
'@certd/plugin-cert': '@certd/plugin-cert':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../plugins/plugin-cert version: link:../../plugins/plugin-cert
'@certd/plugin-lib': '@certd/plugin-lib':
specifier: ^1.39.14 specifier: ^1.39.16
version: link:../../plugins/plugin-lib version: link:../../plugins/plugin-lib
'@certd/plugin-plus': '@certd/plugin-plus':
specifier: ^1.39.14 specifier: ^1.39.16
version: 1.39.14 version: link:../../pro/plugin-plus
'@certd/plus-core': '@certd/plus-core':
specifier: ^1.39.14 specifier: ^1.39.16
version: 1.39.14 version: link:../../pro/plus-core
'@google-cloud/dns': '@google-cloud/dns':
specifier: ^5.3.1 specifier: ^5.3.1
version: 5.3.1 version: 5.3.1
@@ -3033,18 +3033,9 @@ packages:
'@better-scroll/zoom@2.5.1': '@better-scroll/zoom@2.5.1':
resolution: {integrity: sha512-aGvFY5ooeZWS4RcxQLD+pGLpQHQxpPy0sMZV3yadcd2QK53PK9gS4Dp+BYfRv8lZ4/P2LoNEhr6Wq1DN6+uPlA==} resolution: {integrity: sha512-aGvFY5ooeZWS4RcxQLD+pGLpQHQxpPy0sMZV3yadcd2QK53PK9gS4Dp+BYfRv8lZ4/P2LoNEhr6Wq1DN6+uPlA==}
'@certd/commercial-core@1.39.14':
resolution: {integrity: sha512-Tys+rjy1zuATSwjqpFKpKCYpz6RoC3gIGYcVjD+qKvTabTSeChvwRjvDvzSiyWpU5iHm6uT+7tpPTc0/XXFvBg==}
'@certd/cv4pve-api-javascript@8.4.2': '@certd/cv4pve-api-javascript@8.4.2':
resolution: {integrity: sha512-udGce7ewrVl4DmZvX+17PjsnqsdDIHEDatr8QP0AVrY2p+8JkaSPW4mXCKiLGf82C9K2+GXgT+qNIqgW7tfF9Q==} resolution: {integrity: sha512-udGce7ewrVl4DmZvX+17PjsnqsdDIHEDatr8QP0AVrY2p+8JkaSPW4mXCKiLGf82C9K2+GXgT+qNIqgW7tfF9Q==}
'@certd/plugin-plus@1.39.14':
resolution: {integrity: sha512-79PX/YmaCqst5StYAB9WfbOhrEAeGbO9ypeSJTTsZhNkqbYEAlqnk/6upEpgdxnHxQ+WNH8DlInMCdJhX26HDw==}
'@certd/plus-core@1.39.14':
resolution: {integrity: sha512-GRJi9mBjrtfng1NbEeKe75AJ6sbfNCUKo1I6G2oYC8DdlGlJ/7XblZ44lwEWMa1IHB2ere51i9TqmZtosmt1FA==}
'@certd/vue-js-cron-core@6.0.3': '@certd/vue-js-cron-core@6.0.3':
resolution: {integrity: sha512-kqzoAMhYz9j6FGNWEODRYtt4NpUEUwjpkU89z5WVg2tCtOcI5VhwyUGOd8AxiBCRfd6PtXvzuqw85PaOps9wrQ==} resolution: {integrity: sha512-kqzoAMhYz9j6FGNWEODRYtt4NpUEUwjpkU89z5WVg2tCtOcI5VhwyUGOd8AxiBCRfd6PtXvzuqw85PaOps9wrQ==}
@@ -15573,64 +15564,12 @@ snapshots:
dependencies: dependencies:
'@better-scroll/core': 2.5.1 '@better-scroll/core': 2.5.1
'@certd/commercial-core@1.39.14(better-sqlite3@11.10.0)(mysql2@3.14.1)(pg@8.16.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@18.19.100)(typescript@5.9.3))':
dependencies:
'@certd/basic': link:packages/core/basic
'@certd/lib-server': link:packages/libs/lib-server
'@certd/pipeline': link:packages/core/pipeline
'@certd/plus-core': 1.39.14
'@midwayjs/core': 3.20.11
'@midwayjs/koa': 3.20.13
'@midwayjs/logger': 3.4.2
'@midwayjs/swagger': 3.20.11
'@midwayjs/typeorm': 3.20.11
dayjs: 1.11.13
typeorm: 0.3.24(better-sqlite3@11.10.0)(mysql2@3.14.1)(pg@8.16.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@18.19.100)(typescript@5.9.3))
transitivePeerDependencies:
- '@google-cloud/spanner'
- '@sap/hana-client'
- babel-plugin-macros
- better-sqlite3
- hdb-pool
- ioredis
- mongodb
- mssql
- mysql2
- oracledb
- pg
- pg-native
- pg-query-stream
- redis
- reflect-metadata
- sql.js
- sqlite3
- supports-color
- ts-node
- typeorm-aurora-data-api-driver
'@certd/cv4pve-api-javascript@8.4.2': '@certd/cv4pve-api-javascript@8.4.2':
dependencies: dependencies:
debug: 4.4.3(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@certd/plugin-plus@1.39.14':
dependencies:
'@certd/basic': link:packages/core/basic
'@certd/pipeline': link:packages/core/pipeline
'@certd/plugin-lib': link:packages/plugins/plugin-lib
'@certd/plus-core': 1.39.14
crypto-js: 4.2.0
dayjs: 1.11.13
form-data: 4.0.2
jsrsasign: 11.1.0
querystring: 0.2.1
'@certd/plus-core@1.39.14':
dependencies:
'@certd/basic': link:packages/core/basic
dayjs: 1.11.13
'@certd/vue-js-cron-core@6.0.3': '@certd/vue-js-cron-core@6.0.3':
dependencies: dependencies:
mustache: 4.2.0 mustache: 4.2.0
+89
View File
@@ -0,0 +1,89 @@
# Certd OpenAPI SDK 示例
本目录提供 `/api/v1/cert/get` 接口的多语言 SDK 示例,演示如何封装 `CertdClient`,生成 `x-certd-token`,并按域名或证书 ID 获取证书。
## 目录结构
| 语言 | SDK 类/类型 | 调用示例 |
| --- | --- | --- |
| Node.js | `sdk/nodejs/certd-client.js` | `sdk/nodejs/get-cert.js` |
| Python | `sdk/python/certd_client.py` | `sdk/python/get_cert.py` |
| Go | `sdk/go/certd_client.go` | `sdk/go/get_cert.go` |
| PHP | `sdk/php/CertdClient.php` | `sdk/php/get_cert.php` |
| Java | `sdk/java/CertdClient.java` | `sdk/java/GetCert.java` |
`CertdClient` 提供以下核心方法:
- `getSign(content)` / `GetSign(content)`:根据 `content + keySecret` 生成签名
- `getToken()` / `GetToken()`:生成 `x-certd-token`
- `request(path, body)` / `Request(path, body)`:携带 token 发起 OpenAPI 请求
- `getCert(params)` / `GetCert(params)`:调用 `/api/v1/cert/get` 的便捷方法
## 接口说明
- 请求地址:`POST /api/v1/cert/get`
- 认证方式:请求头传入 `x-certd-token`
- `certId``domains` 至少传一个;两个都传时,服务端优先使用 `certId`
- `autoApply=true` 时,如果没有可用证书,会尝试自动创建或触发流水线申请证书
- `format` 可选:`pem``jks``pfx``der``one``p7b`
## Token 生成规则
1. 在 OpenKey 页面生成 `keyId``keySecret`
2. 构造 JSON 字符串:
```json
{"keyId":"你的 keyId","t":1710000000,"encrypt":false,"signType":"md5"}
```
3. 签名:`sign = md5(content + keySecret)`
4. Header`x-certd-token = base64(content) + "." + base64(sign)`
注意:签名时必须使用和 base64 编码时完全相同的 `content` 字符串。
## 环境变量
所有示例都支持以下环境变量:
| 变量 | 说明 | 默认值 |
| --- | --- | --- |
| `CERTD_BASE_URL` | Certd 服务地址 | `http://127.0.0.1:7001` |
| `CERTD_KEY_ID` | OpenKey 的 keyId | 必填 |
| `CERTD_KEY_SECRET` | OpenKey 的 keySecret | 必填 |
| `CERTD_DOMAINS` | 域名列表,多个用英文逗号隔开 | 空 |
| `CERTD_CERT_ID` | 证书仓库证书 ID | 空 |
| `CERTD_AUTO_APPLY` | 不存在或过期时是否自动申请 | `false` |
| `CERTD_FORMAT` | 返回证书格式 | 空,表示返回所有格式 |
| `CERTD_ENCRYPT` | 是否要求接口加密返回结果 | `false` |
## 运行示例
PowerShell
```powershell
$env:CERTD_BASE_URL = "http://127.0.0.1:7001"
$env:CERTD_KEY_ID = "your_key_id"
$env:CERTD_KEY_SECRET = "your_key_secret"
$env:CERTD_DOMAINS = "example.com,*.example.com"
$env:CERTD_AUTO_APPLY = "true"
$env:CERTD_FORMAT = "pem"
node sdk\nodejs\get-cert.js
python sdk\python\get_cert.py
go run sdk\go\certd_client.go sdk\go\get_cert.go
php sdk\php\get_cert.php
javac sdk\java\CertdClient.java sdk\java\GetCert.java && java -cp sdk\java GetCert
```
如果使用 `CERTD_CERT_ID` 获取证书,可以不传 `CERTD_DOMAINS`
```powershell
$env:CERTD_CERT_ID = "1"
$env:CERTD_DOMAINS = ""
```
## 返回结果
`CERTD_ENCRYPT=false` 时,示例会直接打印接口返回的 JSON。
`CERTD_ENCRYPT=true` 时,接口返回内容会被服务端加密;这些示例只演示请求和 token 生成,不包含解密逻辑。
+107
View File
@@ -0,0 +1,107 @@
package main
import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
type CertdClient struct {
KeyID string
KeySecret string
BaseURL string
Encrypt bool
SignType string
}
type tokenContent struct {
KeyID string `json:"keyId"`
T int64 `json:"t"`
Encrypt bool `json:"encrypt"`
SignType string `json:"signType"`
}
func NewCertdClient(keyID, keySecret string) (*CertdClient, error) {
if keyID == "" {
return nil, fmt.Errorf("keyID is required")
}
if keySecret == "" {
return nil, fmt.Errorf("keySecret is required")
}
return &CertdClient{
KeyID: keyID,
KeySecret: keySecret,
BaseURL: "http://127.0.0.1:7001",
SignType: "md5",
}, nil
}
func (c *CertdClient) GetSign(content string) (string, error) {
if c.SignType != "md5" {
return "", fmt.Errorf("unsupported signType: %s", c.SignType)
}
sum := md5.Sum([]byte(content + c.KeySecret))
return hex.EncodeToString(sum[:]), nil
}
func (c *CertdClient) GetToken() (string, error) {
contentBytes, err := json.Marshal(tokenContent{
KeyID: c.KeyID,
T: time.Now().Unix(),
Encrypt: c.Encrypt,
SignType: c.SignType,
})
if err != nil {
return "", err
}
content := string(contentBytes)
sign, err := c.GetSign(content)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString([]byte(content)) + "." + base64.StdEncoding.EncodeToString([]byte(sign)), nil
}
func (c *CertdClient) Request(path string, body any) ([]byte, error) {
bodyBytes, err := json.Marshal(body)
if err != nil {
return nil, err
}
token, err := c.GetToken()
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest(http.MethodPost, strings.TrimRight(c.BaseURL, "/")+path, bytes.NewReader(bodyBytes))
if err != nil {
return nil, err
}
httpReq.Header.Set("content-type", "application/json")
httpReq.Header.Set("x-certd-token", token)
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
}
return respBody, nil
}
func (c *CertdClient) GetCert(params any) ([]byte, error) {
return c.Request("/api/v1/cert/get", params)
}
+85
View File
@@ -0,0 +1,85 @@
package main
import (
"fmt"
"os"
"strconv"
"strings"
)
type certGetRequest struct {
Domains string `json:"domains,omitempty"`
CertID int64 `json:"certId,omitempty"`
AutoApply bool `json:"autoApply"`
Format string `json:"format,omitempty"`
}
func requireEnv(name string) (string, error) {
value := os.Getenv(name)
if value == "" {
return "", fmt.Errorf("missing environment variable: %s", name)
}
return value, nil
}
func boolEnv(name string, defaultValue bool) bool {
value := os.Getenv(name)
if value == "" {
return defaultValue
}
switch strings.ToLower(value) {
case "1", "true", "yes", "y":
return true
default:
return false
}
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
keyID, err := requireEnv("CERTD_KEY_ID")
if err != nil {
return err
}
keySecret, err := requireEnv("CERTD_KEY_SECRET")
if err != nil {
return err
}
client, err := NewCertdClient(keyID, keySecret)
if err != nil {
return err
}
if baseURL := os.Getenv("CERTD_BASE_URL"); baseURL != "" {
client.BaseURL = baseURL
}
client.Encrypt = boolEnv("CERTD_ENCRYPT", false)
reqBody := certGetRequest{
Domains: os.Getenv("CERTD_DOMAINS"),
AutoApply: boolEnv("CERTD_AUTO_APPLY", false),
Format: os.Getenv("CERTD_FORMAT"),
}
if certID := os.Getenv("CERTD_CERT_ID"); certID != "" {
reqBody.CertID, err = strconv.ParseInt(certID, 10, 64)
if err != nil || reqBody.CertID <= 0 {
return fmt.Errorf("CERTD_CERT_ID must be a positive integer")
}
}
if reqBody.CertID == 0 && reqBody.Domains == "" {
return fmt.Errorf("set CERTD_CERT_ID or CERTD_DOMAINS")
}
respBody, err := client.GetCert(reqBody)
if err != nil {
return err
}
fmt.Println(string(respBody))
return nil
}
+127
View File
@@ -0,0 +1,127 @@
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;
public class CertdClient {
private final String keyId;
private final String keySecret;
private final String baseUrl;
private final boolean encrypt;
private final String signType;
public CertdClient(String keyId, String keySecret) {
this(keyId, keySecret, "http://127.0.0.1:7001", false);
}
public CertdClient(String keyId, String keySecret, String baseUrl, boolean encrypt) {
if (isBlank(keyId)) {
throw new IllegalArgumentException("keyId is required");
}
if (isBlank(keySecret)) {
throw new IllegalArgumentException("keySecret is required");
}
this.keyId = keyId;
this.keySecret = keySecret;
this.baseUrl = trimRightSlash(isBlank(baseUrl) ? "http://127.0.0.1:7001" : baseUrl);
this.encrypt = encrypt;
this.signType = "md5";
}
public String getSign(String content) throws Exception {
if (!"md5".equals(signType)) {
throw new IllegalArgumentException("Unsupported signType: " + signType);
}
return md5Hex(content + keySecret);
}
public String getToken() throws Exception {
String content = "{\"keyId\":\"" + jsonEscape(keyId) + "\",\"t\":" + Instant.now().getEpochSecond()
+ ",\"encrypt\":" + encrypt + ",\"signType\":\"" + signType + "\"}";
String sign = getSign(content);
return base64(content) + "." + base64(sign);
}
public String request(String path, String bodyJson) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + path))
.header("content-type", "application/json")
.header("x-certd-token", getToken())
.POST(HttpRequest.BodyPublishers.ofString(bodyJson, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
}
return response.body();
}
public String getCert(String paramsJson) throws Exception {
return request("/api/v1/cert/get", paramsJson);
}
public static String jsonEscape(String value) {
StringBuilder escaped = new StringBuilder();
for (int i = 0; i < value.length(); i++) {
char ch = value.charAt(i);
switch (ch) {
case '"':
escaped.append("\\\"");
break;
case '\\':
escaped.append("\\\\");
break;
case '\b':
escaped.append("\\b");
break;
case '\f':
escaped.append("\\f");
break;
case '\n':
escaped.append("\\n");
break;
case '\r':
escaped.append("\\r");
break;
case '\t':
escaped.append("\\t");
break;
default:
if (ch < 0x20) {
escaped.append(String.format("\\u%04x", (int) ch));
} else {
escaped.append(ch);
}
}
}
return escaped.toString();
}
private static String md5Hex(String value) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(value.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : digest) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
private static String base64(String value) {
return Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8));
}
private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
private static String trimRightSlash(String value) {
return value.replaceAll("/+$", "");
}
}
+84
View File
@@ -0,0 +1,84 @@
public class GetCert {
public static void main(String[] args) {
try {
new GetCert().run();
} catch (Exception e) {
System.err.println(e.getMessage());
System.exit(1);
}
}
private void run() throws Exception {
CertdClient client = new CertdClient(
requireEnv("CERTD_KEY_ID"),
requireEnv("CERTD_KEY_SECRET"),
env("CERTD_BASE_URL", "http://127.0.0.1:7001"),
boolEnv("CERTD_ENCRYPT", false)
);
String certId = System.getenv("CERTD_CERT_ID");
String domains = System.getenv("CERTD_DOMAINS");
String format = System.getenv("CERTD_FORMAT");
if (isBlank(certId) && isBlank(domains)) {
throw new IllegalArgumentException("Set CERTD_CERT_ID or CERTD_DOMAINS");
}
StringBuilder body = new StringBuilder();
body.append("{");
boolean hasField = false;
if (!isBlank(certId)) {
body.append("\"certId\":").append(Long.parseLong(certId));
hasField = true;
}
if (!isBlank(domains)) {
appendComma(body, hasField);
body.append("\"domains\":\"").append(CertdClient.jsonEscape(domains)).append("\"");
hasField = true;
}
appendComma(body, hasField);
body.append("\"autoApply\":").append(boolEnv("CERTD_AUTO_APPLY", false));
hasField = true;
if (!isBlank(format)) {
appendComma(body, hasField);
body.append("\"format\":\"").append(CertdClient.jsonEscape(format)).append("\"");
}
body.append("}");
System.out.println(client.getCert(body.toString()));
}
private static String requireEnv(String name) {
String value = System.getenv(name);
if (isBlank(value)) {
throw new IllegalArgumentException("Missing environment variable: " + name);
}
return value;
}
private static String env(String name, String defaultValue) {
String value = System.getenv(name);
return isBlank(value) ? defaultValue : value;
}
private static boolean boolEnv(String name, boolean defaultValue) {
String value = System.getenv(name);
if (isBlank(value)) {
return defaultValue;
}
return value.equalsIgnoreCase("1")
|| value.equalsIgnoreCase("true")
|| value.equalsIgnoreCase("yes")
|| value.equalsIgnoreCase("y");
}
private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
private static void appendComma(StringBuilder builder, boolean hasField) {
if (hasField) {
builder.append(",");
}
}
}
+60
View File
@@ -0,0 +1,60 @@
const crypto = require("crypto");
class CertdClient {
constructor(keyId, keySecret, options = {}) {
if (!keyId) {
throw new Error("keyId is required");
}
if (!keySecret) {
throw new Error("keySecret is required");
}
this.keyId = keyId;
this.keySecret = keySecret;
this.baseUrl = (options.baseUrl || "http://127.0.0.1:7001").replace(/\/$/, "");
this.encrypt = options.encrypt === true;
this.signType = options.signType || "md5";
}
getSign(content) {
if (this.signType !== "md5") {
throw new Error(`Unsupported signType: ${this.signType}`);
}
return crypto.createHash("md5").update(content + this.keySecret, "utf8").digest("hex");
}
getToken(options = {}) {
const encrypt = options.encrypt ?? this.encrypt;
const content = JSON.stringify({
keyId: this.keyId,
t: Math.floor(Date.now() / 1000),
encrypt,
signType: this.signType,
});
const sign = this.getSign(content);
return `${Buffer.from(content, "utf8").toString("base64")}.${Buffer.from(sign, "utf8").toString("base64")}`;
}
async request(path, body = {}, options = {}) {
const response = await fetch(`${this.baseUrl}${path}`, {
method: options.method || "POST",
headers: {
"content-type": "application/json",
"x-certd-token": this.getToken({ encrypt: options.encrypt }),
...(options.headers || {}),
},
body: JSON.stringify(body),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${text}`);
}
return text;
}
getCert(params) {
return this.request("/api/v1/cert/get", params);
}
}
module.exports = { CertdClient };
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env node
const { CertdClient } = require("./certd-client");
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing environment variable: ${name}`);
}
return value;
}
function boolEnv(name, defaultValue = false) {
const value = process.env[name];
if (value == null || value === "") {
return defaultValue;
}
return ["1", "true", "yes", "y"].includes(value.toLowerCase());
}
async function main() {
const client = new CertdClient(requireEnv("CERTD_KEY_ID"), requireEnv("CERTD_KEY_SECRET"), {
baseUrl: process.env.CERTD_BASE_URL,
encrypt: boolEnv("CERTD_ENCRYPT"),
});
const params = {
autoApply: boolEnv("CERTD_AUTO_APPLY"),
};
if (process.env.CERTD_CERT_ID) {
params.certId = Number(process.env.CERTD_CERT_ID);
if (!Number.isInteger(params.certId) || params.certId <= 0) {
throw new Error("CERTD_CERT_ID must be a positive integer");
}
}
if (process.env.CERTD_DOMAINS) {
params.domains = process.env.CERTD_DOMAINS;
}
if (process.env.CERTD_FORMAT) {
params.format = process.env.CERTD_FORMAT;
}
if (!params.certId && !params.domains) {
throw new Error("Set CERTD_CERT_ID or CERTD_DOMAINS");
}
console.log(await client.getCert(params));
}
main().catch(error => {
console.error(error.message);
process.exit(1);
});
+84
View File
@@ -0,0 +1,84 @@
<?php
class CertdClient
{
private string $keyId;
private string $keySecret;
private string $baseUrl;
private bool $encrypt;
private string $signType;
public function __construct(string $keyId, string $keySecret, array $options = [])
{
if ($keyId === '') {
throw new InvalidArgumentException('keyId is required');
}
if ($keySecret === '') {
throw new InvalidArgumentException('keySecret is required');
}
$this->keyId = $keyId;
$this->keySecret = $keySecret;
$this->baseUrl = rtrim($options['baseUrl'] ?? 'http://127.0.0.1:7001', '/');
$this->encrypt = $options['encrypt'] ?? false;
$this->signType = $options['signType'] ?? 'md5';
}
public function getSign(string $content): string
{
if ($this->signType !== 'md5') {
throw new InvalidArgumentException("Unsupported signType: {$this->signType}");
}
return md5($content . $this->keySecret);
}
public function getToken(?bool $encrypt = null): string
{
$content = json_encode([
'keyId' => $this->keyId,
't' => time(),
'encrypt' => $encrypt ?? $this->encrypt,
'signType' => $this->signType,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$sign = $this->getSign($content);
return base64_encode($content) . '.' . base64_encode($sign);
}
public function request(string $path, array $body = [], ?bool $encrypt = null): string
{
if (!function_exists('curl_init')) {
throw new RuntimeException('PHP curl extension is required');
}
$payload = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$headers = [
'Content-Type: application/json',
'x-certd-token: ' . $this->getToken($encrypt),
];
$ch = curl_init($this->baseUrl . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
]);
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException(curl_error($ch));
}
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($statusCode < 200 || $statusCode >= 300) {
throw new RuntimeException("HTTP {$statusCode}: {$response}");
}
return $response;
}
public function getCert(array $params): string
{
return $this->request('/api/v1/cert/get', $params);
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
require_once __DIR__ . '/CertdClient.php';
function require_env(string $name): string
{
$value = getenv($name);
if ($value === false || $value === '') {
throw new RuntimeException("Missing environment variable: {$name}");
}
return $value;
}
function bool_env(string $name, bool $default = false): bool
{
$value = getenv($name);
if ($value === false || $value === '') {
return $default;
}
return in_array(strtolower($value), ['1', 'true', 'yes', 'y'], true);
}
try {
$client = new CertdClient(require_env('CERTD_KEY_ID'), require_env('CERTD_KEY_SECRET'), [
'baseUrl' => getenv('CERTD_BASE_URL') ?: 'http://127.0.0.1:7001',
'encrypt' => bool_env('CERTD_ENCRYPT'),
]);
$params = [
'autoApply' => bool_env('CERTD_AUTO_APPLY'),
];
if (getenv('CERTD_CERT_ID')) {
if (!ctype_digit(getenv('CERTD_CERT_ID'))) {
throw new RuntimeException('CERTD_CERT_ID must be a positive integer');
}
$params['certId'] = intval(getenv('CERTD_CERT_ID'));
}
if (getenv('CERTD_DOMAINS')) {
$params['domains'] = getenv('CERTD_DOMAINS');
}
if (getenv('CERTD_FORMAT')) {
$params['format'] = getenv('CERTD_FORMAT');
}
if (empty($params['certId']) && empty($params['domains'])) {
throw new RuntimeException('Set CERTD_CERT_ID or CERTD_DOMAINS');
}
echo $client->getCert($params) . PHP_EOL;
} catch (Throwable $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
exit(1);
}
+81
View File
@@ -0,0 +1,81 @@
import base64
import hashlib
import json
import time
import urllib.error
import urllib.request
from typing import Any, Dict, Optional
class CertdClient:
def __init__(
self,
key_id: str,
key_secret: str,
base_url: str = "http://127.0.0.1:7001",
encrypt: bool = False,
sign_type: str = "md5",
) -> None:
if not key_id:
raise ValueError("key_id is required")
if not key_secret:
raise ValueError("key_secret is required")
self.key_id = key_id
self.key_secret = key_secret
self.base_url = base_url.rstrip("/")
self.encrypt = encrypt
self.sign_type = sign_type
def get_sign(self, content: str) -> str:
if self.sign_type != "md5":
raise ValueError(f"Unsupported sign_type: {self.sign_type}")
return hashlib.md5((content + self.key_secret).encode("utf-8")).hexdigest()
def getSign(self, content: str) -> str:
return self.get_sign(content)
def get_token(self, encrypt: Optional[bool] = None) -> str:
content = json.dumps(
{
"keyId": self.key_id,
"t": int(time.time()),
"encrypt": self.encrypt if encrypt is None else encrypt,
"signType": self.sign_type,
},
separators=(",", ":"),
ensure_ascii=False,
)
sign = self.get_sign(content)
return (
base64.b64encode(content.encode("utf-8")).decode("ascii")
+ "."
+ base64.b64encode(sign.encode("utf-8")).decode("ascii")
)
def getToken(self, encrypt: Optional[bool] = None) -> str:
return self.get_token(encrypt)
def request(self, path: str, body: Optional[Dict[str, Any]] = None, encrypt: Optional[bool] = None) -> str:
data = json.dumps(body or {}, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
request = urllib.request.Request(
f"{self.base_url}{path}",
data=data,
method="POST",
headers={
"content-type": "application/json",
"x-certd-token": self.get_token(encrypt),
},
)
try:
with urllib.request.urlopen(request, timeout=60) as response:
return response.read().decode("utf-8")
except urllib.error.HTTPError as error:
message = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {error.code}: {message}") from error
def get_cert(self, params: Dict[str, Any]) -> str:
return self.request("/api/v1/cert/get", params)
def getCert(self, params: Dict[str, Any]) -> str:
return self.get_cert(params)
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
import os
import sys
from certd_client import CertdClient
def require_env(name: str) -> str:
value = os.getenv(name)
if not value:
raise RuntimeError(f"Missing environment variable: {name}")
return value
def bool_env(name: str, default: bool = False) -> bool:
value = os.getenv(name)
if value is None or value == "":
return default
return value.lower() in ("1", "true", "yes", "y")
def main() -> None:
client = CertdClient(
require_env("CERTD_KEY_ID"),
require_env("CERTD_KEY_SECRET"),
base_url=os.getenv("CERTD_BASE_URL", "http://127.0.0.1:7001"),
encrypt=bool_env("CERTD_ENCRYPT"),
)
params = {"autoApply": bool_env("CERTD_AUTO_APPLY")}
cert_id = os.getenv("CERTD_CERT_ID")
domains = os.getenv("CERTD_DOMAINS")
cert_format = os.getenv("CERTD_FORMAT")
if cert_id:
params["certId"] = int(cert_id)
if domains:
params["domains"] = domains
if cert_format:
params["format"] = cert_format
if "certId" not in params and "domains" not in params:
raise RuntimeError("Set CERTD_CERT_ID or CERTD_DOMAINS")
print(client.get_cert(params))
if __name__ == "__main__":
try:
main()
except Exception as exc:
print(str(exc), file=sys.stderr)
sys.exit(1)
+1 -1
View File
@@ -1 +1 @@
00:22 14:22
+1 -1
View File
@@ -1 +1 @@
00:51 17:29