mirror of https://github.com/certd/certd
feat: 域名验证方法支持CNAME间接方式,此方式支持所有域名注册商,且无需提供Access授权,但是需要手动添加cname解析
parent
0c8e83e125
commit
f3d35084ed
|
@ -118,16 +118,16 @@ module.exports = async (client, userOpts) => {
|
||||||
/* Trigger challengeCreateFn() */
|
/* Trigger challengeCreateFn() */
|
||||||
log(`[auto] [${d}] Trigger challengeCreateFn()`);
|
log(`[auto] [${d}] Trigger challengeCreateFn()`);
|
||||||
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
||||||
let recordItem = null;
|
|
||||||
try {
|
try {
|
||||||
recordItem = await opts.challengeCreateFn(authz, challenge, keyAuthorization);
|
const { recordReq, recordRes, dnsProvider } = await opts.challengeCreateFn(authz, challenge, keyAuthorization);
|
||||||
log(`[auto] [${d}] challengeCreateFn success`);
|
log(`[auto] [${d}] challengeCreateFn success`);
|
||||||
log(`[auto] [${d}] add challengeRemoveFn()`);
|
log(`[auto] [${d}] add challengeRemoveFn()`);
|
||||||
clearTasks.push(async () => {
|
clearTasks.push(async () => {
|
||||||
/* Trigger challengeRemoveFn(), suppress errors */
|
/* Trigger challengeRemoveFn(), suppress errors */
|
||||||
log(`[auto] [${d}] Trigger challengeRemoveFn()`);
|
log(`[auto] [${d}] Trigger challengeRemoveFn()`);
|
||||||
try {
|
try {
|
||||||
await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem);
|
await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
|
log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
|
||||||
|
|
|
@ -68,6 +68,7 @@ async function walkDnsChallengeRecord(recordName, resolver = dns) {
|
||||||
|
|
||||||
if (txtRecords.length) {
|
if (txtRecords.length) {
|
||||||
log(`Found ${txtRecords.length} TXT records at ${recordName}`);
|
log(`Found ${txtRecords.length} TXT records at ${recordName}`);
|
||||||
|
log(`TXT records: ${JSON.stringify(txtRecords)}`);
|
||||||
return [].concat(...txtRecords);
|
return [].concat(...txtRecords);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,8 +55,8 @@ export interface ClientExternalAccountBindingOptions {
|
||||||
|
|
||||||
export interface ClientAutoOptions {
|
export interface ClientAutoOptions {
|
||||||
csr: CsrBuffer | CsrString;
|
csr: CsrBuffer | CsrString;
|
||||||
challengeCreateFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string) => Promise<any>;
|
challengeCreateFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string) => Promise<{recordReq:any,recordRes:any,dnsProvider:any}>;
|
||||||
challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string, recordRes:any) => Promise<any>;
|
challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string,recordReq:any, recordRes:any,dnsProvider:any) => Promise<any>;
|
||||||
email?: string;
|
email?: string;
|
||||||
termsOfServiceAgreed?: boolean;
|
termsOfServiceAgreed?: boolean;
|
||||||
skipChallengeVerification?: boolean;
|
skipChallengeVerification?: boolean;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { createAxiosService } from "../utils/util.request.js";
|
||||||
import { IAccessService } from "../access/index.js";
|
import { IAccessService } from "../access/index.js";
|
||||||
import { RegistryItem } from "../registry/index.js";
|
import { RegistryItem } from "../registry/index.js";
|
||||||
import { Decorator } from "../decorator/index.js";
|
import { Decorator } from "../decorator/index.js";
|
||||||
import { IEmailService } from "../service/index.js";
|
import { ICnameProxyService, IEmailService } from "../service/index.js";
|
||||||
import { FileStore } from "./file-store.js";
|
import { FileStore } from "./file-store.js";
|
||||||
import { hashUtils, utils } from "../utils/index.js";
|
import { hashUtils, utils } from "../utils/index.js";
|
||||||
// import { TimeoutPromise } from "../utils/util.promise.js";
|
// import { TimeoutPromise } from "../utils/util.promise.js";
|
||||||
|
@ -21,6 +21,7 @@ export type ExecutorOptions = {
|
||||||
onChanged: (history: RunHistory) => Promise<void>;
|
onChanged: (history: RunHistory) => Promise<void>;
|
||||||
accessService: IAccessService;
|
accessService: IAccessService;
|
||||||
emailService: IEmailService;
|
emailService: IEmailService;
|
||||||
|
cnameProxyService: ICnameProxyService;
|
||||||
fileRootDir?: string;
|
fileRootDir?: string;
|
||||||
user: UserInfo;
|
user: UserInfo;
|
||||||
};
|
};
|
||||||
|
@ -221,7 +222,7 @@ export class Executor {
|
||||||
//从outputContext读取输入参数
|
//从outputContext读取输入参数
|
||||||
const input = _.cloneDeep(step.input);
|
const input = _.cloneDeep(step.input);
|
||||||
Decorator.inject(define.input, instance, input, (item, key) => {
|
Decorator.inject(define.input, instance, input, (item, key) => {
|
||||||
if (item.component?.name === "pi-output-selector") {
|
if (item.component?.name === "output-selector") {
|
||||||
const contextKey = input[key];
|
const contextKey = input[key];
|
||||||
if (contextKey != null) {
|
if (contextKey != null) {
|
||||||
if (typeof contextKey !== "string") {
|
if (typeof contextKey !== "string") {
|
||||||
|
@ -268,6 +269,7 @@ export class Executor {
|
||||||
inputChanged,
|
inputChanged,
|
||||||
accessService: this.options.accessService,
|
accessService: this.options.accessService,
|
||||||
emailService: this.options.emailService,
|
emailService: this.options.emailService,
|
||||||
|
cnameProxyService: this.options.cnameProxyService,
|
||||||
pipelineContext: this.pipelineContext,
|
pipelineContext: this.pipelineContext,
|
||||||
userContext: this.contextFactory.getContext("user", this.options.user.id),
|
userContext: this.contextFactory.getContext("user", this.options.user.id),
|
||||||
fileStore: new FileStore({
|
fileStore: new FileStore({
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { FileItem, FormItemProps, Pipeline, Runnable, Step } from "../dt/index.j
|
||||||
import { FileStore } from "../core/file-store.js";
|
import { FileStore } from "../core/file-store.js";
|
||||||
import { Logger } from "log4js";
|
import { Logger } from "log4js";
|
||||||
import { IAccessService } from "../access/index.js";
|
import { IAccessService } from "../access/index.js";
|
||||||
import { IEmailService } from "../service/index.js";
|
import { ICnameProxyService, IEmailService } from "../service/index.js";
|
||||||
import { IContext, PluginRequestHandleReq, RunnableCollection } from "../core/index.js";
|
import { IContext, PluginRequestHandleReq, RunnableCollection } from "../core/index.js";
|
||||||
import { ILogger, logger, utils } from "../utils/index.js";
|
import { ILogger, logger, utils } from "../utils/index.js";
|
||||||
import { HttpClient } from "../utils/util.request.js";
|
import { HttpClient } from "../utils/util.request.js";
|
||||||
|
@ -70,6 +70,8 @@ export type TaskInstanceContext = {
|
||||||
accessService: IAccessService;
|
accessService: IAccessService;
|
||||||
//邮件服务
|
//邮件服务
|
||||||
emailService: IEmailService;
|
emailService: IEmailService;
|
||||||
|
//cname记录服务
|
||||||
|
cnameProxyService: ICnameProxyService;
|
||||||
//流水线上下文
|
//流水线上下文
|
||||||
pipelineContext: IContext;
|
pipelineContext: IContext;
|
||||||
//用户上下文
|
//用户上下文
|
||||||
|
@ -84,7 +86,7 @@ export type TaskInstanceContext = {
|
||||||
signal: AbortSignal;
|
signal: AbortSignal;
|
||||||
//工具类
|
//工具类
|
||||||
utils: typeof utils;
|
utils: typeof utils;
|
||||||
|
//用户信息
|
||||||
user: UserInfo;
|
user: UserInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
export type CnameProvider = {
|
||||||
|
id: any;
|
||||||
|
domain: string;
|
||||||
|
dnsProviderType: string;
|
||||||
|
accessId: any;
|
||||||
|
};
|
||||||
|
export type CnameRecord = {
|
||||||
|
id: any;
|
||||||
|
domain: string;
|
||||||
|
hostRecord: string;
|
||||||
|
recordValue: string;
|
||||||
|
cnameProvider: CnameProvider;
|
||||||
|
};
|
||||||
|
export type ICnameProxyService = {
|
||||||
|
getByDomain: (domain: string) => Promise<CnameRecord>;
|
||||||
|
};
|
|
@ -1 +1,2 @@
|
||||||
export * from "./email.js";
|
export * from "./email.js";
|
||||||
|
export * from "./cname.js";
|
||||||
|
|
|
@ -192,18 +192,17 @@ export abstract class BaseService<T> {
|
||||||
return await qb.getMany();
|
return await qb.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkUserId(id: any = 0, userId, userKey = 'userId') {
|
async checkUserId(id: any = 0, userId: number, userKey = 'userId') {
|
||||||
// @ts-ignore
|
|
||||||
const res = await this.getRepository().findOne({
|
const res = await this.getRepository().findOne({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
select: { [userKey]: true },
|
select: { [userKey]: true },
|
||||||
// @ts-ignore
|
|
||||||
where: {
|
where: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
|
||||||
if (!res || res[userKey] === userId) {
|
if (!res || res[userKey] === userId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,16 @@ export type DnsProviderDefine = Registrable & {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateRecordOptions = {
|
export type CreateRecordOptions = {
|
||||||
|
domain: string;
|
||||||
fullRecord: string;
|
fullRecord: string;
|
||||||
|
hostRecord: string;
|
||||||
type: string;
|
type: string;
|
||||||
value: any;
|
value: any;
|
||||||
domain: string;
|
|
||||||
};
|
};
|
||||||
export type RemoveRecordOptions<T> = CreateRecordOptions & {
|
export type RemoveRecordOptions<T> = {
|
||||||
|
recordReq: CreateRecordOptions;
|
||||||
// 本次创建的dns解析记录,实际上就是createRecord接口的返回值
|
// 本次创建的dns解析记录,实际上就是createRecord接口的返回值
|
||||||
record: T;
|
recordRes: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DnsProviderContext = {
|
export type DnsProviderContext = {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions } from "./api.js";
|
import { CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions } from "./api.js";
|
||||||
|
import psl from "psl";
|
||||||
|
|
||||||
export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
||||||
ctx!: DnsProviderContext;
|
ctx!: DnsProviderContext;
|
||||||
|
@ -13,3 +14,11 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
||||||
|
|
||||||
abstract removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
|
abstract removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,28 @@
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as acme from "@certd/acme-client";
|
import * as acme from "@certd/acme-client";
|
||||||
|
import { ClientExternalAccountBindingOptions, UrlMapping } from "@certd/acme-client";
|
||||||
import _ from "lodash-es";
|
import _ from "lodash-es";
|
||||||
import { Challenge } from "@certd/acme-client/types/rfc8555";
|
import { Challenge } from "@certd/acme-client/types/rfc8555";
|
||||||
import { Logger } from "log4js";
|
import { Logger } from "log4js";
|
||||||
import { IContext } from "@certd/pipeline";
|
import { IContext, utils } from "@certd/pipeline";
|
||||||
import { IDnsProvider } from "../../dns-provider/index.js";
|
import { IDnsProvider, parseDomain } from "../../dns-provider/index.js";
|
||||||
import psl from "psl";
|
|
||||||
import { ClientExternalAccountBindingOptions, UrlMapping } from "@certd/acme-client";
|
export type CnameVerifyPlan = {
|
||||||
import { utils } from "@certd/pipeline";
|
domain: string;
|
||||||
|
fullRecord: string;
|
||||||
|
dnsProvider: IDnsProvider;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DomainVerifyPlan = {
|
||||||
|
domain: string;
|
||||||
|
type: "cname" | "dns";
|
||||||
|
dnsProvider?: IDnsProvider;
|
||||||
|
cnameVerifyPlan?: Record<string, CnameVerifyPlan>;
|
||||||
|
};
|
||||||
|
export type DomainsVerifyPlan = {
|
||||||
|
[key: string]: DomainVerifyPlan;
|
||||||
|
};
|
||||||
|
|
||||||
export type CertInfo = {
|
export type CertInfo = {
|
||||||
crt: string;
|
crt: string;
|
||||||
key: string;
|
key: string;
|
||||||
|
@ -132,14 +147,7 @@ export class AcmeService {
|
||||||
return key.toString();
|
return key.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
parseDomain(fullDomain: string) {
|
async challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider, domainsVerifyPlan: DomainsVerifyPlan) {
|
||||||
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 challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider) {
|
|
||||||
this.logger.info("Triggered challengeCreateFn()");
|
this.logger.info("Triggered challengeCreateFn()");
|
||||||
|
|
||||||
/* http-01 */
|
/* http-01 */
|
||||||
|
@ -155,21 +163,62 @@ export class AcmeService {
|
||||||
// await fs.writeFileAsync(filePath, fileContents);
|
// await fs.writeFileAsync(filePath, fileContents);
|
||||||
} else if (challenge.type === "dns-01") {
|
} else if (challenge.type === "dns-01") {
|
||||||
/* dns-01 */
|
/* dns-01 */
|
||||||
const dnsRecord = `_acme-challenge.${fullDomain}`;
|
let fullRecord = `_acme-challenge.${fullDomain}`;
|
||||||
const recordValue = keyAuthorization;
|
const recordValue = keyAuthorization;
|
||||||
|
|
||||||
this.logger.info(`Creating TXT record for ${fullDomain}: ${dnsRecord}`);
|
this.logger.info(`Creating TXT record for ${fullDomain}: ${fullRecord}`);
|
||||||
/* Replace this */
|
/* Replace this */
|
||||||
this.logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`);
|
this.logger.info(`Would create TXT record "${fullRecord}" with value "${recordValue}"`);
|
||||||
|
|
||||||
const domain = this.parseDomain(fullDomain);
|
let domain = parseDomain(fullDomain);
|
||||||
this.logger.info("解析到域名domain=", domain);
|
this.logger.info("解析到域名domain=", domain);
|
||||||
return await dnsProvider.createRecord({
|
|
||||||
fullRecord: dnsRecord,
|
if (domainsVerifyPlan) {
|
||||||
|
//按照计划执行
|
||||||
|
const domainVerifyPlan = domainsVerifyPlan[domain];
|
||||||
|
if (domainVerifyPlan) {
|
||||||
|
if (domainVerifyPlan.type === "dns") {
|
||||||
|
dnsProvider = domainVerifyPlan.dnsProvider;
|
||||||
|
} else if (domainVerifyPlan.type === "cname") {
|
||||||
|
const cnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan;
|
||||||
|
if (cnameVerifyPlan) {
|
||||||
|
const cname = cnameVerifyPlan[fullDomain];
|
||||||
|
if (cname) {
|
||||||
|
dnsProvider = cname.dnsProvider;
|
||||||
|
domain = parseDomain(cname.domain);
|
||||||
|
fullRecord = cname.fullRecord;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.error("未找到域名Cname校验计划,使用默认的dnsProvider");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.error("不支持的校验类型", domainVerifyPlan.type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.info("未找到域名校验计划,使用默认的dnsProvider");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hostRecord = fullRecord.replace(`${domain}`, "");
|
||||||
|
if (hostRecord.endsWith(".")) {
|
||||||
|
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordReq = {
|
||||||
|
domain,
|
||||||
|
fullRecord,
|
||||||
|
hostRecord,
|
||||||
type: "TXT",
|
type: "TXT",
|
||||||
value: recordValue,
|
value: recordValue,
|
||||||
domain,
|
};
|
||||||
});
|
this.logger.info("添加 TXT 解析记录", JSON.stringify(recordReq));
|
||||||
|
const recordRes = await dnsProvider.createRecord(recordReq);
|
||||||
|
this.logger.info("添加 TXT 解析记录成功", JSON.stringify(recordRes));
|
||||||
|
return {
|
||||||
|
recordReq,
|
||||||
|
recordRes,
|
||||||
|
dnsProvider,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,12 +228,13 @@ export class AcmeService {
|
||||||
* @param {object} authz Authorization object
|
* @param {object} authz Authorization object
|
||||||
* @param {object} challenge Selected challenge
|
* @param {object} challenge Selected challenge
|
||||||
* @param {string} keyAuthorization Authorization key
|
* @param {string} keyAuthorization Authorization key
|
||||||
* @param recordItem challengeCreateFn create record item
|
* @param recordReq
|
||||||
|
* @param recordRes
|
||||||
* @param dnsProvider dnsProvider
|
* @param dnsProvider dnsProvider
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordItem: any, dnsProvider: IDnsProvider) {
|
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider: IDnsProvider) {
|
||||||
this.logger.info("Triggered challengeRemoveFn()");
|
this.logger.info("Triggered challengeRemoveFn()");
|
||||||
|
|
||||||
/* http-01 */
|
/* http-01 */
|
||||||
|
@ -198,24 +248,13 @@ export class AcmeService {
|
||||||
this.logger.info(`Would remove file on path "${filePath}"`);
|
this.logger.info(`Would remove file on path "${filePath}"`);
|
||||||
// await fs.unlinkAsync(filePath);
|
// await fs.unlinkAsync(filePath);
|
||||||
} else if (challenge.type === "dns-01") {
|
} else if (challenge.type === "dns-01") {
|
||||||
const dnsRecord = `_acme-challenge.${fullDomain}`;
|
this.logger.info(`删除 TXT 解析记录:${JSON.stringify(recordReq)} ,recordRes = ${JSON.stringify(recordRes)}`);
|
||||||
const recordValue = keyAuthorization;
|
|
||||||
|
|
||||||
this.logger.info(`Removing TXT record for ${fullDomain}: ${dnsRecord}`);
|
|
||||||
|
|
||||||
/* Replace this */
|
|
||||||
this.logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
|
|
||||||
|
|
||||||
const domain = this.parseDomain(fullDomain);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dnsProvider.removeRecord({
|
await dnsProvider.removeRecord({
|
||||||
fullRecord: dnsRecord,
|
recordReq,
|
||||||
type: "TXT",
|
recordRes,
|
||||||
value: keyAuthorization,
|
|
||||||
record: recordItem,
|
|
||||||
domain,
|
|
||||||
});
|
});
|
||||||
|
this.logger.info("删除解析记录成功");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error("删除解析记录出错:", e);
|
this.logger.error("删除解析记录出错:", e);
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -226,12 +265,13 @@ export class AcmeService {
|
||||||
async order(options: {
|
async order(options: {
|
||||||
email: string;
|
email: string;
|
||||||
domains: string | string[];
|
domains: string | string[];
|
||||||
dnsProvider: any;
|
dnsProvider?: any;
|
||||||
|
domainsVerifyPlan?: DomainsVerifyPlan;
|
||||||
csrInfo: any;
|
csrInfo: any;
|
||||||
isTest?: boolean;
|
isTest?: boolean;
|
||||||
privateKeyType?: string;
|
privateKeyType?: string;
|
||||||
}): Promise<CertInfo> {
|
}): Promise<CertInfo> {
|
||||||
const { email, isTest, domains, csrInfo, dnsProvider } = options;
|
const { email, isTest, domains, csrInfo, dnsProvider, domainsVerifyPlan } = options;
|
||||||
const client: acme.Client = await this.getAcmeClient(email, isTest);
|
const client: acme.Client = await this.getAcmeClient(email, isTest);
|
||||||
|
|
||||||
/* Create CSR */
|
/* Create CSR */
|
||||||
|
@ -271,8 +311,8 @@ export class AcmeService {
|
||||||
privateKey
|
privateKey
|
||||||
);
|
);
|
||||||
|
|
||||||
if (dnsProvider == null) {
|
if (dnsProvider == null && domainsVerifyPlan == null) {
|
||||||
throw new Error("dnsProvider 不能为空");
|
throw new Error("dnsProvider 、 domainsVerifyPlan 不能都为空");
|
||||||
}
|
}
|
||||||
/* 自动申请证书 */
|
/* 自动申请证书 */
|
||||||
const crt = await client.auto({
|
const crt = await client.auto({
|
||||||
|
@ -281,11 +321,22 @@ export class AcmeService {
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
skipChallengeVerification: this.skipLocalVerify,
|
skipChallengeVerification: this.skipLocalVerify,
|
||||||
challengePriority: ["dns-01"],
|
challengePriority: ["dns-01"],
|
||||||
challengeCreateFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string): Promise<any> => {
|
challengeCreateFn: async (
|
||||||
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider);
|
authz: acme.Authorization,
|
||||||
|
challenge: Challenge,
|
||||||
|
keyAuthorization: string
|
||||||
|
): Promise<{ recordReq: any; recordRes: any; dnsProvider: any }> => {
|
||||||
|
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider, domainsVerifyPlan);
|
||||||
},
|
},
|
||||||
challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordItem: any): Promise<any> => {
|
challengeRemoveFn: async (
|
||||||
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider);
|
authz: acme.Authorization,
|
||||||
|
challenge: Challenge,
|
||||||
|
keyAuthorization: string,
|
||||||
|
recordReq: any,
|
||||||
|
recordRes: any,
|
||||||
|
dnsProvider: IDnsProvider
|
||||||
|
): Promise<any> => {
|
||||||
|
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider);
|
||||||
},
|
},
|
||||||
signal: this.options.signal,
|
signal: this.options.signal,
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,7 +24,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
||||||
col: {
|
col: {
|
||||||
span: 24,
|
span: 24,
|
||||||
},
|
},
|
||||||
order: -1,
|
order: -999,
|
||||||
helper:
|
helper:
|
||||||
"1、支持通配符域名,例如: *.foo.com、foo.com、*.test.handsfree.work\n" +
|
"1、支持通配符域名,例如: *.foo.com、foo.com、*.test.handsfree.work\n" +
|
||||||
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" +
|
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" +
|
||||||
|
@ -39,6 +39,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
||||||
name: "a-input",
|
name: "a-input",
|
||||||
vModel: "value",
|
vModel: "value",
|
||||||
},
|
},
|
||||||
|
rules: [{ type: "email" }],
|
||||||
required: true,
|
required: true,
|
||||||
order: -1,
|
order: -1,
|
||||||
helper: "请输入邮箱",
|
helper: "请输入邮箱",
|
||||||
|
|
|
@ -1,13 +1,27 @@
|
||||||
import { Decorator, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from "@certd/pipeline";
|
import { Decorator, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from "@certd/pipeline";
|
||||||
import type { CertInfo, PrivateKeyType, SSLProvider } from "./acme.js";
|
import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
|
||||||
import { AcmeService } from "./acme.js";
|
import { AcmeService } from "./acme.js";
|
||||||
import _ from "lodash-es";
|
import _ from "lodash-es";
|
||||||
import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider/index.js";
|
import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry, IDnsProvider } from "../../dns-provider/index.js";
|
||||||
import { CertReader } from "./cert-reader.js";
|
import { CertReader } from "./cert-reader.js";
|
||||||
import { CertApplyBasePlugin } from "./base.js";
|
import { CertApplyBasePlugin } from "./base.js";
|
||||||
|
|
||||||
export type { CertInfo };
|
export type { CertInfo };
|
||||||
export * from "./cert-reader.js";
|
export * from "./cert-reader.js";
|
||||||
|
export type CnameRecordInput = {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
export type DomainVerifyPlanInput = {
|
||||||
|
domain: string;
|
||||||
|
type: "cname" | "dns";
|
||||||
|
dnsProviderType?: string;
|
||||||
|
dnsProviderAccessId?: number;
|
||||||
|
cnameVerifyPlan?: Record<string, CnameRecordInput>;
|
||||||
|
};
|
||||||
|
export type DomainsVerifyPlanInput = {
|
||||||
|
[key: string]: DomainVerifyPlanInput;
|
||||||
|
};
|
||||||
|
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: "CertApply",
|
name: "CertApply",
|
||||||
|
@ -26,6 +40,85 @@ export * from "./cert-reader.js";
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class CertApplyPlugin extends CertApplyBasePlugin {
|
export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
|
@TaskInput({
|
||||||
|
title: "域名验证方式",
|
||||||
|
value: "dns",
|
||||||
|
component: {
|
||||||
|
name: "a-select",
|
||||||
|
vModel: "value",
|
||||||
|
options: [
|
||||||
|
{ value: "dns", label: "DNS直接验证" },
|
||||||
|
{ value: "cname", label: "CNAME间接验证" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
helper:
|
||||||
|
"DNS直接验证:适合域名是在阿里云、腾讯云、华为云、Cloudflare、西数注册的,需要提供Access授权信息。\nCNAME间接验证:支持任何注册商注册的域名,并且不需要提供Access授权信息,但第一次需要手动添加CNAME记录",
|
||||||
|
})
|
||||||
|
challengeType!: string;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "DNS提供商",
|
||||||
|
component: {
|
||||||
|
name: "dns-provider-selector",
|
||||||
|
},
|
||||||
|
mergeScript: `
|
||||||
|
return {
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
return form.challengeType === 'dns'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
required: true,
|
||||||
|
helper: "请选择dns解析提供商,您的域名是在哪里注册的,或者域名的dns解析服务器属于哪个平台\n如果这里没有您需要的dns解析提供商,请选择CNAME间接验证校验方式",
|
||||||
|
})
|
||||||
|
dnsProviderType!: string;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "DNS解析授权",
|
||||||
|
component: {
|
||||||
|
name: "access-selector",
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
helper: "请选择dns解析提供商授权",
|
||||||
|
mergeScript: `return {
|
||||||
|
component:{
|
||||||
|
type: ctx.compute(({form})=>{
|
||||||
|
return form.dnsProviderType
|
||||||
|
})
|
||||||
|
},
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
return form.challengeType === 'dns'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
dnsProviderAccess!: number;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "域名验证配置",
|
||||||
|
component: {
|
||||||
|
name: "domains-verify-plan-editor",
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
helper: "如果选择CNAME方式,请按照上面的显示,给域名添加CNAME记录",
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
mergeScript: `return {
|
||||||
|
component:{
|
||||||
|
domains: ctx.compute(({form})=>{
|
||||||
|
return form.domains
|
||||||
|
})
|
||||||
|
},
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
return form.challengeType === 'cname'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
domainsVerifyPlan!: DomainsVerifyPlanInput;
|
||||||
|
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: "证书提供商",
|
title: "证书提供商",
|
||||||
value: "letsencrypt",
|
value: "letsencrypt",
|
||||||
|
@ -38,7 +131,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
{ value: "zerossl", label: "ZeroSSL" },
|
{ value: "zerossl", label: "ZeroSSL" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
helper: "Let's Encrypt最简单,如果使用ZeroSSL、google证书,需要提供EAB授权",
|
helper: "Let's Encrypt最简单,如果使用ZeroSSL、Google证书,需要提供EAB授权",
|
||||||
required: true,
|
required: true,
|
||||||
})
|
})
|
||||||
sslProvider!: SSLProvider;
|
sslProvider!: SSLProvider;
|
||||||
|
@ -46,7 +139,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: "EAB授权",
|
title: "EAB授权",
|
||||||
component: {
|
component: {
|
||||||
name: "pi-access-selector",
|
name: "access-selector",
|
||||||
type: "eab",
|
type: "eab",
|
||||||
},
|
},
|
||||||
maybeNeed: true,
|
maybeNeed: true,
|
||||||
|
@ -80,39 +173,11 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
// { value: "ec_521", label: "EC 521" },
|
// { value: "ec_521", label: "EC 521" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
helper: "如无特殊需求,默认即可",
|
||||||
required: true,
|
required: true,
|
||||||
})
|
})
|
||||||
privateKeyType!: PrivateKeyType;
|
privateKeyType!: PrivateKeyType;
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "DNS提供商",
|
|
||||||
component: {
|
|
||||||
name: "pi-dns-provider-selector",
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
helper:
|
|
||||||
"请选择dns解析提供商,您的域名是在哪里注册的,或者域名的dns解析服务器属于哪个平台\n如果这里没有您需要的dns解析提供商,您需要将域名解析服务器设置成上面的任意一个提供商",
|
|
||||||
})
|
|
||||||
dnsProviderType!: string;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "DNS解析授权",
|
|
||||||
component: {
|
|
||||||
name: "pi-access-selector",
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
helper: "请选择dns解析提供商授权",
|
|
||||||
mergeScript: `return {
|
|
||||||
component:{
|
|
||||||
type: ctx.compute(({form})=>{
|
|
||||||
return form.dnsProviderType
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
dnsProviderAccess!: string;
|
|
||||||
|
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: "使用代理",
|
title: "使用代理",
|
||||||
value: false,
|
value: false,
|
||||||
|
@ -120,7 +185,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
name: "a-switch",
|
name: "a-switch",
|
||||||
vModel: "checked",
|
vModel: "checked",
|
||||||
},
|
},
|
||||||
helper: "如果acme-v02.api.letsencrypt.org或dv.acme-v02.api.pki.goog被墙无法访问,请尝试开启此选项",
|
helper: "如果acme-v02.api.letsencrypt.org或dv.acme-v02.api.pki.goog被墙无法访问,请尝试开启此选项\n默认情况会进行测试,如果无法访问,将会自动使用代理",
|
||||||
})
|
})
|
||||||
useProxy = false;
|
useProxy = false;
|
||||||
|
|
||||||
|
@ -131,7 +196,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
name: "a-switch",
|
name: "a-switch",
|
||||||
vModel: "checked",
|
vModel: "checked",
|
||||||
},
|
},
|
||||||
helper: "如果重试多次出现Authorization not found TXT record,导致无法申请成功,请尝试开启此选项",
|
helper: "跳过本地校验可以加快申请速度,同时也会增加失败概率。",
|
||||||
})
|
})
|
||||||
skipLocalVerify = false;
|
skipLocalVerify = false;
|
||||||
|
|
||||||
|
@ -150,14 +215,15 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
skipLocalVerify: this.skipLocalVerify,
|
skipLocalVerify: this.skipLocalVerify,
|
||||||
useMappingProxy: this.useProxy,
|
useMappingProxy: this.useProxy,
|
||||||
privateKeyType: this.privateKeyType,
|
privateKeyType: this.privateKeyType,
|
||||||
|
// cnameProxyService: this.ctx.cnameProxyService,
|
||||||
|
// dnsProviderCreator: this.createDnsProvider.bind(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async doCertApply() {
|
async doCertApply() {
|
||||||
const email = this["email"];
|
const email = this["email"];
|
||||||
const domains = this["domains"];
|
const domains = this["domains"];
|
||||||
const dnsProviderType = this["dnsProviderType"];
|
|
||||||
const dnsProviderAccessId = this["dnsProviderAccess"];
|
|
||||||
const csrInfo = _.merge(
|
const csrInfo = _.merge(
|
||||||
{
|
{
|
||||||
country: "CN",
|
country: "CN",
|
||||||
|
@ -171,26 +237,22 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
);
|
);
|
||||||
this.logger.info("开始申请证书,", email, domains);
|
this.logger.info("开始申请证书,", email, domains);
|
||||||
|
|
||||||
const dnsProviderPlugin = dnsProviderRegistry.get(dnsProviderType);
|
let dnsProvider: any = null;
|
||||||
const DnsProviderClass = dnsProviderPlugin.target;
|
let domainsVerifyPlan: DomainsVerifyPlan = null;
|
||||||
const dnsProviderDefine = dnsProviderPlugin.define as DnsProviderDefine;
|
if (this.challengeType === "cname") {
|
||||||
if (dnsProviderDefine.deprecated) {
|
domainsVerifyPlan = await this.createDomainsVerifyPlan();
|
||||||
throw new Error(dnsProviderDefine.deprecated);
|
} else {
|
||||||
|
const dnsProviderType = this.dnsProviderType;
|
||||||
|
const dnsProviderAccessId = this.dnsProviderAccess;
|
||||||
|
dnsProvider = await this.createDnsProvider(dnsProviderType, dnsProviderAccessId);
|
||||||
}
|
}
|
||||||
const access = await this.accessService.getById(dnsProviderAccessId);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const dnsProvider: IDnsProvider = new DnsProviderClass();
|
|
||||||
const context: DnsProviderContext = { access, logger: this.logger, http: this.http, utils };
|
|
||||||
Decorator.inject(dnsProviderDefine.autowire, dnsProvider, context);
|
|
||||||
dnsProvider.setCtx(context);
|
|
||||||
await dnsProvider.onInstance();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cert = await this.acme.order({
|
const cert = await this.acme.order({
|
||||||
email,
|
email,
|
||||||
domains,
|
domains,
|
||||||
dnsProvider,
|
dnsProvider,
|
||||||
|
domainsVerifyPlan,
|
||||||
csrInfo,
|
csrInfo,
|
||||||
isTest: false,
|
isTest: false,
|
||||||
privateKeyType: this.privateKeyType,
|
privateKeyType: this.privateKeyType,
|
||||||
|
@ -207,6 +269,52 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createDnsProvider(dnsProviderType: string, dnsProviderAccessId: number): Promise<IDnsProvider> {
|
||||||
|
const dnsProviderPlugin = dnsProviderRegistry.get(dnsProviderType);
|
||||||
|
const DnsProviderClass = dnsProviderPlugin.target;
|
||||||
|
const dnsProviderDefine = dnsProviderPlugin.define as DnsProviderDefine;
|
||||||
|
if (dnsProviderDefine.deprecated) {
|
||||||
|
throw new Error(dnsProviderDefine.deprecated);
|
||||||
|
}
|
||||||
|
const access = await this.accessService.getById(dnsProviderAccessId);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const dnsProvider: IDnsProvider = new DnsProviderClass();
|
||||||
|
const context: DnsProviderContext = { access, logger: this.logger, http: this.http, utils };
|
||||||
|
Decorator.inject(dnsProviderDefine.autowire, dnsProvider, context);
|
||||||
|
dnsProvider.setCtx(context);
|
||||||
|
await dnsProvider.onInstance();
|
||||||
|
return dnsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDomainsVerifyPlan(): Promise<DomainsVerifyPlan> {
|
||||||
|
const plan: DomainsVerifyPlan = {};
|
||||||
|
for (const domain in this.domainsVerifyPlan) {
|
||||||
|
const domainVerifyPlan = this.domainsVerifyPlan[domain];
|
||||||
|
let dnsProvider = null;
|
||||||
|
const cnameVerifyPlan: Record<string, CnameVerifyPlan> = {};
|
||||||
|
if (domainVerifyPlan.type === "dns") {
|
||||||
|
dnsProvider = await this.createDnsProvider(domainVerifyPlan.dnsProviderType, domainVerifyPlan.dnsProviderAccessId);
|
||||||
|
} else {
|
||||||
|
for (const key in domainVerifyPlan.cnameVerifyPlan) {
|
||||||
|
const cnameRecord = await this.ctx.cnameProxyService.getByDomain(key);
|
||||||
|
cnameVerifyPlan[key] = {
|
||||||
|
domain: cnameRecord.cnameProvider.domain,
|
||||||
|
fullRecord: cnameRecord.recordValue,
|
||||||
|
dnsProvider: await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.accessId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plan[domain] = {
|
||||||
|
domain,
|
||||||
|
type: domainVerifyPlan.type,
|
||||||
|
dnsProvider,
|
||||||
|
cnameVerifyPlan,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new CertApplyPlugin();
|
new CertApplyPlugin();
|
||||||
|
|
|
@ -69,7 +69,7 @@ export class CertApplyLegoPlugin extends CertApplyBasePlugin {
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: "EAB授权",
|
title: "EAB授权",
|
||||||
component: {
|
component: {
|
||||||
name: "pi-access-selector",
|
name: "access-selector",
|
||||||
type: "eab",
|
type: "eab",
|
||||||
},
|
},
|
||||||
maybeNeed: true,
|
maybeNeed: true,
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<link rel="icon" href="/static/logo.svg"/>
|
<link rel="icon" href="/static/logo.svg"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Certd-让你的证书永不过期</title>
|
<title>Loading</title>
|
||||||
<script src="/static/icons/iconfont.js"></script>
|
<script src="/static/icons/iconfont.js"></script>
|
||||||
<link rel="stylesheet" type="text/css" href="/static/index.css"/>
|
<link rel="stylesheet" type="text/css" href="/static/index.css"/>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"@soerenmartius/vue3-clipboard": "^0.1.2",
|
"@soerenmartius/vue3-clipboard": "^0.1.2",
|
||||||
"@vue-js-cron/light": "^4.0.5",
|
"@vue-js-cron/light": "^4.0.5",
|
||||||
"ant-design-vue": "^4.1.2",
|
"ant-design-vue": "^4.1.2",
|
||||||
|
"async-validator": "^4.2.5",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"axios-mock-adapter": "^1.22.0",
|
"axios-mock-adapter": "^1.22.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
|
@ -49,6 +50,7 @@
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"pinia": "2.1.7",
|
"pinia": "2.1.7",
|
||||||
|
"psl": "^1.9.0",
|
||||||
"qiniu-js": "^3.4.2",
|
"qiniu-js": "^3.4.2",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.2",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
defineOptions({
|
||||||
|
name: "CronEditor"
|
||||||
|
});
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue?: string;
|
modelValue?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
<template>
|
|
||||||
<a-select class="pi-dns-provider-selector" :value="modelValue" :options="options" @update:value="onChanged">
|
|
||||||
</a-select>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { inject, Ref, ref, watch } from "vue";
|
|
||||||
import * as api from "./api";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "PiDnsProviderSelector",
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props:any, ctx:any) {
|
|
||||||
const options = ref<any[]>([]);
|
|
||||||
|
|
||||||
async function onCreate() {
|
|
||||||
const list = await api.GetList();
|
|
||||||
const array: any[] = [];
|
|
||||||
for (let item of list) {
|
|
||||||
array.push({
|
|
||||||
value: item.name,
|
|
||||||
label: item.title
|
|
||||||
});
|
|
||||||
}
|
|
||||||
options.value = array;
|
|
||||||
if (props.modelValue == null && options.value.length > 0) {
|
|
||||||
ctx.emit("update:modelValue", options.value[0].value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onCreate();
|
|
||||||
|
|
||||||
function onChanged(value:any) {
|
|
||||||
ctx.emit("update:modelValue", value);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
options,
|
|
||||||
onChanged
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.step-edit-form {
|
|
||||||
.body {
|
|
||||||
padding: 10px;
|
|
||||||
.ant-card {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
border-color: #00b7ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-card-meta-title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-avatar {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-left: 5px;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex: 1;
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-card-body {
|
|
||||||
padding: 14px;
|
|
||||||
height: 100px;
|
|
||||||
|
|
||||||
overflow-y: hidden;
|
|
||||||
|
|
||||||
.ant-card-meta-description {
|
|
||||||
font-size: 10px;
|
|
||||||
line-height: 20px;
|
|
||||||
height: 40px;
|
|
||||||
color: #7f7f7f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="pi-editable" :class="{ disabled, 'hover-show': hoverShow }">
|
<div class="text-editable" :class="{ disabled, 'hover-show': hoverShow }">
|
||||||
<div v-if="isEdit" class="input">
|
<div v-if="isEdit" class="input">
|
||||||
<a-input ref="inputRef" v-model:value="valueRef" :validate-status="modelValue ? '' : 'error'" v-bind="input" @keyup.enter="save()" @blur="save()">
|
<a-input ref="inputRef" v-model:value="valueRef" :validate-status="modelValue ? '' : 'error'" v-bind="input" @keyup.enter="save()" @blur="save()">
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
import { watch, ref, nextTick } from "vue";
|
import { watch, ref, nextTick } from "vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PiEditable",
|
name: "TextEditable",
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -73,7 +73,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.pi-editable {
|
.text-editable {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import PiContainer from "./container.vue";
|
import PiContainer from "./container.vue";
|
||||||
import PiAccessSelector from "../views/certd/access/access-selector/index.vue";
|
import TextEditable from "./editable.vue";
|
||||||
import PiDnsProviderSelector from "./dns-provider-selector/index.vue";
|
|
||||||
import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";
|
|
||||||
import PiEditable from "./editable.vue";
|
|
||||||
import vip from "./vip-button/install.js";
|
import vip from "./vip-button/install.js";
|
||||||
import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
|
import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
|
||||||
import CronEditor from "./cron-editor/index.vue";
|
import CronEditor from "./cron-editor/index.vue";
|
||||||
|
@ -13,10 +10,7 @@ import Plugins from "./plugins/index";
|
||||||
export default {
|
export default {
|
||||||
install(app: any) {
|
install(app: any) {
|
||||||
app.component("PiContainer", PiContainer);
|
app.component("PiContainer", PiContainer);
|
||||||
app.component("PiAccessSelector", PiAccessSelector);
|
app.component("TextEditable", TextEditable);
|
||||||
app.component("PiEditable", PiEditable);
|
|
||||||
app.component("PiOutputSelector", PiOutputSelector);
|
|
||||||
app.component("PiDnsProviderSelector", PiDnsProviderSelector);
|
|
||||||
|
|
||||||
app.component("CronLight", CronLight);
|
app.component("CronLight", CronLight);
|
||||||
app.component("CronEditor", CronEditor);
|
app.component("CronEditor", CronEditor);
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<a-select class="dns-provider-selector" :value="modelValue" :options="options" @update:value="onChanged"> </a-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import * as api from "./api";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DnsProviderSelector",
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ["update:modelValue"],
|
||||||
|
setup(props: any, ctx: any) {
|
||||||
|
const options = ref<any[]>([]);
|
||||||
|
|
||||||
|
async function onCreate() {
|
||||||
|
const list = await api.GetList();
|
||||||
|
const array: any[] = [];
|
||||||
|
for (let item of list) {
|
||||||
|
array.push({
|
||||||
|
value: item.name,
|
||||||
|
label: item.title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
options.value = array;
|
||||||
|
if (props.modelValue == null && options.value.length > 0) {
|
||||||
|
ctx.emit("update:modelValue", options.value[0].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCreate();
|
||||||
|
|
||||||
|
function onChanged(value: any) {
|
||||||
|
ctx.emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
onChanged
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less"></style>
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { request } from "/src/api/service";
|
||||||
|
|
||||||
|
const apiPrefix = "/cname/record";
|
||||||
|
|
||||||
|
export type CnameRecord = {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function GetList() {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/list",
|
||||||
|
method: "post"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetByDomain(domain: string) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/getByDomain",
|
||||||
|
method: "post",
|
||||||
|
data: {
|
||||||
|
domain,
|
||||||
|
createOnNotFound: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<tr v-if="cnameRecord" class="cname-record-info">
|
||||||
|
<!-- <td class="domain">-->
|
||||||
|
<!-- {{ props.domain }}-->
|
||||||
|
<!-- </td>-->
|
||||||
|
<td class="host-record" :title="'域名:' + props.domain">
|
||||||
|
<fs-copyable v-model="cnameRecord.hostRecord"></fs-copyable>
|
||||||
|
</td>
|
||||||
|
<td class="record-value">
|
||||||
|
<fs-copyable v-model="cnameRecord.recordValue"></fs-copyable>
|
||||||
|
</td>
|
||||||
|
<td class="status center flex-center">
|
||||||
|
<fs-values-format v-model="cnameRecord.status" :dict="statusDict" />
|
||||||
|
<fs-icon icon="ion:refresh-outline" class="pointer" @click="doRefresh"></fs-icon>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CnameRecord, GetByDomain } from "/@/components/plugins/cert/domains-verify-plan-editor/api";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { dict } from "@fast-crud/fast-crud";
|
||||||
|
|
||||||
|
const statusDict = dict({
|
||||||
|
data: [
|
||||||
|
{ label: "待设置CNAME", value: "cname", color: "warning" },
|
||||||
|
{ label: "验证中", value: "validating", color: "primary" },
|
||||||
|
{ label: "验证成功", value: "valid", color: "success" },
|
||||||
|
{ label: "验证失败", value: "failed", color: "error" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "CnameRecordInfo"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
domain: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: {
|
||||||
|
id: number | null;
|
||||||
|
status: string | null;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const cnameRecord = ref<CnameRecord | null>(null);
|
||||||
|
|
||||||
|
function onRecordChange() {
|
||||||
|
emit("change", {
|
||||||
|
id: cnameRecord.value?.id,
|
||||||
|
status: cnameRecord.value?.status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRefresh() {
|
||||||
|
if (!props.domain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cnameRecord.value = await GetByDomain(props.domain);
|
||||||
|
onRecordChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.domain,
|
||||||
|
async (value) => {
|
||||||
|
await doRefresh();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.cname-record-info {
|
||||||
|
.fs-copyable {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<table class="cname-verify-plan">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 160px">主机记录</td>
|
||||||
|
<td style="width: 250px">请设置CNAME记录</td>
|
||||||
|
<td style="width: 120px" class="center">状态</td>
|
||||||
|
</tr>
|
||||||
|
<template v-for="key in domains" :key="key">
|
||||||
|
<cname-record-info :domain="key" @change="onRecordChange(key, $event)" />
|
||||||
|
</template>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CnameRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/api";
|
||||||
|
import CnameRecordInfo from "/@/components/plugins/cert/domains-verify-plan-editor/cname-record-info.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "CnameVerifyPlan"
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": any;
|
||||||
|
change: Record<string, any>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Record<string, any>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const domains = computed(() => {
|
||||||
|
return Object.keys(props.modelValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
function onRecordChange(domain: string, record: CnameRecord) {
|
||||||
|
const value = { ...props.modelValue };
|
||||||
|
value[domain] = record;
|
||||||
|
emit("update:modelValue", value);
|
||||||
|
emit("change", value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.cname-verify-plan {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
border: 0 !important;
|
||||||
|
border-bottom: 1px solid #e8e8e8 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
td {
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,315 @@
|
||||||
|
<template>
|
||||||
|
<div class="domains-verify-plan-editor" :class="{ fullscreen }">
|
||||||
|
<div class="fullscreen-modal" @click="fullscreenExit"></div>
|
||||||
|
<div class="plan-wrapper">
|
||||||
|
<div class="plan-box">
|
||||||
|
<div class="fullscreen-button pointer">
|
||||||
|
<fs-icon :icon="fullscreen ? 'material-symbols:fullscreen' : 'material-symbols:fullscreen-exit'" @click="fullscreen = !fullscreen"></fs-icon>
|
||||||
|
</div>
|
||||||
|
<table class="plan-table">
|
||||||
|
<tr>
|
||||||
|
<th>域名</th>
|
||||||
|
<th>验证方式</th>
|
||||||
|
<th>验证计划</th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="(item, key) of planRef" :key="key" class="row">
|
||||||
|
<td>{{ item.domain }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="type">
|
||||||
|
<a-select v-model:value="item.type" size="small" :options="challengeTypeOptions" @change="onPlanChanged"></a-select>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 0">
|
||||||
|
<div class="plan">
|
||||||
|
<div v-if="item.type === 'dns'" class="plan-dns">
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="label">DNS类型:</span>
|
||||||
|
<span class="input">
|
||||||
|
<fs-dict-select
|
||||||
|
v-model="item.dnsProviderType"
|
||||||
|
size="small"
|
||||||
|
:dict="dnsProviderTypeDict"
|
||||||
|
placeholder="DNS提供商"
|
||||||
|
@change="onPlanChanged"
|
||||||
|
></fs-dict-select>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<a-divider type="vertical" />
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="label">DNS授权:</span>
|
||||||
|
<span class="input">
|
||||||
|
<access-selector
|
||||||
|
v-model="item.dnsProviderAccessId"
|
||||||
|
size="small"
|
||||||
|
:type="item.dnsProviderType"
|
||||||
|
placeholder="请选择"
|
||||||
|
@change="onPlanChanged"
|
||||||
|
></access-selector>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.type === 'cname'" class="plan-cname">
|
||||||
|
<cname-verify-plan v-model="item.cnameVerifyPlan" @change="onPlanChanged" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="error">
|
||||||
|
{{ errorMessageRef }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
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 psl from "psl";
|
||||||
|
defineOptions({
|
||||||
|
name: "DomainsVerifyPlanEditor"
|
||||||
|
});
|
||||||
|
|
||||||
|
type DomainVerifyPlanInput = {
|
||||||
|
domain: string;
|
||||||
|
type: "cname" | "dns";
|
||||||
|
dnsProviderType?: string;
|
||||||
|
dnsProviderAccessId?: number;
|
||||||
|
cnameVerifyPlan?: Record<string, CnameRecord>;
|
||||||
|
};
|
||||||
|
type DomainsVerifyPlanInput = {
|
||||||
|
[key: string]: DomainVerifyPlanInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
const challengeTypeOptions = ref<any[]>([
|
||||||
|
{
|
||||||
|
label: "DNS验证",
|
||||||
|
value: "dns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "CNAME验证",
|
||||||
|
value: "cname"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue?: DomainsVerifyPlanInput;
|
||||||
|
domains?: string[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const fullscreen = ref(false);
|
||||||
|
function fullscreenExit() {
|
||||||
|
if (fullscreen.value) {
|
||||||
|
fullscreen.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const planRef = ref<DomainsVerifyPlanInput>(props.modelValue || {});
|
||||||
|
const dnsProviderTypeDict = dict({
|
||||||
|
url: "pi/dnsProvider/dnsProviderTypeDict"
|
||||||
|
});
|
||||||
|
function onPlanChanged() {
|
||||||
|
debugger;
|
||||||
|
emit("update:modelValue", planRef.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessageRef = ref<string>("");
|
||||||
|
function showError(error: string) {
|
||||||
|
errorMessageRef.value = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainGroup = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
[key: string]: CnameRecord;
|
||||||
|
}
|
||||||
|
>[];
|
||||||
|
|
||||||
|
function onDomainsChanged(domains: string[]) {
|
||||||
|
console.log("域名变化", domains);
|
||||||
|
if (domains == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domainGroups: DomainGroup = {};
|
||||||
|
for (let domain of domains) {
|
||||||
|
domain = domain.replace("*.", "");
|
||||||
|
const parsed = psl.parse(domain);
|
||||||
|
if (parsed.error) {
|
||||||
|
showError(`域名${domain}解析失败: ${JSON.stringify(parsed.error)}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const mainDomain = parsed.domain;
|
||||||
|
let group = domainGroups[mainDomain];
|
||||||
|
if (!group) {
|
||||||
|
group = {};
|
||||||
|
domainGroups[mainDomain] = group;
|
||||||
|
}
|
||||||
|
group[domain] = {
|
||||||
|
id: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const domain in domainGroups) {
|
||||||
|
let planItem = planRef.value[domain];
|
||||||
|
const subDomains = domainGroups[domain];
|
||||||
|
if (!planItem) {
|
||||||
|
planItem = {
|
||||||
|
domain,
|
||||||
|
type: "cname",
|
||||||
|
cnameVerifyPlan: {
|
||||||
|
...subDomains
|
||||||
|
}
|
||||||
|
};
|
||||||
|
planRef.value[domain] = planItem;
|
||||||
|
} else {
|
||||||
|
const cnamePlan = planItem.cnameVerifyPlan;
|
||||||
|
for (const subDomain in subDomains) {
|
||||||
|
if (!cnamePlan[subDomain]) {
|
||||||
|
cnamePlan[subDomain] = {
|
||||||
|
id: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const subDomain of Object.keys(cnamePlan)) {
|
||||||
|
if (!subDomains[subDomain]) {
|
||||||
|
delete cnamePlan[subDomain];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const domain of Object.keys(planRef.value)) {
|
||||||
|
const mainDomains = Object.keys(domainGroups);
|
||||||
|
if (!mainDomains.includes(domain)) {
|
||||||
|
delete planRef.value[domain];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => {
|
||||||
|
return props.domains;
|
||||||
|
},
|
||||||
|
(domains: string[]) => {
|
||||||
|
onDomainsChanged(domains);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.domains-verify-plan-editor {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
overflow-x: auto;
|
||||||
|
.fullscreen-modal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(74, 74, 74, 0.78);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 100px;
|
||||||
|
margin: auto;
|
||||||
|
.plan-wrapper {
|
||||||
|
width: 1400px;
|
||||||
|
margin: auto;
|
||||||
|
//background-color: #a3a3a3;
|
||||||
|
//padding: 50px;
|
||||||
|
.plan-box {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-modal {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 10px;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-table {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
//table-layout: fixed;
|
||||||
|
th {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
border-left: 1px solid #e8e8e8;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px 6px;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
border-left: 1px solid #e8e8e8;
|
||||||
|
padding: 6px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan {
|
||||||
|
font-size: 14px;
|
||||||
|
.ant-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.plan-dns {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
.form-item {
|
||||||
|
min-width: 250px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
.label {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.plan-cname {
|
||||||
|
.cname-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
.domain {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
.cname-record {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, ref, watch } from "vue";
|
import { inject, ref, watch } from "vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "CertDomainsGetter"
|
||||||
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
inputKey?: string;
|
inputKey?: string;
|
||||||
modelValue?: string[];
|
modelValue?: string[];
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<a-select class="pi-output-selector" :value="modelValue" :options="options" @update:value="onChanged"> </a-select>
|
<a-select class="output-selector" :value="modelValue" :options="options" @update:value="onChanged"> </a-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { inject, onMounted, Ref, ref, watch } from "vue";
|
import { inject, onMounted, Ref, ref, watch } from "vue";
|
||||||
import { pluginManager } from "../../plugin";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PiOutputSelector",
|
name: "OutputSelector",
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined
|
default: undefined
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line vue/require-default-prop
|
||||||
from: {
|
from: {
|
||||||
type: [String, Array]
|
type: [String, Array]
|
||||||
}
|
}
|
|
@ -2,6 +2,10 @@
|
||||||
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
|
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "RemoteSelect"
|
||||||
|
});
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<
|
||||||
{
|
{
|
||||||
watches: string[];
|
watches: string[];
|
||||||
|
@ -63,14 +67,14 @@ watch(
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<a-select
|
<a-select
|
||||||
class="remote-select"
|
class="remote-select"
|
||||||
show-search
|
show-search
|
||||||
:filter-option="filterOption"
|
:filter-option="filterOption"
|
||||||
:options="optionsRef"
|
:options="optionsRef"
|
||||||
:value="value"
|
:value="value"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@update:value="emit('update:value', $event)"
|
@update:value="emit('update:value', $event)"
|
||||||
/>
|
/>
|
||||||
<div class="helper">
|
<div class="helper">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
import SynologyIdDeviceGetter from "./synology/device-id-getter.vue";
|
import SynologyIdDeviceGetter from "./synology/device-id-getter.vue";
|
||||||
import RemoteSelect from "./common/remote-select.vue";
|
import RemoteSelect from "./common/remote-select.vue";
|
||||||
import CertDomainsGetter from "./common/cert-domains-getter.vue";
|
import CertDomainsGetter from "./common/cert-domains-getter.vue";
|
||||||
|
import OutputSelector from "/@/components/plugins/common/output-selector/index.vue";
|
||||||
|
import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue";
|
||||||
|
import DomainsVerifyPlanEditor from "/@/components/plugins/cert/domains-verify-plan-editor/index.vue";
|
||||||
|
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install(app: any) {
|
install(app: any) {
|
||||||
|
app.component("OutputSelector", OutputSelector);
|
||||||
|
app.component("DnsProviderSelector", DnsProviderSelector);
|
||||||
|
app.component("DomainsVerifyPlanEditor", DomainsVerifyPlanEditor);
|
||||||
|
app.component("AccessSelector", AccessSelector);
|
||||||
|
|
||||||
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
|
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
|
||||||
app.component("RemoteSelect", RemoteSelect);
|
app.component("RemoteSelect", RemoteSelect);
|
||||||
app.component("CertDomainsGetter", CertDomainsGetter);
|
app.component("CertDomainsGetter", CertDomainsGetter);
|
||||||
|
|
|
@ -14,6 +14,10 @@ import { defineProps, ref, useAttrs } from "vue";
|
||||||
import { Modal } from "ant-design-vue";
|
import { Modal } from "ant-design-vue";
|
||||||
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
|
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "DeviceIdGetter"
|
||||||
|
});
|
||||||
|
|
||||||
const props = defineProps<ComponentPropsType>();
|
const props = defineProps<ComponentPropsType>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -68,9 +68,13 @@ function install(app: App, options: any = {}) {
|
||||||
},
|
},
|
||||||
conditionalRender: {
|
conditionalRender: {
|
||||||
match(scope) {
|
match(scope) {
|
||||||
|
if (scope.column.conditionalRenderDisabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (scope.key === "__blank__") {
|
if (scope.key === "__blank__") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//不能用 !scope.value , 否则switch组件设置为关之后就消失了
|
//不能用 !scope.value , 否则switch组件设置为关之后就消失了
|
||||||
const { value, key, props } = scope;
|
const { value, key, props } = scope;
|
||||||
return !value && key != "_index" && value != false;
|
return !value && key != "_index" && value != false;
|
||||||
|
@ -349,8 +353,8 @@ function install(app: App, options: any = {}) {
|
||||||
columnProps.column = {};
|
columnProps.column = {};
|
||||||
}
|
}
|
||||||
columnProps.column.resizable = true;
|
columnProps.column.resizable = true;
|
||||||
if (!columnProps.column.width) {
|
if (columnProps.column.width == null) {
|
||||||
columnProps.column.width = -1;
|
columnProps.column.width = 200;
|
||||||
} else if (typeof columnProps.column?.width === "string" && columnProps.column.width.indexOf("px") > -1) {
|
} else if (typeof columnProps.column?.width === "string" && columnProps.column.width.indexOf("px") > -1) {
|
||||||
columnProps.column.width = parseInt(columnProps.column.width.replace("px", ""));
|
columnProps.column.width = parseInt(columnProps.column.width.replace("px", ""));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import Validator from "async-validator";
|
import Validator from "async-validator";
|
||||||
// 自定义验证器函数
|
// 自定义验证器函数
|
||||||
function isDomain(rule, value, callback) {
|
function isDomain(rule, value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
let domains: string[] = value;
|
let domains: string[] = value;
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
domains = value.split(",");
|
domains = value.split(",");
|
||||||
}
|
}
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
//域名可以是泛域名,中文域名,数字域名,英文域名,域名中可以包含-和.
|
//域名可以是泛域名,中文域名,数字域名,英文域名,域名中可以包含-和. ,可以_开头
|
||||||
if (!/^(?:[0-9a-zA-Z\u4e00-\u9fa5-]+\.)+[0-9a-zA-Z\u4e00-\u9fa5-]+$/.test(domain)) {
|
if (!/^(?:\*\.|[0-9a-zA-Z\u4e00-\u9fa5-]+\.)+[0-9a-zA-Z\u4e00-\u9fa5-]+$/.test(domain)) {
|
||||||
callback(new Error(`域名有误:${domain},请输入正确的域名`));
|
throw new Error(`域名有误:${domain},请输入正确的域名`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
// 注册自定义验证器
|
// 注册自定义验证器
|
||||||
Validator.register("domains", isDomain);
|
Validator.register("domains", isDomain);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export const certdResources = [
|
export const certdResources = [
|
||||||
{
|
{
|
||||||
title: "证书自动化",
|
title: "证书自动化",
|
||||||
name: "certd",
|
name: "CertdRoot",
|
||||||
path: "/certd",
|
path: "/certd",
|
||||||
redirect: "/certd/pipeline",
|
redirect: "/certd/pipeline",
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -11,7 +11,7 @@ export const certdResources = [
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: "证书自动化流水线",
|
title: "证书自动化流水线",
|
||||||
name: "pipeline",
|
name: "PipelineManager",
|
||||||
path: "/certd/pipeline",
|
path: "/certd/pipeline",
|
||||||
component: "/certd/pipeline/index.vue",
|
component: "/certd/pipeline/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -20,7 +20,7 @@ export const certdResources = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "编辑流水线",
|
title: "编辑流水线",
|
||||||
name: "pipelineEdit",
|
name: "PipelineEdit",
|
||||||
path: "/certd/pipeline/detail",
|
path: "/certd/pipeline/detail",
|
||||||
component: "/certd/pipeline/detail.vue",
|
component: "/certd/pipeline/detail.vue",
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -29,7 +29,7 @@ export const certdResources = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "执行历史记录",
|
title: "执行历史记录",
|
||||||
name: "pipelineHistory",
|
name: "PipelineHistory",
|
||||||
path: "/certd/history",
|
path: "/certd/history",
|
||||||
component: "/certd/history/index.vue",
|
component: "/certd/history/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -38,44 +38,43 @@ export const certdResources = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "授权管理",
|
title: "授权管理",
|
||||||
name: "access",
|
name: "AccessManager",
|
||||||
path: "/certd/access",
|
path: "/certd/access",
|
||||||
component: "/certd/access/index.vue",
|
component: "/certd/access/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ion:disc-outline"
|
icon: "ion:disc-outline",
|
||||||
|
auth: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "设置",
|
title: "CNAME记录管理",
|
||||||
name: "certdSettings",
|
name: "CnameRecord",
|
||||||
path: "/certd/settings",
|
path: "/certd/cname/record",
|
||||||
redirect: "/certd/settings/email",
|
component: "/certd/cname/record/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ion:settings-outline",
|
icon: "ion:disc-outline",
|
||||||
auth: true
|
auth: true
|
||||||
},
|
}
|
||||||
children: [
|
},
|
||||||
{
|
{
|
||||||
title: "邮箱设置",
|
title: "邮箱设置",
|
||||||
name: "email",
|
name: "EmailSetting",
|
||||||
path: "/certd/settings/email",
|
path: "/certd/settings/email",
|
||||||
component: "/certd/settings/email-setting.vue",
|
component: "/certd/settings/email-setting.vue",
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ion:mail-outline",
|
icon: "ion:mail-outline",
|
||||||
auth: true
|
auth: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "账号信息",
|
title: "账号信息",
|
||||||
name: "userProfile",
|
name: "UserProfile",
|
||||||
path: "/certd/mine/user-profile",
|
path: "/certd/mine/user-profile",
|
||||||
component: "/certd/mine/user-profile.vue",
|
component: "/certd/mine/user-profile.vue",
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ion:person-outline",
|
icon: "ion:person-outline",
|
||||||
auth: true
|
auth: true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import LayoutPass from "/@/layout/layout-pass.vue";
|
import LayoutPass from "/@/layout/layout-pass.vue";
|
||||||
import { computed } from "vue";
|
|
||||||
import { useUserStore } from "/@/store/modules/user";
|
|
||||||
import { useSettingStore } from "/@/store/modules/settings";
|
import { useSettingStore } from "/@/store/modules/settings";
|
||||||
|
|
||||||
export const sysResources = [
|
export const sysResources = [
|
||||||
{
|
{
|
||||||
title: "系统管理",
|
title: "系统管理",
|
||||||
name: "sys",
|
name: "SysRoot",
|
||||||
path: "/sys",
|
path: "/sys",
|
||||||
redirect: "/sys/settings",
|
redirect: "/sys/settings",
|
||||||
component: LayoutPass,
|
component: LayoutPass,
|
||||||
|
@ -17,7 +15,7 @@ export const sysResources = [
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: "权限管理",
|
title: "权限管理",
|
||||||
name: "authority",
|
name: "AuthorityManager",
|
||||||
path: "/sys/authority",
|
path: "/sys/authority",
|
||||||
redirect: "/sys/authority/permission",
|
redirect: "/sys/authority/permission",
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -28,61 +26,62 @@ export const sysResources = [
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: "权限资源管理",
|
title: "权限资源管理",
|
||||||
name: "permission",
|
name: "PermissionManager",
|
||||||
|
path: "/sys/authority/permission",
|
||||||
|
component: "/sys/authority/permission/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ion:list-outline",
|
icon: "ion:list-outline",
|
||||||
//需要校验权限
|
//需要校验权限
|
||||||
permission: "sys:auth:per:view"
|
permission: "sys:auth:per:view"
|
||||||
},
|
}
|
||||||
path: "/sys/authority/permission",
|
|
||||||
component: "/sys/authority/permission/index.vue"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "角色管理",
|
title: "角色管理",
|
||||||
name: "role",
|
name: "RoleManager",
|
||||||
|
path: "/sys/authority/role",
|
||||||
|
component: "/sys/authority/role/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ion:people-outline",
|
icon: "ion:people-outline",
|
||||||
permission: "sys:auth:role:view"
|
permission: "sys:auth:role:view"
|
||||||
},
|
}
|
||||||
path: "/sys/authority/role",
|
|
||||||
component: "/sys/authority/role/index.vue"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "用户管理",
|
title: "用户管理",
|
||||||
name: "user",
|
name: "UserManager",
|
||||||
|
path: "/sys/authority/user",
|
||||||
|
component: "/sys/authority/user/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ion:person-outline",
|
icon: "ion:person-outline",
|
||||||
permission: "sys:auth:user:view"
|
permission: "sys:auth:user:view"
|
||||||
},
|
}
|
||||||
path: "/sys/authority/user",
|
|
||||||
component: "/sys/authority/user/index.vue"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "账号绑定",
|
|
||||||
name: "account",
|
|
||||||
meta: {
|
|
||||||
icon: "ion:golf-outline",
|
|
||||||
permission: "sys:settings:view"
|
|
||||||
},
|
|
||||||
path: "/sys/account",
|
|
||||||
component: "/sys/account/index.vue"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "系统设置",
|
title: "系统设置",
|
||||||
name: "settings",
|
name: "SysSettings",
|
||||||
|
path: "/sys/settings",
|
||||||
|
component: "/sys/settings/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ion:settings-outline",
|
icon: "ion:settings-outline",
|
||||||
permission: "sys:settings:view"
|
permission: "sys:settings:view"
|
||||||
},
|
}
|
||||||
path: "/sys/settings",
|
},
|
||||||
component: "/sys/settings/index.vue"
|
{
|
||||||
|
title: "CNAME服务设置",
|
||||||
|
name: "CnameSetting",
|
||||||
|
path: "/sys/cname/provider",
|
||||||
|
component: "/sys/cname/provider/index.vue",
|
||||||
|
meta: {
|
||||||
|
icon: "ion:settings-outline",
|
||||||
|
permission: "sys:settings:view"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "站点个性化",
|
title: "站点个性化",
|
||||||
name: "site",
|
name: "SiteSetting",
|
||||||
path: "/sys/site",
|
path: "/sys/site",
|
||||||
|
component: "/sys/site/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
show: () => {
|
show: () => {
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
|
@ -90,9 +89,8 @@ export const sysResources = [
|
||||||
},
|
},
|
||||||
icon: "ion:document-text-outline",
|
icon: "ion:document-text-outline",
|
||||||
permission: "sys:settings:view"
|
permission: "sys:settings:view"
|
||||||
},
|
}
|
||||||
component: "/sys/site/index.vue"
|
},
|
||||||
}
|
|
||||||
// {
|
// {
|
||||||
// title: "商业版设置",
|
// title: "商业版设置",
|
||||||
// name: "SysCommercial",
|
// name: "SysCommercial",
|
||||||
|
@ -120,6 +118,16 @@ export const sysResources = [
|
||||||
// }
|
// }
|
||||||
// ]
|
// ]
|
||||||
// }
|
// }
|
||||||
|
{
|
||||||
|
title: "账号绑定",
|
||||||
|
name: "AccountBind",
|
||||||
|
path: "/sys/account",
|
||||||
|
component: "/sys/account/index.vue",
|
||||||
|
meta: {
|
||||||
|
icon: "ion:golf-outline",
|
||||||
|
permission: "sys:settings:view"
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -20,7 +20,7 @@ div#app {
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
margin-top:0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fs-desc {
|
.fs-desc {
|
||||||
|
@ -69,13 +69,13 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-col{
|
.flex-col {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-y{
|
.scroll-y {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +149,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
.w-100 {
|
.w-100 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-100 {
|
.h-100 {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -187,21 +188,34 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.deleted{
|
.deleted {
|
||||||
color: #c7c7c7;
|
color: #c7c7c7;
|
||||||
//删除线
|
//删除线
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-move{
|
.cursor-move {
|
||||||
cursor: move !important;
|
cursor: move !important;
|
||||||
}
|
}
|
||||||
.cursor-pointer{
|
|
||||||
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.helper{
|
.helper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: #aeaeae;
|
color: #aeaeae;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.fs-copyable {
|
||||||
|
.text {
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button {
|
||||||
|
position: relative !important;
|
||||||
|
/* right: 0; */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="pi-access-selector">
|
<div class="access-selector">
|
||||||
<span v-if="target.name" class="mr-5 cd-flex-inline">
|
<span v-if="target.name" class="mr-5 cd-flex-inline">
|
||||||
<span class="mr-5">{{ target.name }}</span>
|
<span class="mr-5">{{ target.name }}</span>
|
||||||
<fs-icon class="cd-icon-button" icon="ion:close-circle-outline" @click="clear"></fs-icon>
|
<fs-icon class="cd-icon-button" icon="ion:close-circle-outline" @click="clear"></fs-icon>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="mlr-5 gray">请选择</span>
|
<span v-else class="mlr-5 text-gray">{{ placeholder }}</span>
|
||||||
<a-button class="ml-5" @click="chooseForm.open">选择</a-button>
|
<a-button class="ml-5" :size="size" @click="chooseForm.open">选择</a-button>
|
||||||
<a-form-item-rest v-if="chooseForm.show">
|
<a-form-item-rest v-if="chooseForm.show">
|
||||||
<a-modal v-model:open="chooseForm.show" title="选择授权提供者" width="900px" @ok="chooseForm.ok">
|
<a-modal v-model:open="chooseForm.show" title="选择授权提供者" width="900px" @ok="chooseForm.ok">
|
||||||
<div style="height: 400px; position: relative">
|
<div style="height: 400px; position: relative">
|
||||||
|
@ -23,7 +23,7 @@ import CertAccessModal from "./access/index.vue";
|
||||||
import { GetProviderDefineByAccessType } from "../api";
|
import { GetProviderDefineByAccessType } from "../api";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "PiAccessSelector",
|
name: "AccessSelector",
|
||||||
components: { CertAccessModal },
|
components: { CertAccessModal },
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
@ -33,6 +33,14 @@ export default defineComponent({
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "aliyun"
|
default: "aliyun"
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "请选择"
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "middle"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { useFs } from "@fast-crud/fast-crud";
|
||||||
import createCrudOptions from "./crud";
|
import createCrudOptions from "./crud";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "CertdAccess",
|
name: "AccessManager",
|
||||||
setup() {
|
setup() {
|
||||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
|
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { request } from "/src/api/service";
|
||||||
|
|
||||||
|
const apiPrefix = "/cname/record";
|
||||||
|
|
||||||
|
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 + "/deleteByIds",
|
||||||
|
method: "post",
|
||||||
|
data: { ids }
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
import * as api from "./api";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { computed, Ref, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
|
||||||
|
import { useUserStore } from "/@/store/modules/user";
|
||||||
|
import { useSettingStore } from "/@/store/modules/settings";
|
||||||
|
|
||||||
|
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
|
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) => {
|
||||||
|
form.content = JSON.stringify({
|
||||||
|
title: form.title
|
||||||
|
});
|
||||||
|
const res = await api.AddObj(form);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const selectedRowKeys: Ref<any[]> = 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
|
||||||
|
},
|
||||||
|
rowHandle: {
|
||||||
|
minWidth: 200,
|
||||||
|
fixed: "right"
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
id: {
|
||||||
|
title: "ID",
|
||||||
|
key: "id",
|
||||||
|
type: "number",
|
||||||
|
column: {
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
title: "被代理域名",
|
||||||
|
type: "text",
|
||||||
|
search: {
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
editForm: {
|
||||||
|
component: {
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hostRecord: {
|
||||||
|
title: "主机记录",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 250,
|
||||||
|
cellRender: ({ value }) => {
|
||||||
|
return <fs-copyable v-model={value} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recordValue: {
|
||||||
|
title: "请设置CNAME",
|
||||||
|
type: "copyable",
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cnameProviderId: {
|
||||||
|
title: "CNAME提供者",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: dict({
|
||||||
|
url: "/cname/provider/list",
|
||||||
|
value: "id",
|
||||||
|
label: "domain"
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
component: {
|
||||||
|
onDictChange: ({ form, dict }) => {
|
||||||
|
if (!form.cnameProviderId) {
|
||||||
|
const item = dict.data.find((item) => item.isDefault);
|
||||||
|
if (item) {
|
||||||
|
form.cnameProviderId = item.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
title: "状态",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "待设置CNAME", value: "cname", color: "warning" },
|
||||||
|
{ label: "验证中", value: "validating", color: "primary" },
|
||||||
|
{ label: "验证成功", value: "valid", color: "success" },
|
||||||
|
{ label: "验证失败", value: "failed", color: "error" }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
addForm: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 120,
|
||||||
|
align: "center"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
triggerValidate: {
|
||||||
|
title: "验证",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
conditionalRenderDisabled: true,
|
||||||
|
width: 100,
|
||||||
|
align: "center",
|
||||||
|
cellRender({ row, value }) {
|
||||||
|
if (row.status === "valid") {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a-button size={"small"} type={"primary"}>
|
||||||
|
点击验证
|
||||||
|
</a-button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createTime: {
|
||||||
|
title: "创建时间",
|
||||||
|
type: "datetime",
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
sorter: true,
|
||||||
|
width: 160,
|
||||||
|
align: "center"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateTime: {
|
||||||
|
title: "更新时间",
|
||||||
|
type: "datetime",
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
<template>
|
||||||
|
<fs-page class="page-cert">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">CNAME记录管理</div>
|
||||||
|
</template>
|
||||||
|
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||||
|
<template #pagination-left>
|
||||||
|
<a-tooltip title="批量删除">
|
||||||
|
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
</fs-crud>
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
|
import createCrudOptions from "./crud";
|
||||||
|
import { message, Modal } from "ant-design-vue";
|
||||||
|
import { DeleteBatch } from "/@/views/certd/history/api";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "CnameRecord"
|
||||||
|
});
|
||||||
|
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
|
||||||
|
|
||||||
|
const selectedRowKeys = context.selectedRowKeys;
|
||||||
|
const handleBatchDelete = () => {
|
||||||
|
if (selectedRowKeys.value?.length > 0) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认",
|
||||||
|
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
|
||||||
|
async onOk() {
|
||||||
|
await DeleteBatch(selectedRowKeys.value);
|
||||||
|
message.info("删除成功");
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error("请先勾选记录");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面打开后获取列表数据
|
||||||
|
onMounted(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less"></style>
|
|
@ -23,6 +23,10 @@ import { Ref, ref } from "vue";
|
||||||
import { CrudOptions, useColumns, useFormWrapper } from "@fast-crud/fast-crud";
|
import { CrudOptions, useColumns, useFormWrapper } from "@fast-crud/fast-crud";
|
||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "UserProfile"
|
||||||
|
});
|
||||||
|
|
||||||
const userInfo: Ref = ref({});
|
const userInfo: Ref = ref({});
|
||||||
|
|
||||||
const getUserInfo = async () => {
|
const getUserInfo = async () => {
|
||||||
|
|
|
@ -3,10 +3,12 @@ import { PluginGroup } from "@certd/pipeline";
|
||||||
import { useReference } from "/@/use/use-refrence";
|
import { useReference } from "/@/use/use-refrence";
|
||||||
import _ from "lodash-es";
|
import _ from "lodash-es";
|
||||||
import { useUserStore } from "/@/store/modules/user";
|
import { useUserStore } from "/@/store/modules/user";
|
||||||
|
import { useSettingStore } from "/@/store/modules/settings";
|
||||||
|
|
||||||
export default function (certPluginGroup: PluginGroup, formWrapperRef: any): CreateCrudOptionsRet {
|
export default function (certPluginGroup: PluginGroup, formWrapperRef: any): CreateCrudOptionsRet {
|
||||||
const inputs: any = {};
|
const inputs: any = {};
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
for (const plugin of certPluginGroup.plugins) {
|
for (const plugin of certPluginGroup.plugins) {
|
||||||
for (const inputKey in plugin.input) {
|
for (const inputKey in plugin.input) {
|
||||||
if (inputs[inputKey]) {
|
if (inputs[inputKey]) {
|
||||||
|
@ -66,8 +68,8 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
|
||||||
render: () => {
|
render: () => {
|
||||||
return (
|
return (
|
||||||
<ul>
|
<ul>
|
||||||
<li>JS-ACME:如果你的域名DNS属于阿里云、腾讯云、Cloudflare、西部数码可以选择用它来申请</li>
|
<li>JS-ACME:使用简单方便,功能强大【推荐】</li>
|
||||||
<li>Lego-ACME:基于Lego实现,支持海量DNS提供商,熟悉LEGO的用户可以使用</li>
|
<li>Lego-ACME:基于Lego实现,支持海量DNS提供商,熟悉LEGO的用户可以使用【即将废弃】</li>
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,6 +313,9 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
||||||
name: "a-input"
|
name: "a-input"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
form: {
|
||||||
|
rules: [{ required: true, message: "此项必填" }]
|
||||||
|
},
|
||||||
column: {
|
column: {
|
||||||
width: 350,
|
width: 350,
|
||||||
sorter: true,
|
sorter: true,
|
||||||
|
|
|
@ -73,27 +73,28 @@
|
||||||
<a-alert type="info" :message="currentPlugin.title" :description="currentPlugin.desc"> </a-alert>
|
<a-alert type="info" :message="currentPlugin.title" :description="currentPlugin.desc"> </a-alert>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<a-form ref="stepFormRef" class="step-form" :model="currentStep" :label-col="labelCol" :wrapper-col="wrapperCol">
|
<div class="w-100 h-100">
|
||||||
<fs-form-item
|
<a-form ref="stepFormRef" class="step-form" :model="currentStep" :label-col="labelCol" :wrapper-col="wrapperCol">
|
||||||
v-model="currentStep.title"
|
<fs-form-item
|
||||||
:item="{
|
v-model="currentStep.title"
|
||||||
title: '任务名称',
|
:item="{
|
||||||
key: 'title',
|
title: '任务名称',
|
||||||
component: {
|
key: 'title',
|
||||||
name: 'a-input',
|
component: {
|
||||||
vModel: 'value'
|
name: 'a-input',
|
||||||
},
|
vModel: 'value'
|
||||||
rules: [{ required: true, message: '此项必填' }]
|
},
|
||||||
}"
|
rules: [{ required: true, message: '此项必填' }]
|
||||||
:get-context-fn="blankFn"
|
}"
|
||||||
/>
|
:get-context-fn="blankFn"
|
||||||
<template v-for="(item, key) in currentPlugin.input" :key="key">
|
/>
|
||||||
<fs-form-item v-if="item.show !== false" v-model="currentStep.input[key]" :item="item" :get-context-fn="blankFn" />
|
<template v-for="(item, key) in currentPlugin.input" :key="key">
|
||||||
</template>
|
<fs-form-item v-if="item.show !== false" v-model="currentStep.input[key]" :item="item" :get-context-fn="blankFn" />
|
||||||
|
</template>
|
||||||
<fs-form-item v-model="currentStep.strategy.runStrategy" :item="runStrategyProps" :get-context-fn="blankFn" />
|
|
||||||
</a-form>
|
|
||||||
|
|
||||||
|
<fs-form-item v-model="currentStep.strategy.runStrategy" :item="runStrategyProps" :get-context-fn="blankFn" />
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div v-if="editMode" class="bottom-button">
|
<div v-if="editMode" class="bottom-button">
|
||||||
<a-button type="primary" @click="stepSave"> 确定 </a-button>
|
<a-button type="primary" @click="stepSave"> 确定 </a-button>
|
||||||
|
@ -394,17 +395,24 @@ export default {
|
||||||
&.fullscreen {
|
&.fullscreen {
|
||||||
.pi-step-form {
|
.pi-step-form {
|
||||||
.body {
|
.body {
|
||||||
|
margin: auto;
|
||||||
.step-plugin {
|
.step-plugin {
|
||||||
width: 16.666666%;
|
width: 16.666666%;
|
||||||
}
|
}
|
||||||
.step-form {
|
.step-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
width: 1300px;
|
||||||
.fs-form-item {
|
.fs-form-item {
|
||||||
width: 50%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.footer {
|
||||||
|
.bottom-button {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<fs-button class="back" icon="ion:chevron-back-outline" @click="goBack"></fs-button>
|
<fs-button class="back" icon="ion:chevron-back-outline" @click="goBack"></fs-button>
|
||||||
<pi-editable v-model="pipeline.title" :hover-show="false" :disabled="!editMode"></pi-editable>
|
<text-editable v-model="pipeline.title" :hover-show="false" :disabled="!editMode"></text-editable>
|
||||||
</div>
|
</div>
|
||||||
<div class="more">
|
<div class="more">
|
||||||
<template v-if="editMode">
|
<template v-if="editMode">
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="stage first-stage">
|
<div class="stage first-stage">
|
||||||
<div class="title stage-move-handle">
|
<div class="title stage-move-handle">
|
||||||
<pi-editable model-value="触发源" :disabled="true" />
|
<text-editable model-value="触发源" :disabled="true" />
|
||||||
</div>
|
</div>
|
||||||
<div class="tasks">
|
<div class="tasks">
|
||||||
<div class="task-container first-task">
|
<div class="task-container first-task">
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
<template #item="{ element: stage, index }">
|
<template #item="{ element: stage, index }">
|
||||||
<div :key="stage.id" class="stage" :class="{ 'last-stage': isLastStage(index) }">
|
<div :key="stage.id" class="stage" :class="{ 'last-stage': isLastStage(index) }">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable>
|
<text-editable v-model="stage.title" :disabled="!editMode"></text-editable>
|
||||||
<div v-plus class="icon-box stage-move-handle">
|
<div v-plus class="icon-box stage-move-handle">
|
||||||
<fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon>
|
<fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon>
|
||||||
</div>
|
</div>
|
||||||
|
@ -146,7 +146,7 @@
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div v-if="editMode" class="stage last-stage">
|
<div v-if="editMode" class="stage last-stage">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<pi-editable model-value="新阶段" :disabled="true" />
|
<text-editable model-value="新阶段" :disabled="true" />
|
||||||
</div>
|
</div>
|
||||||
<div class="tasks">
|
<div class="tasks">
|
||||||
<div class="task-container first-task">
|
<div class="task-container first-task">
|
||||||
|
@ -188,7 +188,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="stage last-stage">
|
<div v-else class="stage last-stage">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<pi-editable model-value="结束" :disabled="true" />
|
<text-editable model-value="结束" :disabled="true" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="pipeline.notifications?.length > 0" class="tasks">
|
<div v-if="pipeline.notifications?.length > 0" class="tasks">
|
||||||
<div v-for="(item, index) of pipeline.notifications" :key="index" class="task-container" :class="{ 'first-task': index == 0 }">
|
<div v-for="(item, index) of pipeline.notifications" :key="index" class="task-container" :class="{ 'first-task': index == 0 }">
|
||||||
|
@ -704,7 +704,7 @@ export default defineComponent({
|
||||||
.back {
|
.back {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.pi-editable {
|
.text-editable {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,12 +79,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive } from "vue";
|
import { reactive } from "vue";
|
||||||
import * as api from "./api";
|
import * as api from "./api";
|
||||||
import * as emailApi from "./api.email";
|
|
||||||
|
|
||||||
import { SettingKeys } from "./api";
|
import { SettingKeys } from "./api";
|
||||||
|
import * as emailApi from "./api.email";
|
||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
import { useSettingStore } from "/@/store/modules/settings";
|
import { useSettingStore } from "/@/store/modules/settings";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "EmailSetting"
|
||||||
|
});
|
||||||
|
|
||||||
interface FormState {
|
interface FormState {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
|
|
@ -18,6 +18,10 @@ import { useUserStore } from "/@/store/modules/user";
|
||||||
import { useSettingStore } from "/@/store/modules/settings";
|
import { useSettingStore } from "/@/store/modules/settings";
|
||||||
import * as api from "./api";
|
import * as api from "./api";
|
||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "AccountBind"
|
||||||
|
});
|
||||||
const iframeRef = ref();
|
const iframeRef = ref();
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
|
@ -29,7 +29,7 @@ import { usePermission } from "/src/plugin/permission";
|
||||||
import { useFs, useUi } from "@fast-crud/fast-crud";
|
import { useFs, useUi } from "@fast-crud/fast-crud";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "AuthorityPermission",
|
name: "AuthorityManager",
|
||||||
components: { FsPermissionTree },
|
components: { FsPermissionTree },
|
||||||
setup() {
|
setup() {
|
||||||
// 此处传入permission进行通用按钮权限设置,会通过commonOptions去设置actionbar和rowHandle的按钮的show属性
|
// 此处传入permission进行通用按钮权限设置,会通过commonOptions去设置actionbar和rowHandle的按钮的show属性
|
||||||
|
|
|
@ -5,7 +5,15 @@
|
||||||
</template>
|
</template>
|
||||||
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
||||||
<a-modal v-model:open="authzDialogVisible" width="860px" title="分配权限" @ok="updatePermission">
|
<a-modal v-model:open="authzDialogVisible" width="860px" title="分配权限" @ok="updatePermission">
|
||||||
<fs-permission-tree ref="permissionTreeRef" v-model:checkedKeys="checkedKeys" :tree="permissionTreeData" :editable="false" checkable :replace-fields="{ key: 'id', label: 'title' }"> </fs-permission-tree>
|
<fs-permission-tree
|
||||||
|
ref="permissionTreeRef"
|
||||||
|
v-model:checked-keys="checkedKeys"
|
||||||
|
:tree="permissionTreeData"
|
||||||
|
:editable="false"
|
||||||
|
checkable
|
||||||
|
:replace-fields="{ key: 'id', label: 'title' }"
|
||||||
|
>
|
||||||
|
</fs-permission-tree>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</fs-page>
|
</fs-page>
|
||||||
</template>
|
</template>
|
||||||
|
@ -84,7 +92,7 @@ function useAuthz() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "AuthorityRole",
|
name: "RoleManager",
|
||||||
components: { FsPermissionTree },
|
components: { FsPermissionTree },
|
||||||
setup() {
|
setup() {
|
||||||
//授权配置
|
//授权配置
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { defineComponent, ref, onMounted } from "vue";
|
||||||
import { useCrud, useExpose, useFs } from "@fast-crud/fast-crud";
|
import { useCrud, useExpose, useFs } from "@fast-crud/fast-crud";
|
||||||
import createCrudOptions from "./crud";
|
import createCrudOptions from "./crud";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "AuthorityUser",
|
name: "UserManager",
|
||||||
setup() {
|
setup() {
|
||||||
// 初始化crud配置
|
// 初始化crud配置
|
||||||
// 此处传入权限前缀进行通用按钮权限设置,会通过commonOptions去设置actionbar和rowHandle的按钮的show属性
|
// 此处传入权限前缀进行通用按钮权限设置,会通过commonOptions去设置actionbar和rowHandle的按钮的show属性
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { request } from "/src/api/service";
|
||||||
|
|
||||||
|
const apiPrefix = "/sys/cname/provider";
|
||||||
|
|
||||||
|
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 + "/deleteByIds",
|
||||||
|
method: "post",
|
||||||
|
data: { ids }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SetDefault(id: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/setDefault",
|
||||||
|
method: "post",
|
||||||
|
data: { id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SetDisabled(id: any, disabled: boolean) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/setDisabled",
|
||||||
|
method: "post",
|
||||||
|
data: { id, disabled }
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,245 @@
|
||||||
|
import * as api from "./api";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { computed, Ref, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
|
||||||
|
import { useUserStore } from "/@/store/modules/user";
|
||||||
|
import { useSettingStore } from "/@/store/modules/settings";
|
||||||
|
import { Modal } from "ant-design-vue";
|
||||||
|
|
||||||
|
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
|
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) => {
|
||||||
|
form.content = JSON.stringify({
|
||||||
|
title: form.title
|
||||||
|
});
|
||||||
|
const res = await api.AddObj(form);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const selectedRowKeys: Ref<any[]> = 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
|
||||||
|
},
|
||||||
|
rowHandle: {
|
||||||
|
minWidth: 200,
|
||||||
|
fixed: "right"
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
id: {
|
||||||
|
title: "ID",
|
||||||
|
key: "id",
|
||||||
|
type: "number",
|
||||||
|
column: {
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
title: "域名",
|
||||||
|
type: "text",
|
||||||
|
editForm: {
|
||||||
|
component: {
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
helper: "CNAME域名一旦确定不可修改",
|
||||||
|
rules: [{ required: true, message: "此项必填" }]
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dnsProviderType: {
|
||||||
|
title: "DNS提供商",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: dict({
|
||||||
|
url: "pi/dnsProvider/list",
|
||||||
|
value: "key",
|
||||||
|
label: "title"
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
rules: [{ required: true, message: "此项必填" }]
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 150,
|
||||||
|
component: {
|
||||||
|
color: "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accessId: {
|
||||||
|
title: "DNS提供商授权",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: dict({
|
||||||
|
url: "/pi/access/list",
|
||||||
|
value: "id",
|
||||||
|
label: "name"
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
component: {
|
||||||
|
name: "access-selector",
|
||||||
|
vModel: "modelValue",
|
||||||
|
type: compute(({ form }) => {
|
||||||
|
return form.dnsProviderType;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: "此项必填" }]
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 150,
|
||||||
|
component: {
|
||||||
|
color: "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isDefault: {
|
||||||
|
title: "是否默认",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "是", value: true, color: "success" },
|
||||||
|
{ label: "否", value: false, color: "default" }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
value: false,
|
||||||
|
rules: [{ required: true, message: "请选择是否默认" }]
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
align: "center",
|
||||||
|
width: 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setDefault: {
|
||||||
|
title: "设置默认",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 100,
|
||||||
|
align: "center",
|
||||||
|
conditionalRenderDisabled: true,
|
||||||
|
cellRender: ({ row }) => {
|
||||||
|
if (row.isDefault) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onClick = async () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "提示",
|
||||||
|
content: `确定要设置为默认吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
await api.SetDefault(row.id);
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a-button type={"link"} size={"small"} onClick={onClick}>
|
||||||
|
设为默认
|
||||||
|
</a-button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
title: "禁用/启用",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "启用", value: false, color: "success" },
|
||||||
|
{ label: "禁用", value: true, color: "error" }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 100,
|
||||||
|
component: {
|
||||||
|
title: "点击可禁用/启用",
|
||||||
|
on: {
|
||||||
|
async click({ value, row }) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "提示",
|
||||||
|
content: `确定要${!value ? "禁用" : "启用"}吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
await api.SetDisabled(row.id, !value);
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createTime: {
|
||||||
|
title: "创建时间",
|
||||||
|
type: "datetime",
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
sorter: true,
|
||||||
|
width: 160,
|
||||||
|
align: "center"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateTime: {
|
||||||
|
title: "更新时间",
|
||||||
|
type: "datetime",
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
show: true,
|
||||||
|
width: 160
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<fs-page class="page-cert">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">
|
||||||
|
CNAME服务配置
|
||||||
|
<span class="sub">
|
||||||
|
此处配置的域名作为其他域名校验的代理,当别的域名需要申请证书时,通过CNAME映射到此域名上来验证所有权。好处是任何域名都可以通过此方式申请证书,也无需填写AccessSecret。
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||||
|
<template #pagination-left>
|
||||||
|
<a-tooltip title="批量删除">
|
||||||
|
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
</fs-crud>
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
|
import createCrudOptions from "./crud";
|
||||||
|
import { message, Modal } from "ant-design-vue";
|
||||||
|
import { DeleteBatch } from "/@/views/certd/history/api";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "CnameProvider"
|
||||||
|
});
|
||||||
|
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
|
||||||
|
|
||||||
|
const selectedRowKeys = context.selectedRowKeys;
|
||||||
|
const handleBatchDelete = () => {
|
||||||
|
if (selectedRowKeys.value?.length > 0) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认",
|
||||||
|
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
|
||||||
|
async onOk() {
|
||||||
|
await DeleteBatch(selectedRowKeys.value);
|
||||||
|
message.info("删除成功");
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error("请先勾选记录");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面打开后获取列表数据
|
||||||
|
onMounted(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less"></style>
|
|
@ -47,6 +47,10 @@ import { PublicSettingsSave, SettingKeys } from "./api";
|
||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
import { useSettingStore } from "/@/store/modules/settings";
|
import { useSettingStore } from "/@/store/modules/settings";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SysSettings"
|
||||||
|
});
|
||||||
|
|
||||||
interface FormState {
|
interface FormState {
|
||||||
registerEnabled: boolean;
|
registerEnabled: boolean;
|
||||||
managerOtherUserPipeline: boolean;
|
managerOtherUserPipeline: boolean;
|
||||||
|
|
|
@ -68,6 +68,10 @@ import { notification } from "ant-design-vue";
|
||||||
import { useSettingStore } from "/src/store/modules/settings";
|
import { useSettingStore } from "/src/store/modules/settings";
|
||||||
import { useUserStore } from "/@/store/modules/user";
|
import { useUserStore } from "/@/store/modules/user";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SiteSetting"
|
||||||
|
});
|
||||||
|
|
||||||
interface FormState {
|
interface FormState {
|
||||||
title: string;
|
title: string;
|
||||||
slogan: string;
|
slogan: string;
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
CREATE TABLE "cd_cname_provider"
|
||||||
|
(
|
||||||
|
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"domain" varchar(100) NOT NULL,
|
||||||
|
"dns_provider_type" varchar(100) NOT NULL,
|
||||||
|
"access_id" integer NOT NULL,
|
||||||
|
"is_default" boolean NOT NULL,
|
||||||
|
"remark" varchar(200),
|
||||||
|
"disabled" boolean NOT NULL,
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE "cd_cname_record"
|
||||||
|
(
|
||||||
|
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"domain" varchar(100) NOT NULL,
|
||||||
|
"host_record" varchar(100) NOT NULL,
|
||||||
|
"record_value" varchar(200) NOT NULL,
|
||||||
|
"cname_provider_id" integer NOT NULL,
|
||||||
|
"status" varchar(100) NOT NULL,
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
|
|
@ -70,6 +70,7 @@
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
|
"psl": "^1.9.0",
|
||||||
"qiniu": "^7.12.0",
|
"qiniu": "^7.12.0",
|
||||||
"querystring": "^0.2.1",
|
"querystring": "^0.2.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { MidwayConfig } from '@midwayjs/core';
|
||||||
|
|
||||||
// eslint-disable-next-line node/no-extraneous-import
|
// eslint-disable-next-line node/no-extraneous-import
|
||||||
import { FlywayHistory } from '@certd/midway-flyway-js';
|
import { FlywayHistory } from '@certd/midway-flyway-js';
|
||||||
import { UserEntity } from '../modules/authority/entity/user.js';
|
import { UserEntity } from '../modules/sys/authority/entity/user.js';
|
||||||
import { PipelineEntity } from '../modules/pipeline/entity/pipeline.js';
|
import { PipelineEntity } from '../modules/pipeline/entity/pipeline.js';
|
||||||
//import { logger } from '../utils/logger';
|
//import { logger } from '../utils/logger';
|
||||||
// load .env file in process.cwd
|
// load .env file in process.cwd
|
||||||
|
@ -67,7 +67,7 @@ const development = {
|
||||||
logging: false,
|
logging: false,
|
||||||
|
|
||||||
// 配置实体模型 或者 entities: '/entity',
|
// 配置实体模型 或者 entities: '/entity',
|
||||||
entities: ['**/modules/*/entity/*.js', ...libServerEntities, ...commercialEntities, PipelineEntity, FlywayHistory, UserEntity],
|
entities: ['**/modules/**/entity/*.js', ...libServerEntities, ...commercialEntities, PipelineEntity, FlywayHistory, UserEntity],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { Constants } from '@certd/lib-server';
|
import { Constants } from '@certd/lib-server';
|
||||||
import { logger } from '@certd/pipeline';
|
import { logger } from '@certd/pipeline';
|
||||||
import { AuthService } from '../modules/authority/service/auth-service.js';
|
import { AuthService } from '../modules/sys/authority/service/auth-service.js';
|
||||||
import { SysSettingsService } from '@certd/lib-server';
|
import { SysSettingsService } from '@certd/lib-server';
|
||||||
import { SysPrivateSettings } from '@certd/lib-server';
|
import { SysPrivateSettings } from '@certd/lib-server';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Autoload, Config, Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
import { Autoload, Config, Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
|
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
|
||||||
import { CommonException } from '@certd/lib-server';
|
import { CommonException } from '@certd/lib-server';
|
||||||
import { UserService } from '../../modules/authority/service/user-service.js';
|
import { UserService } from '../../modules/sys/authority/service/user-service.js';
|
||||||
import { logger } from '@certd/pipeline';
|
import { logger } from '@certd/pipeline';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
|
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
import { logger } from '@certd/pipeline';
|
import { logger } from '@certd/pipeline';
|
||||||
import { UserService } from '../authority/service/user-service.js';
|
import { UserService } from '../sys/authority/service/user-service.js';
|
||||||
import { PlusService, SysSettingsService } from '@certd/lib-server';
|
import { PlusService, SysSettingsService } from '@certd/lib-server';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { SysInstallInfo, SysPrivateSettings } from '@certd/lib-server';
|
import { SysInstallInfo, SysPrivateSettings } from '@certd/lib-server';
|
||||||
|
|
|
@ -13,7 +13,7 @@ export class FileController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
fileService: FileService;
|
fileService: FileService;
|
||||||
|
|
||||||
@Post('/upload', { summary: 'sys:settings:view' })
|
@Post('/upload', { summary: Constants.per.authOnly })
|
||||||
async upload(@Files() files: UploadFileInfo<string>[], @Fields() fields: any) {
|
async upload(@Files() files: UploadFileInfo<string>[], @Fields() fields: any) {
|
||||||
console.log('files', files, fields);
|
console.log('files', files, fields);
|
||||||
const cacheKey = uploadTmpFileCacheKey + nanoid();
|
const cacheKey = uploadTmpFileCacheKey + nanoid();
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
||||||
|
import { BaseController, Constants } from '@certd/lib-server';
|
||||||
|
import { CnameRecordService } from '../service/cname-record-service.js';
|
||||||
|
import { CnameProviderService } from '../../sys/cname/service/cname-provider-service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权
|
||||||
|
*/
|
||||||
|
@Provide()
|
||||||
|
@Controller('/api/cname/provider')
|
||||||
|
export class CnameProviderController extends BaseController {
|
||||||
|
@Inject()
|
||||||
|
service: CnameRecordService;
|
||||||
|
@Inject()
|
||||||
|
providerService: CnameProviderService;
|
||||||
|
|
||||||
|
getService(): CnameRecordService {
|
||||||
|
return this.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/list', { summary: Constants.per.authOnly })
|
||||||
|
async list(@Body(ALL) body: any) {
|
||||||
|
body.userId = this.ctx.user.id;
|
||||||
|
const res = await this.providerService.find({});
|
||||||
|
return this.ok(res);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
||||||
|
import { Constants, CrudController } from '@certd/lib-server';
|
||||||
|
import { CnameRecordService } from '../service/cname-record-service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权
|
||||||
|
*/
|
||||||
|
@Provide()
|
||||||
|
@Controller('/api/cname/record')
|
||||||
|
export class CnameRecordController extends CrudController<CnameRecordService> {
|
||||||
|
@Inject()
|
||||||
|
service: CnameRecordService;
|
||||||
|
|
||||||
|
getService(): CnameRecordService {
|
||||||
|
return this.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/page', { summary: Constants.per.authOnly })
|
||||||
|
async page(@Body(ALL) body: any) {
|
||||||
|
body.query = body.query ?? {};
|
||||||
|
body.query.userId = this.ctx.user.id;
|
||||||
|
return await super.page(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/list', { summary: Constants.per.authOnly })
|
||||||
|
async list(@Body(ALL) body: any) {
|
||||||
|
body.userId = this.ctx.user.id;
|
||||||
|
return super.list(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/add', { summary: Constants.per.authOnly })
|
||||||
|
async add(@Body(ALL) bean: any) {
|
||||||
|
bean.userId = this.ctx.user.id;
|
||||||
|
return super.add(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/update', { summary: Constants.per.authOnly })
|
||||||
|
async update(@Body(ALL) bean: any) {
|
||||||
|
await this.service.checkUserId(bean.id, this.ctx.user.id);
|
||||||
|
return super.update(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/info', { summary: Constants.per.authOnly })
|
||||||
|
async info(@Query('id') id: number) {
|
||||||
|
await this.service.checkUserId(id, this.ctx.user.id);
|
||||||
|
return super.info(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/delete', { summary: Constants.per.authOnly })
|
||||||
|
async delete(@Query('id') id: number) {
|
||||||
|
await this.service.checkUserId(id, this.ctx.user.id);
|
||||||
|
return super.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/getByDomain', { summary: Constants.per.authOnly })
|
||||||
|
async getByDomain(@Body(ALL) body: { domain: string; createOnNotFound: boolean }) {
|
||||||
|
const userId = this.ctx.user.id;
|
||||||
|
const res = await this.service.getByDomain(body.domain, userId, body.createOnNotFound);
|
||||||
|
return this.ok(res);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cname record配置
|
||||||
|
*/
|
||||||
|
@Entity('cd_cname_record')
|
||||||
|
export class CnameRecordEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ comment: '用户ID', name: 'user_id' })
|
||||||
|
userId: number;
|
||||||
|
|
||||||
|
@Column({ comment: '证书申请域名', length: 100 })
|
||||||
|
domain: string;
|
||||||
|
|
||||||
|
@Column({ comment: '主机记录', name: 'host_record', length: 100 })
|
||||||
|
hostRecord: string;
|
||||||
|
|
||||||
|
@Column({ comment: '记录值', name: 'record_value', length: 200 })
|
||||||
|
recordValue: string;
|
||||||
|
|
||||||
|
@Column({ comment: 'CNAME提供者', name: 'cname_provider_id' })
|
||||||
|
cnameProviderId: number;
|
||||||
|
|
||||||
|
@Column({ comment: '验证状态', length: 20 })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
comment: '创建时间',
|
||||||
|
name: 'create_time',
|
||||||
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
|
})
|
||||||
|
createTime: Date;
|
||||||
|
@Column({
|
||||||
|
comment: '修改时间',
|
||||||
|
name: 'update_time',
|
||||||
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
|
})
|
||||||
|
updateTime: Date;
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { BaseService, ValidateException } from '@certd/lib-server';
|
||||||
|
import { CnameRecordEntity } from '../entity/cname-record.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { CnameProviderService } from '../../sys/cname/service/cname-provider-service.js';
|
||||||
|
import { CnameProviderEntity } from '../../sys/cname/entity/cname_provider.js';
|
||||||
|
import { parseDomain } from '@certd/plugin-cert';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权
|
||||||
|
*/
|
||||||
|
@Provide()
|
||||||
|
@Scope(ScopeEnum.Singleton)
|
||||||
|
export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||||
|
@InjectEntityModel(CnameRecordEntity)
|
||||||
|
repository: Repository<CnameRecordEntity>;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
cnameProviderService: CnameProviderService;
|
||||||
|
getRepository() {
|
||||||
|
return this.repository;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 新增
|
||||||
|
* @param param 数据
|
||||||
|
*/
|
||||||
|
async add(param: any): Promise<CnameRecordEntity> {
|
||||||
|
if (!param.domain) {
|
||||||
|
throw new ValidateException('域名不能为空');
|
||||||
|
}
|
||||||
|
if (!param.userId) {
|
||||||
|
throw new ValidateException('userId不能为空');
|
||||||
|
}
|
||||||
|
if (param.domain.startsWith('*.')) {
|
||||||
|
param.domain = param.domain.substring(2);
|
||||||
|
}
|
||||||
|
const info = await this.getRepository().findOne({ where: { domain: param.domain } });
|
||||||
|
if (info) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cnameProvider: CnameProviderEntity = null;
|
||||||
|
if (!param.cnameProviderId) {
|
||||||
|
//获取默认的cnameProviderId
|
||||||
|
cnameProvider = await this.cnameProviderService.getByPriority();
|
||||||
|
if (cnameProvider == null) {
|
||||||
|
throw new ValidateException('找不到CNAME服务,请先联系管理员添加CNAME服务');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
|
||||||
|
}
|
||||||
|
this.cnameProviderChanged(param, cnameProvider);
|
||||||
|
|
||||||
|
param.status = 'cname';
|
||||||
|
const { id } = await super.add(param);
|
||||||
|
return await this.info(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cnameProviderChanged(param: any, cnameProvider: CnameProviderEntity) {
|
||||||
|
param.cnameProviderId = cnameProvider.id;
|
||||||
|
|
||||||
|
const realDomain = parseDomain(param.domain);
|
||||||
|
const prefix = param.domain.replace(realDomain, '');
|
||||||
|
let hostRecord = `_acme-challenge.${prefix}`;
|
||||||
|
if (hostRecord.endsWith('.')) {
|
||||||
|
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
|
||||||
|
}
|
||||||
|
param.hostRecord = hostRecord;
|
||||||
|
|
||||||
|
const cnameKey = uuidv4().replaceAll('-', '');
|
||||||
|
param.recordValue = `${cnameKey}.${cnameProvider.domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(param: any) {
|
||||||
|
if (!param.id) {
|
||||||
|
throw new ValidateException('id不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
const old = await this.info(param.id);
|
||||||
|
if (!old) {
|
||||||
|
throw new ValidateException('数据不存在');
|
||||||
|
}
|
||||||
|
if (old.domain !== param.domain) {
|
||||||
|
throw new ValidateException('域名不允许修改');
|
||||||
|
}
|
||||||
|
if (old.cnameProviderId !== param.cnameProviderId) {
|
||||||
|
const cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
|
||||||
|
this.cnameProviderChanged(param, cnameProvider);
|
||||||
|
param.status = 'cname';
|
||||||
|
}
|
||||||
|
return await super.update(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(id: number) {
|
||||||
|
const info = await this.info(id);
|
||||||
|
if (info.status === 'success') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//开始校验
|
||||||
|
// 1. dnsProvider
|
||||||
|
// 2. 添加txt记录
|
||||||
|
// 3. 检查原域名是否有cname记录
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByDomain(domain: string, userId: number, createOnNotFound = true) {
|
||||||
|
if (!domain) {
|
||||||
|
throw new ValidateException('domain不能为空');
|
||||||
|
}
|
||||||
|
if (userId == null) {
|
||||||
|
throw new ValidateException('userId不能为空');
|
||||||
|
}
|
||||||
|
let record = await this.getRepository().findOne({ where: { domain, userId } });
|
||||||
|
if (record == null) {
|
||||||
|
if (createOnNotFound) {
|
||||||
|
record = await this.add({ domain, userId });
|
||||||
|
} else {
|
||||||
|
throw new ValidateException(`找不到${domain}的CNAME记录`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const provider = await this.cnameProviderService.info(record.cnameProviderId);
|
||||||
|
if (provider == null) {
|
||||||
|
throw new ValidateException(`找不到${domain}的CNAME服务`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
cnameProvider: provider,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
||||||
import { BaseController } from '@certd/lib-server';
|
import { BaseController } from '@certd/lib-server';
|
||||||
import { Constants } from '@certd/lib-server';
|
import { Constants } from '@certd/lib-server';
|
||||||
import { UserService } from '../../authority/service/user-service.js';
|
import { UserService } from '../../sys/authority/service/user-service.js';
|
||||||
import { UserEntity } from '../../authority/entity/user.js';
|
import { UserEntity } from '../../sys/authority/entity/user.js';
|
||||||
import { SysSettingsService } from '@certd/lib-server';
|
import { SysSettingsService } from '@certd/lib-server';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Config, Inject, Provide } from '@midwayjs/core';
|
import { Config, Inject, Provide } from '@midwayjs/core';
|
||||||
import { UserService } from '../../authority/service/user-service.js';
|
import { UserService } from '../../sys/authority/service/user-service.js';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { CommonException } from '@certd/lib-server';
|
import { CommonException } from '@certd/lib-server';
|
||||||
import { RoleService } from '../../authority/service/role-service.js';
|
import { RoleService } from '../../sys/authority/service/role-service.js';
|
||||||
import { UserEntity } from '../../authority/entity/user.js';
|
import { UserEntity } from '../../sys/authority/entity/user.js';
|
||||||
import { SysSettingsService } from '@certd/lib-server';
|
import { SysSettingsService } from '@certd/lib-server';
|
||||||
import { SysPrivateSettings } from '@certd/lib-server';
|
import { SysPrivateSettings } from '@certd/lib-server';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
||||||
import { BaseController, Constants } from '@certd/lib-server';
|
import { BaseController, Constants } from '@certd/lib-server';
|
||||||
import { UserService } from '../../authority/service/user-service.js';
|
import { UserService } from '../../sys/authority/service/user-service.js';
|
||||||
import { RoleService } from '../../authority/service/role-service.js';
|
import { RoleService } from '../../sys/authority/service/role-service.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
import {
|
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
||||||
ALL,
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Inject,
|
|
||||||
Post,
|
|
||||||
Provide,
|
|
||||||
Query,
|
|
||||||
} from '@midwayjs/core';
|
|
||||||
import { CrudController } from '@certd/lib-server';
|
import { CrudController } from '@certd/lib-server';
|
||||||
import { Constants } from '@certd/lib-server';
|
import { Constants } from '@certd/lib-server';
|
||||||
import { UserSettingsService } from '../service/user-settings-service.js';
|
import { UserSettingsService } from '../service/user-settings-service.js';
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { HistoryLogEntity } from '../entity/history-log.js';
|
||||||
import { PipelineService } from '../service/pipeline-service.js';
|
import { PipelineService } from '../service/pipeline-service.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { logger } from '@certd/pipeline';
|
import { logger } from '@certd/pipeline';
|
||||||
import { AuthService } from '../../authority/service/auth-service.js';
|
import { AuthService } from '../../sys/authority/service/auth-service.js';
|
||||||
import { SysSettingsService } from '@certd/lib-server';
|
import { SysSettingsService } from '@certd/lib-server';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { PipelineService } from '../service/pipeline-service.js';
|
||||||
import { PipelineEntity } from '../entity/pipeline.js';
|
import { PipelineEntity } from '../entity/pipeline.js';
|
||||||
import { Constants } from '@certd/lib-server';
|
import { Constants } from '@certd/lib-server';
|
||||||
import { HistoryService } from '../service/history-service.js';
|
import { HistoryService } from '../service/history-service.js';
|
||||||
import { AuthService } from '../../authority/service/auth-service.js';
|
import { AuthService } from '../../sys/authority/service/auth-service.js';
|
||||||
import { SysSettingsService } from '@certd/lib-server';
|
import { SysSettingsService } from '@certd/lib-server';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { IAccessService } from '@certd/pipeline';
|
||||||
|
|
||||||
|
export class AccessGetter implements IAccessService {
|
||||||
|
userId: number;
|
||||||
|
getter: <T>(id: any, userId?: number) => Promise<T>;
|
||||||
|
constructor(userId: number, getter: (id: any, userId: number) => Promise<any>) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.getter = getter;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById<T = any>(id: any) {
|
||||||
|
return await this.getter<T>(id, this.userId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,17 @@
|
||||||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { BaseService } from '@certd/lib-server';
|
import { BaseService, PermissionException, ValidateException } from '@certd/lib-server';
|
||||||
import { AccessEntity } from '../entity/access.js';
|
import { AccessEntity } from '../entity/access.js';
|
||||||
import { AccessDefine, accessRegistry, IAccessService, newAccess } from '@certd/pipeline';
|
import { AccessDefine, accessRegistry, newAccess } from '@certd/pipeline';
|
||||||
import { EncryptService } from './encrypt-service.js';
|
import { EncryptService } from './encrypt-service.js';
|
||||||
import { ValidateException } from '@certd/lib-server';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 授权
|
* 授权
|
||||||
*/
|
*/
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Singleton)
|
@Scope(ScopeEnum.Singleton)
|
||||||
export class AccessService extends BaseService<AccessEntity> implements IAccessService {
|
export class AccessService extends BaseService<AccessEntity> {
|
||||||
@InjectEntityModel(AccessEntity)
|
@InjectEntityModel(AccessEntity)
|
||||||
repository: Repository<AccessEntity>;
|
repository: Repository<AccessEntity>;
|
||||||
|
|
||||||
|
@ -102,11 +101,14 @@ export class AccessService extends BaseService<AccessEntity> implements IAccessS
|
||||||
return await super.update(param);
|
return await super.update(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(id: any): Promise<any> {
|
async getById(id: any, userId?: number): Promise<any> {
|
||||||
const entity = await this.info(id);
|
const entity = await this.info(id);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
|
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
|
||||||
}
|
}
|
||||||
|
if (userId !== entity.userId) {
|
||||||
|
throw new PermissionException('您对该授权无访问权限');
|
||||||
|
}
|
||||||
// const access = accessRegistry.get(entity.type);
|
// const access = accessRegistry.get(entity.type);
|
||||||
const setting = this.decryptAccessEntity(entity);
|
const setting = this.decryptAccessEntity(entity);
|
||||||
const input = {
|
const input = {
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { CnameRecord, ICnameProxyService } from '@certd/pipeline';
|
||||||
|
|
||||||
|
export class CnameProxyService implements ICnameProxyService {
|
||||||
|
userId: number;
|
||||||
|
getter: <T>(domain: string, userId?: number) => Promise<T>;
|
||||||
|
constructor(userId: number, getter: (domain: string, userId: number) => Promise<any>) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.getter = getter;
|
||||||
|
}
|
||||||
|
|
||||||
|
getByDomain(domain: string): Promise<CnameRecord> {
|
||||||
|
return this.getter<CnameRecord>(domain, this.userId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,10 @@ import { HistoryLogService } from './history-log-service.js';
|
||||||
import { logger } from '@certd/pipeline';
|
import { logger } from '@certd/pipeline';
|
||||||
import { EmailService } from '../../basic/service/email-service.js';
|
import { EmailService } from '../../basic/service/email-service.js';
|
||||||
import { NeedVIPException } from '@certd/lib-server';
|
import { NeedVIPException } from '@certd/lib-server';
|
||||||
import { UserService } from '../../authority/service/user-service.js';
|
import { UserService } from '../../sys/authority/service/user-service.js';
|
||||||
|
import { AccessGetter } from './access-getter.js';
|
||||||
|
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
|
||||||
|
import { CnameProxyService } from './cname-proxy-service.js';
|
||||||
|
|
||||||
const runningTasks: Map<string | number, Executor> = new Map();
|
const runningTasks: Map<string | number, Executor> = new Map();
|
||||||
const freeCount = 10;
|
const freeCount = 10;
|
||||||
|
@ -34,6 +37,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||||
@Inject()
|
@Inject()
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
@Inject()
|
@Inject()
|
||||||
|
cnameRecordService: CnameRecordService;
|
||||||
|
@Inject()
|
||||||
storageService: StorageService;
|
storageService: StorageService;
|
||||||
@Inject()
|
@Inject()
|
||||||
historyService: HistoryService;
|
historyService: HistoryService;
|
||||||
|
@ -341,11 +346,14 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||||
id: userId,
|
id: userId,
|
||||||
role: userIsAdmin ? 'admin' : 'user',
|
role: userIsAdmin ? 'admin' : 'user',
|
||||||
};
|
};
|
||||||
|
const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService));
|
||||||
|
const cnameProxyService = new CnameProxyService(userId, this.cnameRecordService.getByDomain.bind(this.cnameRecordService));
|
||||||
const executor = new Executor({
|
const executor = new Executor({
|
||||||
user,
|
user,
|
||||||
pipeline,
|
pipeline,
|
||||||
onChanged,
|
onChanged,
|
||||||
accessService: this.accessService,
|
accessService: accessGetter,
|
||||||
|
cnameProxyService,
|
||||||
storage: new DbStorage(userId, this.storageService),
|
storage: new DbStorage(userId, this.storageService),
|
||||||
emailService: this.emailService,
|
emailService: this.emailService,
|
||||||
fileRootDir: this.certdConfig.fileRootDir,
|
fileRootDir: this.certdConfig.fileRootDir,
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { UserRoleEntity } from '../entity/user-role.js';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { SysSettingsService } from '@certd/lib-server';
|
import { SysSettingsService } from '@certd/lib-server';
|
||||||
import { SysInstallInfo } from '@certd/lib-server';
|
import { SysInstallInfo } from '@certd/lib-server';
|
||||||
import { RandomUtil } from '../../../utils/random.js';
|
import { RandomUtil } from '../../../../utils/random.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统用户
|
* 系统用户
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
||||||
|
import { CrudController } from '@certd/lib-server';
|
||||||
|
import { CnameProviderService } from '../service/cname-provider-service.js';
|
||||||
|
import { merge } from 'lodash-es';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权
|
||||||
|
*/
|
||||||
|
@Provide()
|
||||||
|
@Controller('/api/sys/cname/provider')
|
||||||
|
export class CnameRecordController extends CrudController<CnameProviderService> {
|
||||||
|
@Inject()
|
||||||
|
service: CnameProviderService;
|
||||||
|
|
||||||
|
getService(): CnameProviderService {
|
||||||
|
return this.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/page', { summary: 'sys:settings:view' })
|
||||||
|
async page(@Body(ALL) body: any) {
|
||||||
|
body.query = body.query ?? {};
|
||||||
|
return await super.page(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/list', { summary: 'sys:settings:view' })
|
||||||
|
async list(@Body(ALL) body: any) {
|
||||||
|
return super.list(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/add', { summary: 'sys:settings:edit' })
|
||||||
|
async add(@Body(ALL) bean: any) {
|
||||||
|
const def: any = {
|
||||||
|
isDefault: false,
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
merge(bean, def);
|
||||||
|
return super.add(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/update', { summary: 'sys:settings:edit' })
|
||||||
|
async update(@Body(ALL) bean: any) {
|
||||||
|
return super.update(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/info', { summary: 'sys:settings:view' })
|
||||||
|
async info(@Query('id') id: number) {
|
||||||
|
return super.info(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/delete', { summary: 'sys:settings:edit' })
|
||||||
|
async delete(@Query('id') id: number) {
|
||||||
|
return super.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/setDefault', { summary: 'sys:settings:edit' })
|
||||||
|
async setDefault(@Body('id') id: number) {
|
||||||
|
await this.service.setDefault(id);
|
||||||
|
return this.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/setDisabled', { summary: 'sys:settings:edit' })
|
||||||
|
async setDisabled(@Body('id') id: number, @Body('disabled') disabled: boolean) {
|
||||||
|
await this.service.setDisabled(id, disabled);
|
||||||
|
return this.ok();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cname配置
|
||||||
|
*/
|
||||||
|
@Entity('cd_cname_provider')
|
||||||
|
export class CnameProviderEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
@Column({ comment: '域名', length: 100 })
|
||||||
|
domain: string;
|
||||||
|
@Column({ comment: 'DNS提供商类型', name: 'dns_provider_type', length: 20 })
|
||||||
|
dnsProviderType: string;
|
||||||
|
@Column({ comment: 'DNS授权Id', name: 'access_id' })
|
||||||
|
accessId: number;
|
||||||
|
@Column({ comment: '是否默认', name: 'is_default' })
|
||||||
|
isDefault: boolean;
|
||||||
|
@Column({ comment: '是否禁用', name: 'disabled' })
|
||||||
|
disabled: boolean;
|
||||||
|
@Column({ comment: '备注', length: 200 })
|
||||||
|
remark: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
comment: '创建时间',
|
||||||
|
name: 'create_time',
|
||||||
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
|
})
|
||||||
|
createTime: Date;
|
||||||
|
@Column({
|
||||||
|
comment: '修改时间',
|
||||||
|
name: 'update_time',
|
||||||
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
|
})
|
||||||
|
updateTime: Date;
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { BaseService, ValidateException } from '@certd/lib-server';
|
||||||
|
import { CnameProviderEntity } from '../entity/cname_provider.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权
|
||||||
|
*/
|
||||||
|
@Provide()
|
||||||
|
@Scope(ScopeEnum.Singleton)
|
||||||
|
export class CnameProviderService extends BaseService<CnameProviderEntity> {
|
||||||
|
@InjectEntityModel(CnameProviderEntity)
|
||||||
|
repository: Repository<CnameProviderEntity>;
|
||||||
|
|
||||||
|
getRepository() {
|
||||||
|
return this.repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDefault() {
|
||||||
|
return await this.repository.findOne({ where: { isDefault: true } });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 新增
|
||||||
|
* @param param 数据
|
||||||
|
*/
|
||||||
|
async add(param: any) {
|
||||||
|
const def = await this.getDefault();
|
||||||
|
if (!def) {
|
||||||
|
param.isDefault = true;
|
||||||
|
}
|
||||||
|
const res = await super.add(param);
|
||||||
|
if (param.isDefault) {
|
||||||
|
await this.setDefault(res.id);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改
|
||||||
|
* @param param 数据
|
||||||
|
*/
|
||||||
|
async update(param: any) {
|
||||||
|
await super.update(param);
|
||||||
|
if (param.isDefault) {
|
||||||
|
await this.setDefault(param.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(ids: any) {
|
||||||
|
if (!(ids instanceof Array)) {
|
||||||
|
ids = [ids];
|
||||||
|
}
|
||||||
|
for (const id of ids) {
|
||||||
|
const info = await this.info(id);
|
||||||
|
if (info.isDefault) {
|
||||||
|
throw new ValidateException('默认的CNAME服务不能删除,请先修改为非默认值');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await super.delete(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDefault(id: number) {
|
||||||
|
await this.transaction(async em => {
|
||||||
|
await em.getRepository(CnameProviderEntity).update({ isDefault: true }, { isDefault: false });
|
||||||
|
await em.getRepository(CnameProviderEntity).update({ id }, { isDefault: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDisabled(id: number, disabled: boolean) {
|
||||||
|
await this.repository.update({ id }, { disabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByPriority() {
|
||||||
|
const def = await this.getDefault();
|
||||||
|
if (def) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
const found = await this.repository.findOne({ order: { createTime: 'DESC' } });
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import { SysSettingsService } from '@certd/lib-server';
|
||||||
import { SysSettingsEntity } from '../entity/sys-settings.js';
|
import { SysSettingsEntity } from '../entity/sys-settings.js';
|
||||||
import { SysPublicSettings } from '@certd/lib-server';
|
import { SysPublicSettings } from '@certd/lib-server';
|
||||||
import * as _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import { PipelineService } from '../../pipeline/service/pipeline-service.js';
|
import { PipelineService } from '../../../pipeline/service/pipeline-service.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
|
@ -17,13 +17,13 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
|
||||||
async onInstance() {
|
async onInstance() {
|
||||||
const access: any = this.access;
|
const access: any = this.access;
|
||||||
|
|
||||||
this.client = new AliyunClient({logger:this.logger})
|
this.client = new AliyunClient({ logger: this.logger });
|
||||||
await this.client.init({
|
await this.client.init({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
endpoint: 'https://alidns.aliyuncs.com',
|
endpoint: 'https://alidns.aliyuncs.com',
|
||||||
apiVersion: '2015-01-09',
|
apiVersion: '2015-01-09',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// async getDomainList() {
|
// async getDomainList() {
|
||||||
|
@ -101,7 +101,6 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
|
||||||
// Line: 'oversea' // 海外
|
// Line: 'oversea' // 海外
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const requestOption = {
|
const requestOption = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
};
|
};
|
||||||
|
@ -119,7 +118,8 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async removeRecord(options: RemoveRecordOptions<any>): Promise<any> {
|
async removeRecord(options: RemoveRecordOptions<any>): Promise<any> {
|
||||||
const { fullRecord, value, record } = options;
|
const { fullRecord, value } = options.recordReq;
|
||||||
|
const record = options.recordRes;
|
||||||
const params = {
|
const params = {
|
||||||
RegionId: 'cn-hangzhou',
|
RegionId: 'cn-hangzhou',
|
||||||
RecordId: record,
|
RecordId: record,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue