diff --git a/docs/guide/use/google/images/google-acme.png b/docs/guide/use/google/images/google-acme.png new file mode 100644 index 000000000..22ce08410 Binary files /dev/null and b/docs/guide/use/google/images/google-acme.png differ diff --git a/docs/guide/use/google/index.md b/docs/guide/use/google/index.md index 89afa6a8e..caf6061c5 100644 --- a/docs/guide/use/google/index.md +++ b/docs/guide/use/google/index.md @@ -1,15 +1,23 @@ # google证书申请教程 -## 1、启用API +## 1、 添加流水线 + + 点击“创建证书流水线”按钮 + +## 2、 生成google ACME账号 + +![](./images/google-acme.png) + +## 3、 获取Google EAB + +### 3.1、启用API 打开如下链接,启用 API https://console.cloud.google.com/apis/library/publicca.googleapis.com 打开该链接后点击“启用”,随后等待右侧出现“API已启用”则可以关闭该页。 -## 2、 获取授权 -以下两种方式任选其一 -### 2.1 直接获取EAB 【推荐】 +## 3.2、 创建EAB 1. 打开“Google Cloud Shell”(在右上角点击激活CloudShell图标)。 @@ -28,31 +36,13 @@ keyId: xxxxxxxxxxxxx] ``` ![](./images/google-eab.png) -3. 到Certd中,创建一条EAB授权记录,填写keyId(=kid) 和 b64MacKey 信息 +3. 到Certd中,创建一条EAB授权记录,填写keyId(=kid) 和 b64MacKey 信息 注意:keyId没有`]`结尾,不要把`]`也复制了 +## 4、 生成Google ACME账号 + 注意:EAB授权使用过一次之后,会绑定邮箱,后续再次使用时,要使用相同的邮箱,所以邮箱切记不要修改 否则会报错 `Unknown external account binding (EAB) key. This may be due to the EAB key expiring which occurs 7 days after creation` -4. 创建证书流水线,选择证书提供商为google,选择EAB授权,运行流水线申请证书 - - -### 2.2 通过google服务账号接口获取授权 - -此方式可以自动获取EAB,需要服务端配置代理 - -1. 创建服务账号 -https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account&hl=zh-cn#step_index=1 - -2. 选择一个项目,进入创建服务账号页面 -3. 给服务账号起一个名字,点击`创建并继续` -4. 向此服务账号授予对项目的访问权限: `选择角色`->`基本`->`Owner` -5. 点击完成 -6. 点击服务账号,进入服务账号详情页面 -7. 点击`添加密钥`->`创建新密钥`->`JSON`,下载密钥文件 -8. 将json文件内容粘贴到 certd中 Google服务授权输入框中 - -9. 创建证书流水线,选择证书提供商为google, 选择服务账号授权,运行流水线申请证书 - - +## 5、创建证书流水线,运行流水线申请证书 diff --git a/packages/libs/lib-server/src/basic/base-service.ts b/packages/libs/lib-server/src/basic/base-service.ts index 805fb68cd..8c21b7ae5 100644 --- a/packages/libs/lib-server/src/basic/base-service.ts +++ b/packages/libs/lib-server/src/basic/base-service.ts @@ -1,10 +1,10 @@ -import { PermissionException, ValidateException } from './exception/index.js'; -import { EntityTarget, FindOneOptions, In, Repository, SelectQueryBuilder } from 'typeorm'; -import { Inject } from '@midwayjs/core'; -import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; -import { EntityManager } from 'typeorm/entity-manager/EntityManager.js'; -import { FindManyOptions } from 'typeorm'; -import { Constants } from './constants.js'; +import { PermissionException, ValidateException } from "./exception/index.js"; +import { EntityTarget, FindOneOptions, In, Repository, SelectQueryBuilder } from "typeorm"; +import { Inject } from "@midwayjs/core"; +import { TypeORMDataSourceManager } from "@midwayjs/typeorm"; +import { EntityManager } from "typeorm/entity-manager/EntityManager.js"; +import { FindManyOptions } from "typeorm"; +import { Constants } from "./constants.js"; export type PageReq = { page?: { offset: number; limit: number }; @@ -34,7 +34,7 @@ export abstract class BaseService { abstract getRepository(): Repository; async transaction(callback: (entityManager: EntityManager) => Promise) { - const dataSource = this.dataSourceManager.getDataSource('default'); + const dataSource = this.dataSourceManager.getDataSource("default"); return await dataSource.transaction(callback as any); } @@ -52,7 +52,7 @@ export abstract class BaseService { if (ctx.manager) { return ctx.manager.getRepository(entity); } - const dataSource = this.dataSourceManager.getDataSource('default'); + const dataSource = this.dataSourceManager.getDataSource("default"); return dataSource.getRepository(entity); } @@ -73,7 +73,7 @@ export abstract class BaseService { */ async info(id, infoIgnoreProperty?): Promise { if (!id) { - throw new ValidateException('id不能为空'); + throw new ValidateException("id不能为空"); } const info = await this.getRepository().findOneBy({ id } as any); if (info && infoIgnoreProperty) { @@ -119,18 +119,18 @@ export abstract class BaseService { ...where, }); await this.modifyAfter(idArr); - return ids + return ids; } resolveIdArr(ids: string | any[]) { if (!ids) { - throw new ValidateException('ids不能为空'); + throw new ValidateException("ids不能为空"); } - if (typeof ids === 'string') { - return ids.split(','); - } else if(!Array.isArray(ids)){ + if (typeof ids === "string") { + return ids.split(","); + } else if (!Array.isArray(ids)) { return [ids]; - }else { + } else { return ids; } } @@ -147,7 +147,7 @@ export abstract class BaseService { * 新增 * @param param 数据 */ - async add(param: any) { + async add(param: any): Promise<{ id: number; [key: string]: any }> { const now = new Date(); param.createTime = now; param.updateTime = now; @@ -163,7 +163,7 @@ export abstract class BaseService { * @param param 数据 */ async update(param: any) { - if (!param.id) throw new ValidateException('id 不能为空'); + if (!param.id) throw new ValidateException("id 不能为空"); param.updateTime = new Date(); await this.addOrUpdate(param); await this.modifyAfter(param); @@ -201,10 +201,10 @@ export abstract class BaseService { } private buildListQuery(listReq: ListReq) { - const { query, sort, buildQuery,select } = listReq; - const qb = this.getRepository().createQueryBuilder('main'); + const { query, sort, buildQuery, select } = listReq; + const qb = this.getRepository().createQueryBuilder("main"); if (select) { - qb.setFindOptions({select}); + qb.setFindOptions({ select }); } if (query) { const keys = Object.keys(query); @@ -223,10 +223,10 @@ export abstract class BaseService { } }); if (found) { - qb.addOrderBy('main.' + sort.prop, sort.asc ? 'ASC' : 'DESC'); + qb.addOrderBy("main." + sort.prop, sort.asc ? "ASC" : "DESC"); } } - qb.addOrderBy('id', 'DESC'); + qb.addOrderBy("id", "DESC"); //自定义query if (buildQuery) { buildQuery(qb); @@ -243,12 +243,12 @@ export abstract class BaseService { return await qb.getMany(); } - async checkUserId(ids: number | number[] = 0, userId: number, userKey = 'userId') { + async checkUserId(ids: number | number[] = 0, userId: number, userKey = "userId") { if (ids == null) { - throw new ValidateException('id不能为空'); + throw new ValidateException("id不能为空"); } if (userId == null) { - throw new ValidateException('userId不能为空'); + throw new ValidateException("userId不能为空"); } if (!Array.isArray(ids)) { ids = [ids]; @@ -268,20 +268,20 @@ export abstract class BaseService { if (!res || res.length === ids.length) { return; } - throw new PermissionException('权限不足'); + throw new PermissionException("权限不足"); } - filterIds(ids: any[]) { + filterIds(ids: any[]) { if (!ids) { - throw new ValidateException('ids不能为空'); + throw new ValidateException("ids不能为空"); } - return ids.filter((item) => { - return item!=null && item != "" + return ids.filter(item => { + return item != null && item != ""; }); } - async batchDelete(ids: number[], userId: number,projectId?:number) { + async batchDelete(ids: number[], userId: number, projectId?: number) { ids = this.filterIds(ids); - if(userId!=null){ + if (userId != null) { const userProjectQuery = this.buildUserProjectQuery(userId, projectId); const list = await this.getRepository().find({ where: { @@ -289,9 +289,9 @@ export abstract class BaseService { id: In(ids), ...userProjectQuery, }, - }) + }); // @ts-ignore - ids = list.map(item => item.id) + ids = list.map(item => item.id); } await this.delete(ids); @@ -300,19 +300,18 @@ export abstract class BaseService { async findOne(options: FindOneOptions) { return await this.getRepository().findOne(options); } - } export function checkUserProjectParam(userId: number, projectId: number) { - if (projectId != null ){ - if( userId !== Constants.enterpriseUserId) { - throw new ValidateException('userId projectId 错误'); + if (projectId != null) { + if (userId !== Constants.enterpriseUserId) { + throw new ValidateException("userId projectId 错误"); } - return true - }else{ - if( userId != null) { - return true + return true; + } else { + if (userId != null) { + return true; } - throw new ValidateException('userId不能为空'); + throw new ValidateException("userId不能为空"); } } diff --git a/packages/libs/lib-server/src/user/access/service/access-service.ts b/packages/libs/lib-server/src/user/access/service/access-service.ts index 383bb1c71..44be1c588 100644 --- a/packages/libs/lib-server/src/user/access/service/access-service.ts +++ b/packages/libs/lib-server/src/user/access/service/access-service.ts @@ -35,7 +35,7 @@ export class AccessService extends BaseService { return res; } - async add(param) { + async add(param: any): Promise<{ id: number; [key: string]: any }> { let oldEntity = null; if (param._copyFrom) { oldEntity = await this.info(param._copyFrom); diff --git a/packages/ui/certd-server/.eslintrc b/packages/ui/certd-server/.eslintrc index 6cc4a8e2f..7ddaf032e 100644 --- a/packages/ui/certd-server/.eslintrc +++ b/packages/ui/certd-server/.eslintrc @@ -19,6 +19,8 @@ "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-this-alias": "off" + "@typescript-eslint/no-this-alias": "off", + // 允许any + "@typescript-eslint/no-unsafe-anyassignment": "off" } } diff --git a/packages/ui/certd-server/src/controller/basic/file-controller.ts b/packages/ui/certd-server/src/controller/basic/file-controller.ts index 566824964..565895dbd 100644 --- a/packages/ui/certd-server/src/controller/basic/file-controller.ts +++ b/packages/ui/certd-server/src/controller/basic/file-controller.ts @@ -55,7 +55,7 @@ export class FileController extends BaseController { const key = await this.fileService.saveFile(this.getUserId(), cacheKey, "public"); return this.ok({ key, - url: `/api/basic/file/download?key=${encodeURIComponent(key)}`, + url: `/api/basic/file/download?key=${encodeURIComponent(key as string)}`, }); } return this.ok({ diff --git a/packages/ui/certd-server/src/modules/auto/fix/cert-info-wildcard-domain-count-fix.ts b/packages/ui/certd-server/src/modules/auto/fix/cert-info-wildcard-domain-count-fix.ts index 20a3f6d09..f6609b320 100644 --- a/packages/ui/certd-server/src/modules/auto/fix/cert-info-wildcard-domain-count-fix.ts +++ b/packages/ui/certd-server/src/modules/auto/fix/cert-info-wildcard-domain-count-fix.ts @@ -21,7 +21,7 @@ export class CertInfoWildcardDomainCountFix { }, }); let fixedCount = 0; - for (const item of list) { + for (const item of list as any[]) { if (!item.domains) { continue; } diff --git a/packages/ui/certd-server/src/modules/auto/fix/common-eab-to-acme-account-fix.ts b/packages/ui/certd-server/src/modules/auto/fix/common-eab-to-acme-account-fix.ts index f93c2e1b4..d1da731cc 100644 --- a/packages/ui/certd-server/src/modules/auto/fix/common-eab-to-acme-account-fix.ts +++ b/packages/ui/certd-server/src/modules/auto/fix/common-eab-to-acme-account-fix.ts @@ -112,7 +112,7 @@ export class CommonEabToAcmeAccountFix { return null; } const email = eabAccess.email || `${caType}@common.certd.local`; - const exists = await this.accessService.findOne({ + const exists: any = await this.accessService.findOne({ where: { userId: 0, projectId: null, diff --git a/packages/ui/certd-server/src/modules/sys/authority/service/role-service.ts b/packages/ui/certd-server/src/modules/sys/authority/service/role-service.ts index 29bc24853..6ffd05d6a 100644 --- a/packages/ui/certd-server/src/modules/sys/authority/service/role-service.ts +++ b/packages/ui/certd-server/src/modules/sys/authority/service/role-service.ts @@ -36,7 +36,7 @@ export class RoleService extends BaseService { } async getRoleIdsByUserId(id: any) { - const userRoles = await this.userRoleService.find({ + const userRoles: any = await this.userRoleService.find({ where: { userId: id }, }); return userRoles.map(item => item.roleId); @@ -131,7 +131,7 @@ export class RoleService extends BaseService { async delete(id: any) { const idArr = this.resolveIdArr(id); //@ts-ignore - const urs = await this.userRoleService.find({ where: { roleId: In(idArr) } }); + const urs:any = await this.userRoleService.find({ where: { roleId: In(idArr) } }); if (urs.length > 0) { throw new Error("该角色已被用户使用,无法删除"); } diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts b/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts index bf598b40f..5f29915f4 100644 --- a/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts +++ b/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts @@ -100,7 +100,7 @@ export class AcmeAccountAccess extends BaseAccess { "\nGoogle:请查看[google获取eab帮助文档](https://certd.docmirror.cn/guide/use/google/),用过一次后会绑定邮箱,后续复用EAB要用同一个邮箱" + "\nSSL.com:[SSL.com账号页面](https://secure.ssl.com/account),然后点击api credentials链接,然后点击编辑按钮,查看Secret key和HMAC key" + "\nlitessl:[litesslEAB页面](https://freessl.cn/automation/eab-manager),然后点击新增EAB", - required: false, + required: true, encrypt: true, mergeScript: ` return { @@ -121,7 +121,7 @@ export class AcmeAccountAccess extends BaseAccess { component: { placeholder: "需要EAB的颁发机构生成账号时填写", }, - required: false, + required: true, encrypt: true, mergeScript: ` return { diff --git a/packages/ui/certd-server/src/plugins/plugin-plus/baota/plugins/index.ts b/packages/ui/certd-server/src/plugins/plugin-plus/baota/plugins/index.ts index e99d24e32..8349c005f 100644 --- a/packages/ui/certd-server/src/plugins/plugin-plus/baota/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-plus/baota/plugins/index.ts @@ -3,3 +3,4 @@ export * from "./plugin-deploy-to-website.js"; export * from "./plugin-deploy-to-aawaf.js"; export * from "./plugin-deploy-to-website-win.js"; export * from "./plugin-delete-expiring-cert.js"; +export * from "./plugin-deploy-automatch.js"; diff --git a/packages/ui/certd-server/src/plugins/plugin-plus/baota/plugins/plugin-deploy-automatch.ts b/packages/ui/certd-server/src/plugins/plugin-plus/baota/plugins/plugin-deploy-automatch.ts new file mode 100644 index 000000000..7feca89c7 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-plus/baota/plugins/plugin-deploy-automatch.ts @@ -0,0 +1,122 @@ +import { HttpClient } from "@certd/basic"; +import { AbstractTaskPlugin, CertTargetItem, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline"; +import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; +import { createCertDomainGetterInputDefine } from "@certd/plugin-lib"; +import { BaotaClient } from "../lib/client.js"; +import { BaotaAccess } from "../access.js"; + +/** + * 宝塔-全自动部署插件 + * 根据证书域名自动匹配宝塔站点,全自动部署SSL证书 + * 参照阿里云DCDN部署插件的"根据证书匹配"模式实现 + */ +@IsTaskPlugin({ + name: "BaotaAutoDeploySiteCert", + title: "宝塔-全自动部署", + icon: "svg:icon-bt", + group: pluginGroups.panel.key, + desc: "根据证书域名自动匹配宝塔站点,全自动部署SSL证书。新增加速域名自动感知,自动新增部署", + runStrategy: RunStrategy.AlwaysRun, +}) +export class BaotaAutoDeploySiteCert extends AbstractTaskPlugin { + /** 域名证书 */ + @TaskInput({ + title: "域名证书", + helper: "请选择前置任务输出的域名证书", + component: { + name: "output-selector", + from: [...CertApplyPluginNames], + }, + required: true, + }) + cert!: CertInfo; + + @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) + certDomains!: string[]; + + /** 宝塔授权 */ + @TaskInput({ + title: "宝塔授权", + helper: "baota的接口密钥", + component: { + name: "access-selector", + type: "baota", + }, + required: true, + }) + accessId!: string; + + /** 输出:已部署过的站点列表 */ + @TaskOutput({ + title: "已部署过的站点", + }) + deployedList!: string[]; + + async onInstance() {} + + async execute(): Promise { + this.logger.info("开始宝塔全自动部署证书"); + const access = await this.getAccess(this.accessId); + const http: HttpClient = this.ctx.http; + const client = new BaotaClient(access, http); + + // 宝塔并发部署会导致nginx的conf错乱,用锁串行化 + const lockKey = `baota-lock-${this.accessId}`; + + const { result, deployedList } = await this.autoMatchedDeploy({ + targetName: "宝塔站点", + // 1. 获取证书域名列表 + getCertDomains: async () => { + return this.certDomains; + }, + // 上传证书(宝塔不需要预上传,直接传入key/crt部署) + uploadCert: async () => { + return { key: this.cert.key, crt: this.cert.crt }; + }, + // 4. 部署证书到匹配的站点 + deployOne: async (req: { target: CertTargetItem; cert: any }) => { + await this.ctx.utils.locker.execute(lockKey, async () => { + this.logger.info(`为站点: ${req.target.label} 设置证书`); + const res = await client.doRequest("/site", "SetSSL", { + type: 0, + siteName: req.target.value, + key: req.cert.key, + csr: req.cert.crt, + }); + this.logger.info(res?.msg || `站点 ${req.target.label} 部署证书成功`); + }); + }, + // 2. 获取待部署证书目标列表 + getDeployTargetList: async (data: PageSearch) => { + return await this.querySiteList(client); + }, + }); + + this.deployedList = deployedList; + return result; + } + + /** + * 从宝塔查询站点列表,按证书域名匹配分组 + */ + private async querySiteList(client: BaotaClient): Promise<{ list: CertTargetItem[]; total: number }> { + const domains = this.certDomains; + const url = "/ssl?action=GetSiteDomain"; + const data = { + cert_list: JSON.stringify(domains), + }; + const res = await client.doRequest(url, null, data, { skipCheckRes: false }); + this.logger.info(`获取到站点数量: ${res?.total ?? res?.all?.length ?? 0}`); + const all: string[] = res.all || []; + const options: CertTargetItem[] = all.map((item: string) => ({ + value: item, + label: item, + domain: item, + })); + return { + list: options, + total: options.length, + }; + } +} +new BaotaAutoDeploySiteCert(); \ No newline at end of file