diff --git a/packages/core/pipeline/src/service/index.ts b/packages/core/pipeline/src/service/index.ts index 2cf4395e..6e992c8b 100644 --- a/packages/core/pipeline/src/service/index.ts +++ b/packages/core/pipeline/src/service/index.ts @@ -4,5 +4,5 @@ export * from "./config.js"; export * from "./url.js"; export * from "./emit.js"; export type IServiceGetter = { - get: (name: string) => Promise; + get: (name: string) => Promise; }; diff --git a/packages/libs/lib-server/src/basic/base-service.ts b/packages/libs/lib-server/src/basic/base-service.ts index 42cc9a99..72a8e369 100644 --- a/packages/libs/lib-server/src/basic/base-service.ts +++ b/packages/libs/lib-server/src/basic/base-service.ts @@ -217,4 +217,20 @@ export abstract class BaseService { } throw new PermissionException('权限不足'); } + + async batchDelete(ids: number[], userId: number) { + if(userId >0){ + const list = await this.getRepository().find({ + where: { + // @ts-ignore + id: In(ids), + userId, + }, + }) + // @ts-ignore + ids = list.map(item => item.id) + } + + await this.delete(ids); + } } diff --git a/packages/plugins/plugin-cert/src/dns-provider/api.ts b/packages/plugins/plugin-cert/src/dns-provider/api.ts index 49a18cc1..f2781e4b 100644 --- a/packages/plugins/plugin-cert/src/dns-provider/api.ts +++ b/packages/plugins/plugin-cert/src/dns-provider/api.ts @@ -27,6 +27,7 @@ export type DnsProviderContext = { logger: ILogger; http: HttpClient; utils: typeof utils; + domainParser: IDomainParser; }; export interface IDnsProvider { @@ -35,3 +36,11 @@ export interface IDnsProvider { removeRecord(options: RemoveRecordOptions): Promise; setCtx(ctx: DnsProviderContext): void; } + +export interface ISubDomainsGetter { + getSubDomains(): Promise; +} + +export interface IDomainParser { + parse(fullDomain: string): Promise; +} diff --git a/packages/plugins/plugin-cert/src/dns-provider/base.ts b/packages/plugins/plugin-cert/src/dns-provider/base.ts index 7e3836eb..b1ccdc17 100644 --- a/packages/plugins/plugin-cert/src/dns-provider/base.ts +++ b/packages/plugins/plugin-cert/src/dns-provider/base.ts @@ -1,6 +1,4 @@ import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, IDnsProvider, RemoveRecordOptions } from "./api.js"; -//@ts-ignore -import psl from "psl"; import { dnsProviderRegistry } from "./registry.js"; import { Decorator } from "@certd/pipeline"; import { HttpClient, ILogger } from "@certd/basic"; @@ -16,6 +14,10 @@ export abstract class AbstractDnsProvider implements IDnsProvider { this.http = ctx.http; } + async parseDomain(fullDomain: string) { + return await this.ctx.domainParser.parse(fullDomain); + } + abstract createRecord(options: CreateRecordOptions): Promise; abstract onInstance(): Promise; @@ -23,14 +25,6 @@ export abstract class AbstractDnsProvider implements IDnsProvider { abstract removeRecord(options: RemoveRecordOptions): Promise; } -export function parseDomain(fullDomain: string) { - const parsed = psl.parse(fullDomain) as psl.ParsedDomain; - if (parsed.error) { - throw new Error(`解析${fullDomain}域名失败:` + JSON.stringify(parsed.error)); - } - return parsed.domain as string; -} - export async function createDnsProvider(opts: { dnsProviderType: string; context: DnsProviderContext }): Promise { const { dnsProviderType, context } = opts; const dnsProviderPlugin = dnsProviderRegistry.get(dnsProviderType); diff --git a/packages/plugins/plugin-cert/src/dns-provider/domain-parser.ts b/packages/plugins/plugin-cert/src/dns-provider/domain-parser.ts new file mode 100644 index 00000000..752795bc --- /dev/null +++ b/packages/plugins/plugin-cert/src/dns-provider/domain-parser.ts @@ -0,0 +1,32 @@ +import { IDomainParser, ISubDomainsGetter } from "./api"; +//@ts-ignore +import psl from "psl"; + +export class DomainParser implements IDomainParser { + subDomainsGetter: ISubDomainsGetter; + constructor(subDomainsGetter: ISubDomainsGetter) { + this.subDomainsGetter = subDomainsGetter; + } + + parseDomain(fullDomain: string) { + const parsed = psl.parse(fullDomain) as psl.ParsedDomain; + if (parsed.error) { + throw new Error(`解析${fullDomain}域名失败:` + JSON.stringify(parsed.error)); + } + return parsed.domain as string; + } + + async parse(fullDomain: string) { + const subDomains = await this.subDomainsGetter.getSubDomains(); + if (subDomains && subDomains.length > 0) { + for (const subDomain of subDomains) { + if (fullDomain.endsWith(subDomain)) { + //找到子域名托管 + return subDomain; + } + } + } + + return this.parseDomain(fullDomain); + } +} diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts index c50eb7ef..29c96247 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts @@ -5,8 +5,9 @@ import * as _ from "lodash-es"; import { Challenge } from "@certd/acme-client/types/rfc8555"; import { IContext } from "@certd/pipeline"; import { ILogger, utils } from "@certd/basic"; -import { IDnsProvider, parseDomain } from "../../dns-provider/index.js"; +import { IDnsProvider, IDomainParser } from "../../dns-provider/index.js"; import { HttpChallengeUploader } from "./uploads/api.js"; + export type CnameVerifyPlan = { type?: string; domain: string; @@ -61,6 +62,8 @@ type AcmeServiceOptions = { privateKeyType?: PrivateKeyType; signal?: AbortSignal; maxCheckRetryCount?: number; + userId: number; + domainParser: IDomainParser; }; export class AcmeService { @@ -174,7 +177,7 @@ export class AcmeService { this.logger.info("Triggered challengeCreateFn()"); const fullDomain = authz.identifier.value; - let domain = parseDomain(fullDomain); + let domain = await this.options.domainParser.parse(fullDomain); this.logger.info("主域名为:" + domain); const getChallenge = (type: string) => { @@ -240,7 +243,7 @@ export class AcmeService { const cname = cnameVerifyPlan[fullDomain]; if (cname) { dnsProvider = cname.dnsProvider; - domain = parseDomain(cname.domain); + domain = await this.options.domainParser.parse(cname.domain); fullRecord = cname.fullRecord; } } else { diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts index 96630690..c2a83bf6 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts @@ -4,12 +4,13 @@ import { utils } from "@certd/basic"; import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, HttpVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js"; import { AcmeService } from "./acme.js"; import * as _ from "lodash-es"; -import { createDnsProvider, DnsProviderContext, IDnsProvider } from "../../dns-provider/index.js"; +import { createDnsProvider, DnsProviderContext, IDnsProvider, ISubDomainsGetter } from "../../dns-provider/index.js"; import { CertReader } from "./cert-reader.js"; import { CertApplyBasePlugin } from "./base.js"; import { GoogleClient } from "../../libs/google.js"; import { EabAccess } from "../../access"; import { httpChallengeUploaderFactory } from "./uploads/factory.js"; +import { DomainParser } from "../../dns-provider/domain-parser.js"; export * from "./base.js"; export type { CertInfo }; export * from "./cert-reader.js"; @@ -314,7 +315,10 @@ HTTP文件验证:不支持泛域名,需要配置网站文件上传`, } } this.eab = eab; + const subDomainsGetter = await this.ctx.serviceGetter.get("subDomainsGetter"); + const domainParser = new DomainParser(subDomainsGetter); this.acme = new AcmeService({ + userId: this.ctx.user.id, userContext: this.userContext, logger: this.logger, sslProvider: this.sslProvider, @@ -325,8 +329,7 @@ HTTP文件验证:不支持泛域名,需要配置网站文件上传`, privateKeyType: this.privateKeyType, signal: this.ctx.signal, maxCheckRetryCount: this.maxCheckRetryCount, - // cnameProxyService: this.ctx.cnameProxyService, - // dnsProviderCreator: this.createDnsProvider.bind(this), + domainParser, }); } @@ -387,7 +390,8 @@ HTTP文件验证:不支持泛域名,需要配置网站文件上传`, } async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise { - const context: DnsProviderContext = { access: dnsProviderAccess, logger: this.logger, http: this.ctx.http, utils }; + const domainParser = this.acme.options.domainParser; + const context: DnsProviderContext = { access: dnsProviderAccess, logger: this.logger, http: this.ctx.http, utils, domainParser }; return await createDnsProvider({ dnsProviderType, context, diff --git a/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/api.ts b/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/api.ts index 717b04ac..9efd7833 100644 --- a/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/api.ts +++ b/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/api.ts @@ -1,6 +1,7 @@ import { request } from "/src/api/service"; const apiPrefix = "/cname/record"; +const subDomainApiPrefix = "/pi/subDomain"; export type CnameRecord = { id?: number; @@ -18,7 +19,7 @@ export type DomainGroupItem = { export async function GetList() { return await request({ url: apiPrefix + "/list", - method: "post" + method: "post", }); } @@ -28,8 +29,8 @@ export async function GetByDomain(domain: string) { method: "post", data: { domain, - createOnNotFound: true - } + createOnNotFound: true, + }, }); } @@ -38,7 +39,17 @@ export async function DoVerify(id: number) { url: apiPrefix + "/verify", method: "post", data: { - id - } + id, + }, + }); +} + +export async function ParseDomain(fullDomain: string) { + return await request({ + url: subDomainApiPrefix + "/parseDomain", + method: "post", + data: { + fullDomain, + }, }); } diff --git a/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/index.vue b/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/index.vue index 54a40d3d..c71d0c38 100644 --- a/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/index.vue +++ b/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/index.vue @@ -32,26 +32,14 @@
DNS类型: - +
DNS授权: - +
@@ -80,28 +68,27 @@ import { dict, FsDictSelect } from "@fast-crud/fast-crud"; import AccessSelector from "/@/views/certd/access/access-selector/index.vue"; import CnameVerifyPlan from "./cname-verify-plan.vue"; import HttpVerifyPlan from "./http-verify-plan.vue"; -//@ts-ignore -import psl from "psl"; import { Form } from "ant-design-vue"; import { DomainsVerifyPlanInput } from "./type"; -import { CnameRecord, DomainGroupItem } from "./api"; +import { DomainGroupItem, ParseDomain } from "./api"; + defineOptions({ - name: "DomainsVerifyPlanEditor" + name: "DomainsVerifyPlanEditor", }); const challengeTypeOptions = ref([ { label: "DNS验证", - value: "dns" + value: "dns", }, { label: "CNAME验证", - value: "cname" + value: "cname", }, { label: "HTTP验证", - value: "http" - } + value: "http", + }, ]); const props = defineProps<{ @@ -122,7 +109,7 @@ function fullscreenExit() { } const planRef = ref(props.modelValue || {}); const dnsProviderTypeDict = dict({ - url: "pi/dnsProvider/dnsProviderTypeDict" + url: "pi/dnsProvider/dnsProviderTypeDict", }); const formItemContext = Form.useInjectFormItemContext(); @@ -139,7 +126,7 @@ function showError(error: string) { type DomainGroup = Record; -function onDomainsChanged(domains: string[]) { +async function onDomainsChanged(domains: string[]) { if (domains == null) { return; } @@ -147,12 +134,7 @@ function onDomainsChanged(domains: string[]) { const domainGroups: DomainGroup = {}; for (let domain of domains) { const keyDomain = domain.replace("*.", ""); - const parsed = psl.parse(keyDomain); - if (parsed.error) { - showError(`域名${domain}解析失败: ${JSON.stringify(parsed.error)}`); - continue; - } - const mainDomain = parsed.domain; + const mainDomain = await ParseDomain(keyDomain); if (mainDomain == null) { continue; } @@ -161,7 +143,7 @@ function onDomainsChanged(domains: string[]) { group = { domain: mainDomain, domains: [], - keySubDomains: [] + keySubDomains: [], } as DomainGroupItem; domainGroups[mainDomain] = group; } @@ -180,7 +162,7 @@ function onDomainsChanged(domains: string[]) { //@ts-ignore cnameVerifyPlan: {}, //@ts-ignore - httpVerifyPlan: {} + httpVerifyPlan: {}, }; planRef.value[domain] = planItem; } @@ -196,7 +178,7 @@ function onDomainsChanged(domains: string[]) { if (!cnameOrigin[subDomain]) { //@ts-ignore planItem.cnameVerifyPlan[subDomain] = { - id: 0 + id: 0, }; } else { planItem.cnameVerifyPlan[subDomain] = cnameOrigin[subDomain]; @@ -205,14 +187,14 @@ function onDomainsChanged(domains: string[]) { if (!cnamePlan[subDomain]) { //@ts-ignore cnamePlan[subDomain] = { - id: 0 + id: 0, }; } if (!httpOrigin[subDomain]) { //@ts-ignore planItem.httpVerifyPlan[subDomain] = { - domain: subDomain + domain: subDomain, }; } else { planItem.httpVerifyPlan[subDomain] = httpOrigin[subDomain]; @@ -221,7 +203,7 @@ function onDomainsChanged(domains: string[]) { if (!httpPlan[subDomain]) { //@ts-ignore httpPlan[subDomain] = { - domain: subDomain + domain: subDomain, }; } } @@ -255,7 +237,7 @@ watch( }, { immediate: true, - deep: true + deep: true, } ); diff --git a/packages/ui/certd-client/src/router/source/modules/certd.ts b/packages/ui/certd-client/src/router/source/modules/certd.ts index d7f01a3d..a37bad48 100644 --- a/packages/ui/certd-client/src/router/source/modules/certd.ts +++ b/packages/ui/certd-client/src/router/source/modules/certd.ts @@ -98,6 +98,17 @@ export const certdResources = [ keepAlive: true, }, }, + { + title: "子域名托管设置", + name: "SubDomain", + path: "/certd/pipeline/subDomain", + component: "/certd/pipeline/sub-domain/index.vue", + meta: { + icon: "ion:link-outline", + auth: true, + keepAlive: true, + }, + }, { title: "流水线分组管理", name: "PipelineGroupManager", diff --git a/packages/ui/certd-client/src/views/certd/pipeline/sub-domain/api.ts b/packages/ui/certd-client/src/views/certd/pipeline/sub-domain/api.ts new file mode 100644 index 00000000..e9b2733d --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/sub-domain/api.ts @@ -0,0 +1,60 @@ +// @ts-ignore +import { request } from "/src/api/service"; + +const apiPrefix = "/pi/subDomain"; + +export async function GetList(query: any) { + return await request({ + url: apiPrefix + "/page", + method: "post", + data: query, + }); +} + +export async function AddObj(obj: any) { + return await request({ + url: apiPrefix + "/add", + method: "post", + data: obj, + }); +} + +export async function UpdateObj(obj: any) { + return await request({ + url: apiPrefix + "/update", + method: "post", + data: obj, + }); +} + +export async function DelObj(id: any) { + return await request({ + url: apiPrefix + "/delete", + method: "post", + params: { id }, + }); +} + +export async function GetObj(id: any) { + return await request({ + url: apiPrefix + "/info", + method: "post", + params: { id }, + }); +} + +export async function GetDetail(id: any) { + return await request({ + url: apiPrefix + "/detail", + method: "post", + params: { id }, + }); +} + +export async function DeleteBatch(ids: any[]) { + return await request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids }, + }); +} diff --git a/packages/ui/certd-client/src/views/certd/pipeline/sub-domain/crud.tsx b/packages/ui/certd-client/src/views/certd/pipeline/sub-domain/crud.tsx new file mode 100644 index 00000000..db9bb79b --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/sub-domain/crud.tsx @@ -0,0 +1,123 @@ +import * as api from "./api"; +import { Ref, ref } from "vue"; +import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; + +export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { + const pageRequest = async (query: UserPageQuery): Promise => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + form.id = row.id; + const res = await api.UpdateObj(form); + return res; + }; + const delRequest = async ({ row }: DelReq) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }: AddReq) => { + const res = await api.AddObj(form); + return res; + }; + + const selectedRowKeys: Ref = ref([]); + context.selectedRowKeys = selectedRowKeys; + + return { + crudOptions: { + settings: { + plugins: { + //这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并 + rowSelection: { + enabled: true, + order: -2, + before: true, + // handle: (pluginProps,useCrudProps)=>CrudOptions, + props: { + multiple: true, + crossPage: true, + selectedRowKeys, + }, + }, + }, + }, + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + // tabs: { + // name: "status", + // show: true, + // }, + rowHandle: { + minWidth: 200, + fixed: "right", + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 80, + }, + form: { + show: false, + }, + }, + domain: { + title: "子域名", + type: "text", + search: { + show: true, + }, + editForm: { + component: { + disabled: true, + }, + }, + }, + disabled: { + title: "是否禁用", + type: "dict-switch", + dict: dict({ + data: [ + { value: false, label: "启用", color: "green" }, + { value: true, label: "禁用", color: "gray" }, + ], + }), + search: { + show: true, + }, + form: { + value: false, + }, + }, + createTime: { + title: "创建时间", + type: "datetime", + form: { + show: false, + }, + column: { + sorter: true, + width: 160, + align: "center", + }, + }, + updateTime: { + title: "更新时间", + type: "datetime", + form: { + show: false, + }, + column: { + show: true, + }, + }, + }, + }, + }; +} diff --git a/packages/ui/certd-client/src/views/certd/pipeline/sub-domain/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/sub-domain/index.vue new file mode 100644 index 00000000..d7eeb7a9 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/sub-domain/index.vue @@ -0,0 +1,57 @@ + + + + diff --git a/packages/ui/certd-server/db/migration/v10021__plugin.sql b/packages/ui/certd-server/db/migration/v10021__plugin.sql index f54b4107..3d91613d 100644 --- a/packages/ui/certd-server/db/migration/v10021__plugin.sql +++ b/packages/ui/certd-server/db/migration/v10021__plugin.sql @@ -1,3 +1,19 @@ ALTER TABLE pi_plugin ADD COLUMN "pluginType" varchar(100); ALTER TABLE pi_plugin ADD COLUMN "metadata" varchar(40960); ALTER TABLE pi_plugin ADD COLUMN "author" varchar(100); + + + +CREATE TABLE "pi_sub_domain" +( + "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, + "user_id" integer, + "domain" varchar(100), + "disabled" boolean NOT NULL DEFAULT (false), + "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), + "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP) +); + + +CREATE INDEX "index_sub_domain_user_id" ON "pi_sub_domain" ("user_id"); +CREATE INDEX "index_sub_domain_domain" ON "pi_sub_domain" ("domain"); diff --git a/packages/ui/certd-server/src/controller/user/pipeline/handle-controller.ts b/packages/ui/certd-server/src/controller/user/pipeline/handle-controller.ts index ba0f3221..fd5a9edd 100644 --- a/packages/ui/certd-server/src/controller/user/pipeline/handle-controller.ts +++ b/packages/ui/certd-server/src/controller/user/pipeline/handle-controller.ts @@ -1,7 +1,8 @@ import {ALL, Body, Controller, Inject, Post, Provide} from '@midwayjs/core'; -import {AccessGetter, AccessService, BaseController, Constants} from '@certd/lib-server'; +import {AccessService, BaseController, Constants} from '@certd/lib-server'; import { AccessRequestHandleReq, + IAccessService, ITaskPlugin, newAccess, newNotification, @@ -13,6 +14,7 @@ import { import {EmailService} from '../../../modules/basic/service/email-service.js'; import {http, HttpRequestConfig, logger, mergeUtils, utils} from '@certd/basic'; import {NotificationService} from '../../../modules/pipeline/service/notification-service.js'; +import {TaskServiceBuilder} from "../../../modules/pipeline/service/task-service-getter.js"; @Provide() @Controller('/api/pi/handle') @@ -23,6 +25,8 @@ export class HandleController extends BaseController { @Inject() emailService: EmailService; + @Inject() + taskServiceBuilder: TaskServiceBuilder; @Inject() notificationService: NotificationService; @@ -82,8 +86,6 @@ export class HandleController extends BaseController { //@ts-ignore const instance = plugin as ITaskPlugin; - const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService)); - const download = async (config: HttpRequestConfig, savePath: string) => { await utils.download({ http, @@ -93,13 +95,9 @@ export class HandleController extends BaseController { }); }; - const serviceContainer:any = { - } - const serviceGetter = { - get:(name: string) => { - return serviceContainer[name] - } - } + const taskServiceGetter = this.taskServiceBuilder.create({userId}) + + const accessGetter = await taskServiceGetter.get("accessService") //@ts-ignore const taskCtx: TaskInstanceContext = { pipeline: undefined, @@ -125,7 +123,7 @@ export class HandleController extends BaseController { // }), // signal: this.abort.signal, utils, - serviceGetter + serviceGetter:taskServiceGetter }; instance.setCtx(taskCtx); mergeUtils.merge(plugin, body.input); diff --git a/packages/ui/certd-server/src/controller/user/pipeline/sub-domain-controller.ts b/packages/ui/certd-server/src/controller/user/pipeline/sub-domain-controller.ts new file mode 100644 index 00000000..c94b0ecd --- /dev/null +++ b/packages/ui/certd-server/src/controller/user/pipeline/sub-domain-controller.ts @@ -0,0 +1,81 @@ +import {ALL, Body, Controller, Inject, Post, Provide, Query} from '@midwayjs/core'; +import {Constants, CrudController} from '@certd/lib-server'; +import {SubDomainService, SubDomainsGetter} from "../../../modules/pipeline/service/sub-domain-service.js"; +import {DomainParser} from '@certd/plugin-cert/dist/dns-provider/domain-parser.js'; + +/** + * 子域名托管 + */ +@Provide() +@Controller('/api/pi/subDomain') +export class SubDomainController extends CrudController { + @Inject() + service: SubDomainService; + + getService() { + return this.service; + } + + @Post('/parseDomain', { summary: Constants.per.authOnly }) + async parseDomain(@Body("fullDomain") fullDomain:string) { + const userId = this.getUserId() + const subDomainGetter = new SubDomainsGetter(userId, this.service) + const domainParser = new DomainParser(subDomainGetter) + const domain = await domainParser.parse(fullDomain) + return this.ok(domain); + } + + + @Post('/page', { summary: Constants.per.authOnly }) + async page(@Body(ALL) body) { + body.query = body.query ?? {}; + delete body.query.userId; + const buildQuery = qb => { + qb.andWhere('user_id = :userId', { userId: this.getUserId() }); + }; + const res = await this.service.page({ + query: body.query, + page: body.page, + sort: body.sort, + buildQuery, + }); + return this.ok(res); + } + + @Post('/list', { summary: Constants.per.authOnly }) + async list(@Body(ALL) body) { + body.query = body.query ?? {}; + body.query.userId = this.getUserId(); + return super.list(body); + } + + @Post('/add', { summary: Constants.per.authOnly }) + async add(@Body(ALL) bean) { + bean.userId = this.getUserId(); + return super.add(bean); + } + + @Post('/update', { summary: Constants.per.authOnly }) + async update(@Body(ALL) bean) { + await this.service.checkUserId(bean.id, this.getUserId()); + delete bean.userId; + return super.update(bean); + } + @Post('/info', { summary: Constants.per.authOnly }) + async info(@Query('id') id: number) { + await this.service.checkUserId(id, this.getUserId()); + return super.info(id); + } + + @Post('/delete', { summary: Constants.per.authOnly }) + async delete(@Query('id') id: number) { + await this.service.checkUserId(id, this.getUserId()); + return super.delete(id); + } + + @Post('/batchDelete', { summary: Constants.per.authOnly }) + async batchDelete(@Body('ids') ids: number[]) { + await this.service.batchDelete(ids, this.getUserId()); + return this.ok({}); + } +} diff --git a/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts b/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts index 167c24f0..e9adb77c 100644 --- a/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts +++ b/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts @@ -1,18 +1,17 @@ -import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; -import { InjectEntityModel } from '@midwayjs/typeorm'; -import { Repository } from 'typeorm'; -import { BaseService, PlusService, ValidateException } from '@certd/lib-server'; -import { CnameRecordEntity, CnameRecordStatusType } from '../entity/cname-record.js'; -import { createDnsProvider, IDnsProvider, parseDomain } from '@certd/plugin-cert'; -import { CnameProvider, CnameRecord } from '@certd/pipeline'; -import { cache, http, logger, utils } from '@certd/basic'; - -import { AccessService } from '@certd/lib-server'; -import { isDev } from '@certd/basic'; -import { walkTxtRecord } from '@certd/acme-client'; -import { CnameProviderService } from './cname-provider-service.js'; -import { CnameProviderEntity } from '../entity/cname-provider.js'; -import { CommonDnsProvider } from './common-provider.js'; +import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core'; +import {InjectEntityModel} from '@midwayjs/typeorm'; +import {Repository} from 'typeorm'; +import {AccessService, BaseService, PlusService, ValidateException} from '@certd/lib-server'; +import {CnameRecordEntity, CnameRecordStatusType} from '../entity/cname-record.js'; +import {createDnsProvider, IDnsProvider} from '@certd/plugin-cert'; +import {CnameProvider, CnameRecord} from '@certd/pipeline'; +import {cache, http, isDev, logger, utils} from '@certd/basic'; +import {walkTxtRecord} from '@certd/acme-client'; +import {CnameProviderService} from './cname-provider-service.js'; +import {CnameProviderEntity} from '../entity/cname-provider.js'; +import {CommonDnsProvider} from './common-provider.js'; +import {SubDomainService, SubDomainsGetter} from "../../pipeline/service/sub-domain-service.js"; +import {DomainParser} from "@certd/plugin-cert/dist/dns-provider/domain-parser.js"; type CnameCheckCacheValue = { validating: boolean; @@ -40,6 +39,9 @@ export class CnameRecordService extends BaseService { @Inject() plusService: PlusService; + @Inject() + subDomainService: SubDomainService; + //@ts-ignore getRepository() { return this.repository; @@ -74,17 +76,20 @@ export class CnameRecordService extends BaseService { } else { cnameProvider = await this.cnameProviderService.info(param.cnameProviderId); } - this.cnameProviderChanged(param, cnameProvider); + await this.cnameProviderChanged(param.userId,param, cnameProvider); param.status = 'cname'; const { id } = await super.add(param); return await this.info(id); } - private cnameProviderChanged(param: any, cnameProvider: CnameProviderEntity) { + private async cnameProviderChanged(userId:number,param: any, cnameProvider: CnameProviderEntity) { param.cnameProviderId = cnameProvider.id; - const realDomain = parseDomain(param.domain); + const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService) + const domainParser = new DomainParser(subDomainGetter); + + const realDomain = await domainParser.parse(param.domain); const prefix = param.domain.replace(realDomain, ''); let hostRecord = `_acme-challenge.${prefix}`; if (hostRecord.endsWith('.')) { @@ -111,7 +116,7 @@ export class CnameRecordService extends BaseService { } if (old.cnameProviderId !== param.cnameProviderId) { const cnameProvider = await this.cnameProviderService.info(param.cnameProviderId); - this.cnameProviderChanged(param, cnameProvider); + await this.cnameProviderChanged(old.userId,param, cnameProvider); param.status = 'cname'; } return await super.update(param); @@ -185,6 +190,9 @@ export class CnameRecordService extends BaseService { return true; } + const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService) + const domainParser = new DomainParser(subDomainGetter); + const cacheKey = `cname.record.verify.${bean.id}`; let value: CnameCheckCacheValue = cache.get(cacheKey); @@ -219,7 +227,7 @@ export class CnameRecordService extends BaseService { } const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId); - const context = { access, logger, http, utils }; + const context = { access, logger, http, utils,domainParser }; const dnsProvider: IDnsProvider = await createDnsProvider({ dnsProviderType: cnameProvider.dnsProviderType, context, @@ -239,7 +247,8 @@ export class CnameRecordService extends BaseService { return false; } - const originDomain = parseDomain(bean.domain); + + const originDomain = await domainParser.parse(bean.domain); const fullDomain = `${bean.hostRecord}.${originDomain}`; logger.info(`检查CNAME配置 ${fullDomain} ${testRecordValue}`); @@ -285,7 +294,7 @@ export class CnameRecordService extends BaseService { ttl: ttl, }); - const domain = parseDomain(bean.recordValue); + const domain = await domainParser.parse(bean.recordValue); const fullRecord = bean.recordValue; const hostRecord = fullRecord.replace(`.${domain}`, ''); const req = { diff --git a/packages/ui/certd-server/src/modules/pipeline/entity/sub-domain.ts b/packages/ui/certd-server/src/modules/pipeline/entity/sub-domain.ts new file mode 100644 index 00000000..a6d324ed --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/entity/sub-domain.ts @@ -0,0 +1,32 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +/** + * 子域名托管 + */ +@Entity('pi_sub_domain') +export class SubDomainEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'user_id', comment: 'UserId' }) + userId: number; + + @Column({ name: 'domain', comment: '子域名' }) + domain: string; + + @Column({ name: 'disabled', comment: '禁用' }) + disabled: boolean; + + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts index eb14c6b7..bdf832ab 100644 --- a/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts +++ b/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts @@ -1,8 +1,7 @@ -import { Config, Inject, Provide, Scope, ScopeEnum, sleep } from "@midwayjs/core"; -import { InjectEntityModel } from "@midwayjs/typeorm"; -import { In, MoreThan, Repository } from "typeorm"; +import {Config, Inject, Provide, Scope, ScopeEnum, sleep} from "@midwayjs/core"; +import {InjectEntityModel} from "@midwayjs/typeorm"; +import {In, MoreThan, Repository} from "typeorm"; import { - AccessGetter, AccessService, BaseService, NeedSuiteException, @@ -12,30 +11,40 @@ import { SysSettingsService, SysSiteInfo } from "@certd/lib-server"; -import { PipelineEntity } from "../entity/pipeline.js"; -import { PipelineDetail } from "../entity/vo/pipeline-detail.js"; -import { Executor, Pipeline, ResultType, RunHistory, RunnableCollection, SysInfo, UserInfo } from "@certd/pipeline"; -import { DbStorage } from "./db-storage.js"; -import { StorageService } from "./storage-service.js"; -import { Cron } from "../../cron/cron.js"; -import { HistoryService } from "./history-service.js"; -import { HistoryEntity } from "../entity/history.js"; -import { HistoryLogEntity } from "../entity/history-log.js"; -import { HistoryLogService } from "./history-log-service.js"; -import { EmailService } from "../../basic/service/email-service.js"; -import { UserService } from "../../sys/authority/service/user-service.js"; -import { CnameRecordService } from "../../cname/service/cname-record-service.js"; -import { CnameProxyService } from "./cname-proxy-service.js"; -import { PluginConfigGetter } from "../../plugin/service/plugin-config-getter.js"; +import {PipelineEntity} from "../entity/pipeline.js"; +import {PipelineDetail} from "../entity/vo/pipeline-detail.js"; +import { + Executor, + IAccessService, + ICnameProxyService, + INotificationService, + Pipeline, + ResultType, + RunHistory, + RunnableCollection, + SysInfo, + UserInfo +} from "@certd/pipeline"; +import {DbStorage} from "./db-storage.js"; +import {StorageService} from "./storage-service.js"; +import {Cron} from "../../cron/cron.js"; +import {HistoryService} from "./history-service.js"; +import {HistoryEntity} from "../entity/history.js"; +import {HistoryLogEntity} from "../entity/history-log.js"; +import {HistoryLogService} from "./history-log-service.js"; +import {EmailService} from "../../basic/service/email-service.js"; +import {UserService} from "../../sys/authority/service/user-service.js"; +import {CnameRecordService} from "../../cname/service/cname-record-service.js"; +import {PluginConfigGetter} from "../../plugin/service/plugin-config-getter.js"; import dayjs from "dayjs"; -import { DbAdapter } from "../../db/index.js"; -import { isComm } from "@certd/plus-core"; -import { logger } from "@certd/basic"; -import { UrlService } from "./url-service.js"; -import { NotificationService } from "./notification-service.js"; -import { NotificationGetter } from "./notification-getter.js"; -import { UserSuiteEntity, UserSuiteService } from "@certd/commercial-core"; -import { CertInfoService } from "../../monitor/service/cert-info-service.js"; +import {DbAdapter} from "../../db/index.js"; +import {isComm} from "@certd/plus-core"; +import {logger} from "@certd/basic"; +import {UrlService} from "./url-service.js"; +import {NotificationService} from "./notification-service.js"; +import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core"; +import {CertInfoService} from "../../monitor/service/cert-info-service.js"; +import {TaskServiceBuilder} from "./task-service-getter.js"; const runningTasks: Map = new Map(); @@ -65,6 +74,9 @@ export class PipelineService extends BaseService { @Inject() pluginConfigGetter: PluginConfigGetter; + @Inject() + taskServiceBuilder: TaskServiceBuilder; + @Inject() sysSettingsService: SysSettingsService; @@ -473,20 +485,19 @@ export class PipelineService extends BaseService { role: userIsAdmin ? 'admin' : 'user', }; - const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService)); - const cnameProxyService = new CnameProxyService(userId, this.cnameRecordService.getWithAccessByDomain.bind(this.cnameRecordService)); - const notificationGetter = new NotificationGetter(userId, this.notificationService); + const sysInfo: SysInfo = {}; if (isComm()) { const siteInfo = await this.sysSettingsService.getSetting(SysSiteInfo); sysInfo.title = siteInfo.title; } - const serviceContainer = {} - const serviceGetter = { - get:(name: string) => { - return serviceContainer[name] - } - } + + const taskServiceGetter = this.taskServiceBuilder.create({ + userId, + }) + const accessGetter = await taskServiceGetter.get("accessService") + const notificationGetter =await taskServiceGetter.get("notificationService") + const cnameProxyService =await taskServiceGetter.get("cnameProxyService") const executor = new Executor({ user, pipeline, @@ -500,7 +511,7 @@ export class PipelineService extends BaseService { notificationService: notificationGetter, fileRootDir: this.certdConfig.fileRootDir, sysInfo, - serviceGetter + serviceGetter:taskServiceGetter }); try { runningTasks.set(historyId, executor); diff --git a/packages/ui/certd-server/src/modules/pipeline/service/sub-domain-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/sub-domain-service.ts new file mode 100644 index 00000000..9df006be --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/sub-domain-service.ts @@ -0,0 +1,58 @@ +import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core'; +import {BaseService, SysSettingsService} from '@certd/lib-server'; +import {InjectEntityModel} from '@midwayjs/typeorm'; +import {Repository} from 'typeorm'; +import {SubDomainEntity} from '../entity/sub-domain.js'; +import {EmailService} from '../../basic/service/email-service.js'; +import {ISubDomainsGetter} from "@certd/plugin-cert"; + +@Provide() +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +export class SubDomainService extends BaseService { + @InjectEntityModel(SubDomainEntity) + repository: Repository; + + @Inject() + emailService: EmailService; + + @Inject() + sysSettingsService: SysSettingsService; + + //@ts-ignore + getRepository() { + return this.repository; + } + + async getListByUserId(userId:number):Promise{ + if (!userId) { + return []; + } + const list = await this.find({ + where: { + userId, + disabled: false, + }, + }); + + return list.map(item=>item.domain); + } + +} + + + + +export class SubDomainsGetter implements ISubDomainsGetter { + userId: number; + subDomainService: SubDomainService; + + constructor(userId: number, subDomainService: SubDomainService) { + this.userId = userId; + this.subDomainService = subDomainService; + } + + async getSubDomains() { + return await this.subDomainService.getListByUserId(this.userId) + } + +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/task-service-getter.ts b/packages/ui/certd-server/src/modules/pipeline/service/task-service-getter.ts new file mode 100644 index 00000000..a54ea260 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/task-service-getter.ts @@ -0,0 +1,63 @@ +import {IServiceGetter} from "@certd/pipeline"; +import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core"; +import {SubDomainService, SubDomainsGetter} from "./sub-domain-service.js"; +import {AccessGetter, AccessService} from "@certd/lib-server"; +import {CnameProxyService} from "./cname-proxy-service.js"; +import {NotificationGetter} from "./notification-getter.js"; +import {NotificationService} from "./notification-service.js"; +import {CnameRecordService} from "../../cname/service/cname-record-service.js"; + +export class TaskServiceGetter implements IServiceGetter{ + serviceContainer:Record; + constructor(serviceContainer:Record) { + this.serviceContainer = serviceContainer; + } + async get(serviceName: string): Promise { + const ret = this.serviceContainer[serviceName] as T; + if(!ret){ + throw new Error(`service ${serviceName} not found`) + } + return ret + } +} +export type TaskServiceCreateReq = { + userId: number; +} + +export type TaskServiceContainer = { + subDomainsGetter:SubDomainsGetter; + accessService: AccessGetter; + cnameProxyService: CnameProxyService; + notificationService: NotificationGetter; +} + +@Provide() +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +export class TaskServiceBuilder { + + @Inject() + subDomainService: SubDomainService; + @Inject() + accessService: AccessService; + @Inject() + cnameRecordService: CnameRecordService; + @Inject() + notificationService: NotificationService; + + + create(req:TaskServiceCreateReq){ + + const userId = req.userId; + const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService)); + const cnameProxyService = new CnameProxyService(userId, this.cnameRecordService.getWithAccessByDomain.bind(this.cnameRecordService)); + const notificationGetter = new NotificationGetter(userId, this.notificationService); + + const serviceContainer:TaskServiceContainer = { + subDomainsGetter:new SubDomainsGetter(req.userId, this.subDomainService), + accessService: accessGetter, + cnameProxyService:cnameProxyService, + notificationService:notificationGetter + } + return new TaskServiceGetter(serviceContainer) + } +}