mirror of
https://github.com/certd/certd.git
synced 2026-04-28 07:57:25 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd23ee2055 | |||
| e00830bebc | |||
| 00e6d580c2 | |||
| 9c7b419e8f | |||
| 95edc0d303 | |||
| 5991b1e37c | |||
| 1aa50cf53a | |||
| eab66e2d19 | |||
| 5b504f094f | |||
| 1460cb9ac1 | |||
| 53782cbf49 | |||
| 0ea22dddf0 |
Vendored
+10
@@ -65,6 +65,16 @@
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"name": "server-new",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/packages/ui/certd-server",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": ["dev-new"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"name": "server-local-plus",
|
||||
"type": "node",
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.39.11](https://github.com/certd/certd/compare/v1.39.10...v1.39.11) (2026-04-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复列表页面底部滚动条与表格之间有空白间隙的bug ([71cfcad](https://github.com/certd/certd/commit/71cfcad2a15aac0badd85a10c4012a1e713654d1))
|
||||
* 修复流水线未编辑模式下也提示未保存的bug ([64a3503](https://github.com/certd/certd/commit/64a350364d820725b5e69d22ac2416809092f97d))
|
||||
* 修复商业版设置了公共eab,创建流水线仍然会显示需要配置eab的bug ([24dff05](https://github.com/certd/certd/commit/24dff05f6427dadec1e40350214c0167e1d6a73d))
|
||||
* 修复站点监控某些情况下获取不到证书的bug ([a2bbc7e](https://github.com/certd/certd/commit/a2bbc7e27298821d75a36abac6ec05d86dcf51f4))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 支持google dns插件 ([edc7bfc](https://github.com/certd/certd/commit/edc7bfc23043c2c6ef5f3564392f8aac6661c4bf))
|
||||
* 阿里云waf支持云产品接入方式应用的证书部署 ([2f7514a](https://github.com/certd/certd/commit/2f7514a2e7d89a34f833401a983149e667da911b))
|
||||
* 模版创建流水线支持随机时间 ([575415b](https://github.com/certd/certd/commit/575415b93a3e10e1c6e5644f71ddc711ea6f8adc))
|
||||
* 商业版支持配置证书申请插件参数 ([7ac789c](https://github.com/certd/certd/commit/7ac789c9c7e91cdf08dfdae1bb49186552e370e3))
|
||||
* 添加全新的未登录首页和路由配置 ([d1988dc](https://github.com/certd/certd/commit/d1988dc982440472ecf61847ccad76e4c96a80fb))
|
||||
* 添加Azure DNS插件支持及文档 ([1f1d687](https://github.com/certd/certd/commit/1f1d6873172d71fadaa5a0005e1d6f3f528096fc))
|
||||
* 添加HiPMDnsmgr DNS提供商的支持 @WUHINS ([296dcab](https://github.com/certd/certd/commit/296dcab4c7c26cb3f9da1ff748cc6a6b7d83edda))
|
||||
* 为DNS解析器添加超时配置,避免查询时间过长 ([cc5154e](https://github.com/certd/certd/commit/cc5154e04e87f648111119b4eeb4e3cb4dd6cc41))
|
||||
* 优化权威域名服务器查询超时时长 ([77db5ec](https://github.com/certd/certd/commit/77db5ecd12c51293e4de178e43ca0067bc70b46d))
|
||||
* 支持部署到nginx-proxy-manager ([2e6e9ed](https://github.com/certd/certd/commit/2e6e9ed9255bcf178edb0eb00d93a7f13c214430))
|
||||
* 支持一键安装脚本 ([dc969dd](https://github.com/certd/certd/commit/dc969dd7edb6934a29d6657afefe6f8af056741c))
|
||||
* 支持主动修改绑定url地址 ([11b7cfe](https://github.com/certd/certd/commit/11b7cfe5cb7e88e6ebd68d53acb4e5b556550ca9))
|
||||
* apisix支持v2 ([23b4658](https://github.com/certd/certd/commit/23b465867244b199bab9b61863a5ca43644834a9))
|
||||
* **technitium:** 添加Technitium DNS Server插件支持 ([edeb817](https://github.com/certd/certd/commit/edeb817c39597e4fa73a17ff4ca3f712f0320fec))
|
||||
|
||||
## [1.39.10](https://github.com/certd/certd/compare/v1.39.9...v1.39.10) (2026-04-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -70,5 +70,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -494,7 +494,7 @@ class AcmeClient {
|
||||
throw new Error('Unable to verify ACME challenge, URL not found');
|
||||
}
|
||||
|
||||
const {challenges} = createChallengeFn({logger:this.logger});
|
||||
const {challenges} = createChallengeFn({logger:this.logger,walkFromAuthoritative: this.opts.walkFromAuthoritative});
|
||||
|
||||
const verify = challenges
|
||||
if (typeof verify[challenge.type] === 'undefined') {
|
||||
|
||||
@@ -252,7 +252,7 @@ async function resolveDomainBySoaRecord(recordName, logger = log) {
|
||||
|
||||
async function getAuthoritativeDnsResolver(recordName, logger = log) {
|
||||
logger(`获取域名${recordName}的权威NS服务器: `);
|
||||
const resolver = new dns.Resolver({ timeout: 10000,maxTimeout: 60000 });
|
||||
const resolver = new dns.Resolver({timeout: 2000,tries: 2});
|
||||
|
||||
try {
|
||||
/* Resolve root domain by SOA */
|
||||
@@ -352,3 +352,5 @@ export {
|
||||
resolveDomainBySoaRecord
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,19 +4,23 @@
|
||||
|
||||
import dnsSdk from "dns"
|
||||
import https from 'https'
|
||||
import {log as defaultLog} from './logger.js'
|
||||
import { log as defaultLog } from './logger.js'
|
||||
import axios from './axios.js'
|
||||
import * as util from './util.js'
|
||||
import {isAlpnCertificateAuthorizationValid} from './crypto/index.js'
|
||||
import {utils} from '@certd/basic'
|
||||
import { isAlpnCertificateAuthorizationValid } from './crypto/index.js'
|
||||
import { utils } from '@certd/basic'
|
||||
|
||||
const dns = dnsSdk.promises
|
||||
|
||||
let walkFromAuthoritative = true
|
||||
export function setWalkFromAuthoritative(value = true) {
|
||||
walkFromAuthoritative = value
|
||||
}
|
||||
|
||||
export function createChallengeFn(opts = {}){
|
||||
const logger = opts?.logger || {info:defaultLog,error:defaultLog,warn:defaultLog,debug:defaultLog}
|
||||
export function createChallengeFn(opts = {}) {
|
||||
const logger = opts?.logger || { info: defaultLog, error: defaultLog, warn: defaultLog, debug: defaultLog }
|
||||
|
||||
const log = function(...args){
|
||||
const log = function (...args) {
|
||||
logger.info(...args)
|
||||
}
|
||||
/**
|
||||
@@ -31,9 +35,9 @@ export function createChallengeFn(opts = {}){
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
|
||||
async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
|
||||
async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
|
||||
|
||||
async function doQuery(challengeUrl){
|
||||
async function doQuery(challengeUrl) {
|
||||
log(`正在测试请求 ${challengeUrl} `)
|
||||
// const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
|
||||
// const challengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`;
|
||||
@@ -43,11 +47,11 @@ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix =
|
||||
|
||||
log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
|
||||
let data = ""
|
||||
try{
|
||||
try {
|
||||
const resp = await axios.get(challengeUrl, { httpsAgent });
|
||||
data = (resp.data || '').replace(/\s+$/, '');
|
||||
}catch (e) {
|
||||
log(`[error] HTTP request error from ${authz.identifier.value}`,e.message);
|
||||
} catch (e) {
|
||||
log(`[error] HTTP request error from ${authz.identifier.value}`, e.message);
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -61,7 +65,7 @@ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix =
|
||||
|
||||
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
|
||||
let host = authz.identifier.value;
|
||||
if(utils.domain.isIpv6(host)){
|
||||
if (utils.domain.isIpv6(host)) {
|
||||
host = `[${host}]`;
|
||||
}
|
||||
const challengeUrl = `http://${host}:${httpPort}${suffix}`;
|
||||
@@ -78,19 +82,20 @@ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix =
|
||||
|
||||
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Walk DNS until TXT records are found
|
||||
*/
|
||||
|
||||
async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
|
||||
async function walkDnsChallengeRecord(recordName, resolver = dns, deep = 0) {
|
||||
|
||||
let records = [];
|
||||
|
||||
const isAuthoritative = resolver === dns
|
||||
/* Resolve TXT records */
|
||||
try {
|
||||
log(`检查域名 ${recordName} 的TXT记录`);
|
||||
log(`检查域名 ${recordName} 的TXT记录(from ${isAuthoritative ? '本地DNS' : '权威DNS服务器'})`);
|
||||
const txtRecords = await resolver.resolveTxt(recordName);
|
||||
if (txtRecords && txtRecords.length) {
|
||||
log(`找到 ${txtRecords.length} 条 TXT记录( ${recordName})`);
|
||||
@@ -109,22 +114,22 @@ async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
|
||||
if (cnameRecords.length) {
|
||||
const cnameRecord = cnameRecords[0];
|
||||
log(`已找到${recordName}的CNAME记录,将检查: ${cnameRecord}`);
|
||||
let res= await walkTxtRecord(cnameRecord,deep+1);
|
||||
let res = await walkTxtRecord(cnameRecord, deep + 1);
|
||||
if (res && res.length) {
|
||||
log(`从CNAME中找到TXT记录: ${JSON.stringify(res)}`);
|
||||
records = records.concat(...res);
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
log(`没有CNAME映射(${recordName})`);
|
||||
}
|
||||
} catch (e) {
|
||||
log(`检查CNAME出错(${recordName}) :${e.message}`);
|
||||
}
|
||||
return records
|
||||
}
|
||||
}
|
||||
|
||||
async function walkTxtRecord(recordName,deep = 0) {
|
||||
if(deep >5){
|
||||
async function walkTxtRecord(recordName, deep = 0) {
|
||||
if (deep > 5) {
|
||||
log(`walkTxtRecord too deep (#${deep}) , skip walk`)
|
||||
return []
|
||||
}
|
||||
@@ -133,7 +138,7 @@ async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
|
||||
try {
|
||||
/* Default DNS resolver first */
|
||||
log('从本地DNS服务器获取TXT解析记录');
|
||||
const res = await walkDnsChallengeRecord(recordName,dns,deep);
|
||||
const res = await walkDnsChallengeRecord(recordName, dns, deep);
|
||||
if (res && res.length > 0) {
|
||||
for (const item of res) {
|
||||
txtRecords.push(item)
|
||||
@@ -144,27 +149,32 @@ async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
|
||||
log(`本地获取TXT解析记录失败:${e.message}`)
|
||||
}
|
||||
|
||||
try{
|
||||
if (walkFromAuthoritative !==false) {
|
||||
try {
|
||||
/* Authoritative DNS resolver */
|
||||
log(`从域名权威服务器获取TXT解析记录`);
|
||||
const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName,log);
|
||||
const res = await walkDnsChallengeRecord(recordName, authoritativeResolver,deep);
|
||||
const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName, log);
|
||||
const res = await walkDnsChallengeRecord(recordName, authoritativeResolver, deep);
|
||||
if (res && res.length > 0) {
|
||||
for (const item of res) {
|
||||
txtRecords.push(item)
|
||||
}
|
||||
}
|
||||
}catch (e) {
|
||||
} catch (e) {
|
||||
log(`权威服务器获取TXT解析记录失败:${e.message}`)
|
||||
}
|
||||
}else{
|
||||
log(`跳过从权威服务器获取TXT解析记录`);
|
||||
}
|
||||
|
||||
|
||||
if (txtRecords.length === 0) {
|
||||
throw new Error(`没有找到TXT解析记录(${recordName})`);
|
||||
}
|
||||
return txtRecords;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Verify ACME DNS challenge
|
||||
*
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
|
||||
@@ -176,10 +186,10 @@ async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
|
||||
async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
|
||||
async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
|
||||
const recordName = `${prefix}${authz.identifier.value}`;
|
||||
log(`本地校验TXT记录): ${recordName}`);
|
||||
let recordValues = await walkTxtRecord(recordName);
|
||||
let recordValues = await walkTxtRecord(recordName, 0, walkFromAuthoritative);
|
||||
//去重
|
||||
recordValues = [...new Set(recordValues)];
|
||||
log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
|
||||
@@ -190,9 +200,9 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
|
||||
|
||||
log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Verify ACME TLS ALPN challenge
|
||||
*
|
||||
* https://datatracker.ietf.org/doc/html/rfc8737
|
||||
@@ -203,7 +213,7 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
|
||||
async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
|
||||
async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
|
||||
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
|
||||
const host = authz.identifier.value;
|
||||
log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
|
||||
@@ -217,15 +227,20 @@ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
|
||||
|
||||
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
challenges:{
|
||||
challenges: {
|
||||
'http-01': verifyHttpChallenge,
|
||||
'dns-01': verifyDnsChallenge,
|
||||
'tls-alpn-01': verifyTlsAlpnChallenge,
|
||||
},
|
||||
walkTxtRecord,
|
||||
walkDnsChallengeRecord,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// createChallengeFn({logger:{info:console.log}}).walkDnsChallengeRecord("handsfree.work")
|
||||
|
||||
+2
@@ -220,3 +220,5 @@ export function getAuthoritativeDnsResolver(record:string): Promise<any>;
|
||||
export const CancelError: typeof CancelError;
|
||||
|
||||
export function resolveDomainBySoaRecord(domain: string): Promise<string>;
|
||||
|
||||
export function setWalkFromAuthoritative(value = true): void;
|
||||
@@ -47,5 +47,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -111,8 +111,13 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
|
||||
if (config.logData == null) {
|
||||
config.logData = false;
|
||||
}
|
||||
if (config.logReq == null) {
|
||||
config.logReq = true;
|
||||
}
|
||||
|
||||
if (config.logReq !== false) {
|
||||
logger.info(`http request:${config.url},method:${config.method}`);
|
||||
}
|
||||
if (config.logParams !== false && config.params) {
|
||||
logger.info(`params:${JSON.stringify(config.params)}`);
|
||||
}
|
||||
@@ -151,10 +156,11 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
|
||||
|
||||
config.retry = merge(
|
||||
{
|
||||
status: [421],
|
||||
status: [421, 524],
|
||||
count: 0,
|
||||
max: 3,
|
||||
delay: 1000,
|
||||
delay: 2000,
|
||||
includes: ["[524]"],
|
||||
},
|
||||
config.retry
|
||||
);
|
||||
@@ -273,7 +279,19 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
|
||||
const originalRequest = error.config || {};
|
||||
// logger.info(`config`, originalRequest);
|
||||
const retry = originalRequest.retry || {};
|
||||
if (retry.status && retry.status.includes(status)) {
|
||||
|
||||
const isRetryStatus = retry.status && retry.status.includes(status);
|
||||
let isRetryMessage = false;
|
||||
if (retry.includes) {
|
||||
for (const item of retry.includes) {
|
||||
if (error.message?.includes(item)) {
|
||||
isRetryMessage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isRetryStatus || isRetryMessage) {
|
||||
if (retry.max > 0 && retry.count < retry.max) {
|
||||
// 重试次数增加
|
||||
retry.count++;
|
||||
@@ -301,6 +319,7 @@ export type HttpClientResponse<R> = any;
|
||||
export type HttpRequestConfig<D = any> = {
|
||||
skipSslVerify?: boolean;
|
||||
skipCheckRes?: boolean;
|
||||
logReq?: boolean;
|
||||
logParams?: boolean;
|
||||
logRes?: boolean;
|
||||
logData?: boolean;
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export type ExecutorOptions = {
|
||||
pipeline: Pipeline;
|
||||
storage: IStorage;
|
||||
onChanged: (history: RunHistory) => Promise<void>;
|
||||
onFinished: (history: RunHistory) => Promise<void>;
|
||||
accessService: IAccessService;
|
||||
emailService: IEmailService;
|
||||
notificationService: INotificationService;
|
||||
@@ -47,16 +48,19 @@ export class Executor {
|
||||
lastRuntime!: RunHistory;
|
||||
options: ExecutorOptions;
|
||||
abort: AbortController = new AbortController();
|
||||
|
||||
_inited = false;
|
||||
|
||||
onChanged: (history: RunHistory) => Promise<void>;
|
||||
onFinished: (history: RunHistory) => Promise<void>;
|
||||
constructor(options: ExecutorOptions) {
|
||||
this.options = options;
|
||||
this.pipeline = cloneDeep(options.pipeline);
|
||||
this.onChanged = async (history: RunHistory) => {
|
||||
await options.onChanged(history);
|
||||
};
|
||||
this.onFinished = async (history: RunHistory) => {
|
||||
await options.onFinished(history);
|
||||
};
|
||||
this.pipeline.userId = options.user.id;
|
||||
this.contextFactory = new ContextFactory(options.storage);
|
||||
this.logger = logger;
|
||||
@@ -77,7 +81,7 @@ export class Executor {
|
||||
async cancel() {
|
||||
this.abort.abort();
|
||||
this.runtime?.cancel(this.pipeline);
|
||||
await this.onChanged(this.runtime);
|
||||
await this.onFinished(this.runtime);
|
||||
}
|
||||
|
||||
async run(runtimeId: any = 0, triggerType: string) {
|
||||
@@ -111,7 +115,7 @@ export class Executor {
|
||||
this.logger.error("pipeline 执行失败", e);
|
||||
} finally {
|
||||
clearInterval(intervalFlushLogId);
|
||||
await this.onChanged(this.runtime);
|
||||
await this.onFinished(this.runtime);
|
||||
//保存之前移除logs
|
||||
const lastRuntime: any = {
|
||||
...this.runtime,
|
||||
|
||||
@@ -87,6 +87,7 @@ export type Notification = {
|
||||
options?: EmailOptions;
|
||||
notificationId: number;
|
||||
title: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type Pipeline = Runnable & {
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
"prettier": "^2.8.8",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -31,5 +31,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"fetch"
|
||||
]
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -33,5 +33,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -64,5 +64,5 @@
|
||||
"typeorm": "^0.3.11",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ export class SysPrivateSettings extends BaseSettings {
|
||||
environmentVars?: string = '';
|
||||
|
||||
|
||||
acmeWalkFromAuthoritative?: boolean = true;
|
||||
|
||||
|
||||
sms?: {
|
||||
type?: string;
|
||||
config?: any;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Repository } from 'typeorm';
|
||||
import { SysSettingsEntity } from '../entity/sys-settings.js';
|
||||
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
|
||||
|
||||
import { getAllSslProviderDomains, setSslProviderReverseProxies } from '@certd/acme-client';
|
||||
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client';
|
||||
import { cache, logger, mergeUtils, setGlobalProxy } from '@certd/basic';
|
||||
import { isPlus } from '@certd/plus-core';
|
||||
import * as dns from 'node:dns';
|
||||
@@ -180,6 +180,9 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
||||
|
||||
//加载环境变量
|
||||
this.setEnvironmentVars(privateSetting.environmentVars);
|
||||
|
||||
setWalkFromAuthoritative(privateSetting.acmeWalkFromAuthoritative);
|
||||
|
||||
}
|
||||
|
||||
setEnvironmentVars(vars: string) {
|
||||
|
||||
@@ -46,5 +46,5 @@
|
||||
"typeorm": "^0.3.11",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "112a565bf74c50e16c27a2c0a716588f84f39789"
|
||||
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@ export async function getTodayVipOrderCount() {
|
||||
return await request({
|
||||
url: "/sys/plus/getTodayVipOrderCount",
|
||||
method: "post",
|
||||
showErrorNotify: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
<template>
|
||||
<div class="mt-10 vip-active-modal">
|
||||
<div v-if="todayOrderCount.enabled" class="order-count hidden md:flex">
|
||||
<div v-for="(stage, index) in todayOrderCount.stages" :key="index" class="status-item" :class="{ 'status-show': TodayVipOrderCountRef.current === index }">
|
||||
<div class="background">
|
||||
<img :src="stage.bg" alt="" />
|
||||
</div>
|
||||
<div class="flex flex-col order-count-text weight-bold">
|
||||
<div class="count-text ml-4 flex items-center">
|
||||
<fs-icon icon="noto:fire" class="fs-20 mr-2"></fs-icon>
|
||||
<template v-if="stage.vipTotal > 0">
|
||||
<span> 已有 </span>
|
||||
<span class="count-number color-red font-bold text-2xl ml-1 mr-1"> {{ stage.vipTotal }} </span> 位小伙伴赞助,
|
||||
<span>
|
||||
{{ stage.title }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span> 今日赞助 </span>
|
||||
<span class="count-number color-red font-bold text-2xl ml-1 mr-1"> {{ stage.orderCount }} </span> 人,
|
||||
<span>
|
||||
{{ stage.title }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="productInfo.notice" class="mt-10">
|
||||
<a-alert type="error" :message="productInfo.notice"></a-alert>
|
||||
</div>
|
||||
@@ -248,73 +222,35 @@ const vipTypeDefine: any = {
|
||||
const TodayVipOrderCountRef: Ref = ref({ enabled: false, current: 0, stages: [] });
|
||||
|
||||
async function getTodayVipOrderCount() {
|
||||
try {
|
||||
const res = await api.getTodayVipOrderCount();
|
||||
if (res) {
|
||||
TodayVipOrderCountRef.value = res;
|
||||
TodayVipOrderCountRef.value.current = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const todayOrderCount = computed(() => {
|
||||
const countInfo = TodayVipOrderCountRef.value;
|
||||
const enabled = countInfo?.enabled || false;
|
||||
const orderCount = countInfo?.orderCount || 0;
|
||||
for (const stage of countInfo?.stages) {
|
||||
stage.orderCount = stage.countGe || 0;
|
||||
}
|
||||
const lastStage = countInfo?.stages?.[countInfo?.stages?.length - 1] || {};
|
||||
lastStage.orderCount = orderCount;
|
||||
|
||||
const vipTotal = countInfo?.vipTotal || 0;
|
||||
const showVipTotal = countInfo?.showVipTotal || false;
|
||||
const userTotal = countInfo?.userTotal || 0;
|
||||
const stages: any = [];
|
||||
stages.push({
|
||||
title: countInfo.title,
|
||||
vipTotal: countInfo?.vipTotal || 0,
|
||||
orderCount: orderCount,
|
||||
bg: lastStage.bg,
|
||||
showVipTotal: showVipTotal,
|
||||
});
|
||||
if (lastStage.orderCount > 0) {
|
||||
stages.push(lastStage);
|
||||
}
|
||||
return {
|
||||
enabled: enabled,
|
||||
stages: stages,
|
||||
showVipTotal: showVipTotal,
|
||||
vipTotal: vipTotal,
|
||||
userTotal: userTotal,
|
||||
};
|
||||
});
|
||||
|
||||
async function scrollOrderCount() {
|
||||
const stages = todayOrderCount.value.stages;
|
||||
if (stages.length === 0) {
|
||||
return;
|
||||
}
|
||||
let index = 0;
|
||||
const doScroll = () => {
|
||||
TodayVipOrderCountRef.value.current = index;
|
||||
index++;
|
||||
if (index >= stages.length) {
|
||||
index = 0;
|
||||
}
|
||||
};
|
||||
doScroll();
|
||||
scrollOrderCountIntervalRef.value = setInterval(doScroll, 7000);
|
||||
}
|
||||
|
||||
const scrollOrderCountIntervalRef: Ref = ref(null);
|
||||
onMounted(async () => {
|
||||
await getTodayVipOrderCount();
|
||||
await nextTick();
|
||||
await scrollOrderCount();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(scrollOrderCountIntervalRef.value);
|
||||
});
|
||||
onUnmounted(() => {});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
||||
@@ -749,6 +749,18 @@ export default {
|
||||
pipelineValidTimeEnabledHelper: "Whether to enable the valid time of the pipeline",
|
||||
certDomainAddToMonitorEnabled: "Add Domain to Certificate Monitor",
|
||||
certDomainAddToMonitorEnabledHelper: "Whether to add the domain to the certificate monitor",
|
||||
|
||||
defaultCertRenewDays: "Default Certificate Renew Days",
|
||||
defaultCertRenewDaysHelper: "Default certificate renewal days, helpful for table list progress bar display",
|
||||
defaultCertRenewDaysRecommend: "Recommend 15",
|
||||
|
||||
pipelineMaxRunningCount: "Max Running Count",
|
||||
pipelineMaxRunningCountHelper: "Max running count of the pipeline",
|
||||
pipelineMaxRunningCountRecommend: "Recommend 5-15, default 10",
|
||||
|
||||
acmeWalkFromAuthoritative: "Check TXT Record from Authoritative NS",
|
||||
acmeWalkFromAuthoritativeHelper: "Apply certificate when whether to check the TXT record from authoritative NS server first",
|
||||
|
||||
fixedCertExpireDays: "Fixed Cert Expire Days",
|
||||
fixedCertExpireDaysHelper: "Fixed cert expiration days, helpful for table list progress bar display",
|
||||
fixedCertExpireDaysRecommend: "Recommend 90",
|
||||
|
||||
@@ -760,6 +760,8 @@ export default {
|
||||
pipelineMaxRunningCount: "同时最大运行流水线数量",
|
||||
pipelineMaxRunningCountHelper: "同一个用户同时运行的最大流水线数量,避免同时触发太多导致ACME账户被限制",
|
||||
pipelineMaxRunningCountRecommend: "推荐5-15,默认10",
|
||||
acmeWalkFromAuthoritative: "从权威NS检查TXT记录",
|
||||
acmeWalkFromAuthoritativeHelper: "申请证书时,是否从权威NS服务器检查TXT记录,如果影响申请证书,可以关闭",
|
||||
|
||||
fixedCertExpireDays: "固定证书有效期天数",
|
||||
fixedCertExpireDaysHelper: "固定证书有效期天数,有助于列表进度条整齐显示",
|
||||
@@ -807,6 +809,7 @@ export default {
|
||||
environmentVars: "环境变量",
|
||||
environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE",
|
||||
bindUrl: "绑定URL",
|
||||
bindUrlHelper: "绑定URL,在各类通知中显示你的站点URL",
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
|
||||
@@ -109,6 +109,7 @@ export type SysPrivateSetting = {
|
||||
type?: string;
|
||||
config?: any;
|
||||
};
|
||||
acmeWalkFromAuthoritative?: boolean;
|
||||
|
||||
//http请求超时时间
|
||||
httpRequestTimeout?: number;
|
||||
|
||||
@@ -303,8 +303,18 @@ export const useSettingStore = defineStore({
|
||||
}
|
||||
};
|
||||
const { closable = false } = opts;
|
||||
let title = "URL地址未绑定,是否绑定此地址?";
|
||||
let okButtonText = "不,回到原来的地址";
|
||||
let okButtonDanger = false;
|
||||
let forceBack = true;
|
||||
if (closable) {
|
||||
title = "绑定URL";
|
||||
okButtonText = "确定";
|
||||
okButtonDanger = false;
|
||||
forceBack = false;
|
||||
}
|
||||
const modalRef: any = Modal.warning({
|
||||
title: "URL地址未绑定,是否绑定此地址?",
|
||||
title: title,
|
||||
width: 500,
|
||||
keyboard: false,
|
||||
closable,
|
||||
@@ -320,6 +330,7 @@ export const useSettingStore = defineStore({
|
||||
绑定到地址1
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="helper">各类通知里面会以地址1作为URL显示</div>
|
||||
<div class="flex items-center justify-between mt-3">
|
||||
<span>
|
||||
绑定地址2:
|
||||
@@ -334,12 +345,14 @@ export const useSettingStore = defineStore({
|
||||
},
|
||||
onOk: async () => {
|
||||
// await this.doBindUrl();
|
||||
if (forceBack) {
|
||||
window.location.href = bindUrl;
|
||||
}
|
||||
},
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
danger: okButtonDanger,
|
||||
},
|
||||
okText: "不,回到原来的地址",
|
||||
okText: okButtonText,
|
||||
// cancelText: "不,回到原来的地址",
|
||||
// onOk: () => {
|
||||
// window.location.href = bindUrl;
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<div class="landing-page">
|
||||
<nav class="landing-nav">
|
||||
<div class="nav-container">
|
||||
<div class="nav-logo">
|
||||
<div class="nav-logo overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<img :src="siteInfo.logo" alt="Certd Logo" class="logo-img" />
|
||||
<span class="logo-text">{{ siteInfo.title }}</span>
|
||||
<span class="logo-text ellipsis">{{ siteInfo.title }}</span>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<div class="nav-links text-nowrap">
|
||||
<ThemeToggle />
|
||||
<template v-if="isLoggedIn">
|
||||
<router-link to="/index" class="btn btn-primary">控制台</router-link>
|
||||
<div class="user-avatar" @click="goProfile">
|
||||
<!-- <div class="user-avatar" @click="goProfile">
|
||||
<div class="avatar-initials">{{ userInitials }}</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</template>
|
||||
<template v-else>
|
||||
<router-link :to="{ name: 'login' }" class="btn btn-outline">登录</router-link>
|
||||
@@ -25,9 +25,9 @@
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">
|
||||
让你的网站证书
|
||||
<span class="gradient-text">永不过期</span>
|
||||
<h1 class="hero-title flex flex-col md:flex-row">
|
||||
<div>让你的网站证书</div>
|
||||
<div class="gradient-text mt-2 md:mt-0 md:ml-4">永不过期</div>
|
||||
</h1>
|
||||
<p class="hero-description">全自动证书管理系统,首创流水线申请部署证书模式,让你告别证书过期的烦恼。</p>
|
||||
<div class="hero-actions">
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
<a-form-item :label="t('certd.sys.setting.bindUrl')">
|
||||
<a-button class="ml-2" type="primary" @click="settingsStore.openBindUrlModal({ closable: true })">{{ t("certd.sys.setting.bindUrl") }}</a-button>
|
||||
<div class="helper" v-html="t('certd.sys.setting.bindUrlHelper')"></div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
||||
|
||||
@@ -53,6 +53,13 @@
|
||||
<div class="helper">{{ t("certd.sys.setting.pipelineMaxRunningCountHelper") }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('certd.sys.setting.acmeWalkFromAuthoritative')" :name="['private', 'acmeWalkFromAuthoritative']">
|
||||
<div class="flex items-center">
|
||||
<a-switch v-model:checked="formState.private.acmeWalkFromAuthoritative" />
|
||||
</div>
|
||||
<div class="helper">{{ t("certd.sys.setting.acmeWalkFromAuthoritativeHelper") }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
||||
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
|
||||
</a-form-item>
|
||||
@@ -76,7 +83,9 @@ defineOptions({
|
||||
|
||||
const formState = reactive<Partial<SysSettings>>({
|
||||
public: {},
|
||||
private: {},
|
||||
private: {
|
||||
acmeWalkFromAuthoritative: true,
|
||||
},
|
||||
});
|
||||
|
||||
async function loadSysSettings() {
|
||||
|
||||
@@ -80,7 +80,7 @@ const development = {
|
||||
type: 'better-sqlite3',
|
||||
database: './data/db.sqlite',
|
||||
synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true
|
||||
logging: true,
|
||||
logging: false,
|
||||
highlightSql: false,
|
||||
|
||||
// 配置实体模型 或者 entities: '/entity',
|
||||
|
||||
@@ -675,7 +675,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
}
|
||||
}
|
||||
|
||||
const onChanged = async (history: RunHistory) => {
|
||||
const doSaveHistory = async (history: RunHistory) => {
|
||||
//保存执行历史
|
||||
try {
|
||||
logger.info("保存执行历史:", history.id);
|
||||
@@ -690,6 +690,46 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
class HistorySaver {
|
||||
latest: RunHistory = null;
|
||||
interval: any = null;
|
||||
started: boolean = false;
|
||||
async save(){
|
||||
const latest = this.latest;
|
||||
this.latest = null;
|
||||
if (latest == null) {
|
||||
return;
|
||||
}
|
||||
await doSaveHistory(latest);
|
||||
}
|
||||
|
||||
async start(){
|
||||
this.started = true
|
||||
await this.save();
|
||||
this.interval = setInterval(()=>{
|
||||
this.save();
|
||||
}, 1000 * 5);
|
||||
}
|
||||
async push(history: RunHistory){
|
||||
this.latest = history;
|
||||
if(!this.started){
|
||||
await this.start();
|
||||
}
|
||||
}
|
||||
async done(){
|
||||
clearInterval(this.interval);
|
||||
await this.save();
|
||||
}
|
||||
}
|
||||
|
||||
const historySaver = new HistorySaver();
|
||||
const onChanged = async (history: RunHistory)=>{
|
||||
await historySaver.push(history);
|
||||
}
|
||||
const onFinished = async (history: RunHistory)=>{
|
||||
await onChanged(history);
|
||||
await historySaver.done();
|
||||
}
|
||||
|
||||
const userId = entity.userId;
|
||||
const projectId = entity.projectId;
|
||||
@@ -723,6 +763,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
user,
|
||||
pipeline,
|
||||
onChanged,
|
||||
onFinished,
|
||||
accessService: accessGetter,
|
||||
cnameProxyService,
|
||||
pluginConfigService: this.pluginConfigGetter,
|
||||
@@ -762,15 +803,15 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
if (executor) {
|
||||
await executor.cancel();
|
||||
}
|
||||
const entity = await this.historyService.info(historyId);
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
const pipeline: Pipeline = JSON.parse(entity.pipeline);
|
||||
pipeline.status.status = ResultType.canceled;
|
||||
pipeline.status.result = ResultType.canceled;
|
||||
const runtime = new RunHistory(historyId, null, pipeline);
|
||||
await this.saveHistory(runtime);
|
||||
// const entity = await this.historyService.info(historyId);
|
||||
// if (entity == null) {
|
||||
// return;
|
||||
// }
|
||||
// const pipeline: Pipeline = JSON.parse(entity.pipeline);
|
||||
// pipeline.status.status = ResultType.canceled;
|
||||
// pipeline.status.result = ResultType.canceled;
|
||||
// const runtime = new RunHistory(historyId, null, pipeline);
|
||||
// await this.saveHistory(runtime);
|
||||
}
|
||||
|
||||
private getTriggerType(triggerId, pipeline) {
|
||||
|
||||
@@ -93,6 +93,9 @@ export class OnePanelClient {
|
||||
if (res.code === 200) {
|
||||
return res.data;
|
||||
}
|
||||
if (res?.message?.includes("record not found")){
|
||||
throw new Error("没有找到证书,请确认证书在1panel上是否已被删除,如果被删除请重新选择新的证书id:"+ config.url);
|
||||
}
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
|
||||
@@ -241,6 +241,7 @@ token=md5(zhangsan + 5dh232kfg!* + 1554691950854)=cfcd208495d565ef66e7dff9f98764
|
||||
try {
|
||||
const contentType = headers['content-type'] || '';
|
||||
// 判断是否是 GB2312/GBK 编码
|
||||
//@ts-ignore
|
||||
if (contentType.includes('gb2312') || contentType.includes('gbk')) {
|
||||
// 使用 iconv-lite 解码
|
||||
data = iconv.decode(data, 'gb2312');
|
||||
|
||||
@@ -40,3 +40,4 @@ function randomStr(length, options?) {
|
||||
}
|
||||
|
||||
export const RandomUtil = { randomStr };
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
23:47
|
||||
13:33
|
||||
|
||||
@@ -1 +1 @@
|
||||
00:29
|
||||
14:09
|
||||
|
||||
Reference in New Issue
Block a user