mirror of https://github.com/certd/certd
perf: 密钥备份
parent
28bb4856be
commit
1c6028abcf
|
@ -307,10 +307,16 @@ export class Executor {
|
||||||
//更新pipeline vars
|
//更新pipeline vars
|
||||||
if (Object.keys(instance._result.pipelineVars).length > 0) {
|
if (Object.keys(instance._result.pipelineVars).length > 0) {
|
||||||
// 判断 pipelineVars 有值时更新
|
// 判断 pipelineVars 有值时更新
|
||||||
const vars = this.pipelineContext.getObj("vars");
|
const vars = await this.pipelineContext.getObj("vars");
|
||||||
merge(vars, instance._result.pipelineVars);
|
merge(vars, instance._result.pipelineVars);
|
||||||
await this.pipelineContext.setObj("vars", vars);
|
await this.pipelineContext.setObj("vars", vars);
|
||||||
}
|
}
|
||||||
|
if (Object.keys(instance._result.pipelinePrivateVars).length > 0) {
|
||||||
|
// 判断 pipelineVars 有值时更新
|
||||||
|
const vars = await this.pipelineContext.getObj("privateVars");
|
||||||
|
merge(vars, instance._result.pipelinePrivateVars);
|
||||||
|
await this.pipelineContext.setObj("privateVars", vars);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async notification(when: NotificationWhen, error?: any) {
|
async notification(when: NotificationWhen, error?: any) {
|
||||||
|
|
|
@ -60,6 +60,7 @@ export type TaskResult = {
|
||||||
clearLastStatus?: boolean;
|
clearLastStatus?: boolean;
|
||||||
files?: FileItem[];
|
files?: FileItem[];
|
||||||
pipelineVars: Record<string, any>;
|
pipelineVars: Record<string, any>;
|
||||||
|
pipelinePrivateVars?: Record<string, any>;
|
||||||
};
|
};
|
||||||
export type TaskInstanceContext = {
|
export type TaskInstanceContext = {
|
||||||
//流水线定义
|
//流水线定义
|
||||||
|
@ -97,7 +98,7 @@ export type TaskInstanceContext = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class AbstractTaskPlugin implements ITaskPlugin {
|
export abstract class AbstractTaskPlugin implements ITaskPlugin {
|
||||||
_result: TaskResult = { clearLastStatus: false, files: [], pipelineVars: {} };
|
_result: TaskResult = { clearLastStatus: false, files: [], pipelineVars: {}, pipelinePrivateVars: {} };
|
||||||
ctx!: TaskInstanceContext;
|
ctx!: TaskInstanceContext;
|
||||||
logger!: ILogger;
|
logger!: ILogger;
|
||||||
accessService!: IAccessService;
|
accessService!: IAccessService;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
export class BaseSettings {
|
export class BaseSettings {
|
||||||
static __key__: string;
|
static __key__: string;
|
||||||
static __title__: string;
|
static __title__: string;
|
||||||
|
@ -29,8 +31,10 @@ export class SysPrivateSettings extends BaseSettings {
|
||||||
httpProxy? = '';
|
httpProxy? = '';
|
||||||
|
|
||||||
removeSecret() {
|
removeSecret() {
|
||||||
delete this.jwtKey;
|
const clone = cloneDeep(this);
|
||||||
delete this.encryptSecret;
|
delete clone.jwtKey;
|
||||||
|
delete clone.encryptSecret;
|
||||||
|
return clone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +87,14 @@ export class SysSiteInfo extends BaseSettings {
|
||||||
loginLogo?: string;
|
loginLogo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SysSecretBackup extends BaseSettings {
|
||||||
|
static __title__ = '密钥信息备份';
|
||||||
|
static __key__ = 'sys.secret';
|
||||||
|
static __access__ = 'private';
|
||||||
|
siteId?: string;
|
||||||
|
encryptSecret?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class SysSiteEnv {
|
export class SysSiteEnv {
|
||||||
agent?: {
|
agent?: {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
|
|
@ -3,10 +3,10 @@ import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { SysSettingsEntity } from '../entity/sys-settings.js';
|
import { SysSettingsEntity } from '../entity/sys-settings.js';
|
||||||
import { CacheManager } from '@midwayjs/cache';
|
import { CacheManager } from '@midwayjs/cache';
|
||||||
import { BaseSettings, SysPrivateSettings, SysPublicSettings } from './models.js';
|
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecretBackup } from './models.js';
|
||||||
import * as _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import { BaseService } from '../../../basic/index.js';
|
import { BaseService } from '../../../basic/index.js';
|
||||||
import { setGlobalProxy } from '@certd/basic';
|
import { logger, setGlobalProxy } from '@certd/basic';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置
|
* 设置
|
||||||
|
@ -146,4 +146,21 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
||||||
}
|
}
|
||||||
await this.cache.del(`settings.${key}`);
|
await this.cache.del(`settings.${key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async backupSecret() {
|
||||||
|
const settings = await this.getSettingByKey(SysSecretBackup.__key__);
|
||||||
|
if (settings == null) {
|
||||||
|
const backup = new SysSecretBackup();
|
||||||
|
const privateSettings = await this.getPrivateSettings();
|
||||||
|
const installInfo = await this.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||||
|
if (installInfo.siteId == null || privateSettings.encryptSecret == null) {
|
||||||
|
logger.error('备份密钥失败,siteId或encryptSecret为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
backup.siteId = installInfo.siteId;
|
||||||
|
backup.encryptSecret = privateSettings.encryptSecret;
|
||||||
|
await this.saveSetting(backup);
|
||||||
|
logger.info('备份密钥成功');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,6 +144,10 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
||||||
this.cert = cert;
|
this.cert = cert;
|
||||||
|
|
||||||
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
|
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
|
||||||
|
if (!this._result.pipelinePrivateVars) {
|
||||||
|
this._result.pipelinePrivateVars = {};
|
||||||
|
}
|
||||||
|
this._result.pipelinePrivateVars.cert = cert;
|
||||||
|
|
||||||
if (cert.pfx == null || cert.der == null) {
|
if (cert.pfx == null || cert.der == null) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -146,6 +146,13 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
})
|
})
|
||||||
googleCommonEabAccessId!: number;
|
googleCommonEabAccessId!: number;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "ZeroSSL公共EAB授权",
|
||||||
|
isSys: true,
|
||||||
|
show: false,
|
||||||
|
})
|
||||||
|
zerosslCommonEabAccessId!: number;
|
||||||
|
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: "EAB授权",
|
title: "EAB授权",
|
||||||
component: {
|
component: {
|
||||||
|
@ -159,7 +166,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
mergeScript: `
|
mergeScript: `
|
||||||
return {
|
return {
|
||||||
show: ctx.compute(({form})=>{
|
show: ctx.compute(({form})=>{
|
||||||
return form.sslProvider === 'zerossl' || (form.sslProvider === 'google' && !form.googleCommonEabAccessId)
|
return (form.sslProvider === 'zerossl' || !form.zerosslCommonEabAccessId) || (form.sslProvider === 'google' && !form.googleCommonEabAccessId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
@ -266,7 +273,11 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
}
|
}
|
||||||
} else if (this.sslProvider === "zerossl") {
|
} else if (this.sslProvider === "zerossl") {
|
||||||
if (this.eabAccessId) {
|
if (this.eabAccessId) {
|
||||||
|
this.logger.info("当前正在使用 zerossl EAB授权");
|
||||||
eab = await this.ctx.accessService.getById(this.eabAccessId);
|
eab = await this.ctx.accessService.getById(this.eabAccessId);
|
||||||
|
} else if (this.zerosslCommonEabAccessId) {
|
||||||
|
this.logger.info("当前正在使用 zerossl 公共EAB授权");
|
||||||
|
eab = await this.ctx.accessService.getById(this.zerosslCommonEabAccessId);
|
||||||
} else {
|
} else {
|
||||||
this.logger.error("zerossl需要配置EAB授权");
|
this.logger.error("zerossl需要配置EAB授权");
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { request } from "/src/api/service";
|
||||||
|
|
||||||
const apiPrefix = "/pi/pipeline";
|
const apiPrefix = "/pi/pipeline";
|
||||||
const historyApiPrefix = "/pi/history";
|
const historyApiPrefix = "/pi/history";
|
||||||
|
const certApiPrefix = "/pi/cert";
|
||||||
|
|
||||||
export async function GetList(query: any) {
|
export async function GetList(query: any) {
|
||||||
return await request({
|
return await request({
|
||||||
|
@ -82,3 +83,19 @@ export async function GetFiles(pipelineId: number) {
|
||||||
params: { pipelineId }
|
params: { pipelineId }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CertInfo = {
|
||||||
|
crt: string;
|
||||||
|
key: string;
|
||||||
|
ic: string;
|
||||||
|
der: string;
|
||||||
|
pfx: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function GetCert(pipelineId: number): Promise<CertInfo> {
|
||||||
|
return await request({
|
||||||
|
url: certApiPrefix + "/get",
|
||||||
|
method: "post",
|
||||||
|
params: { id: pipelineId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -148,6 +148,69 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
||||||
router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } });
|
router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewCert = async (row: any) => {
|
||||||
|
const cert = await api.GetCert(row.id);
|
||||||
|
if (!cert) {
|
||||||
|
notification.error({ message: "还没有产生证书,请先运行流水线" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.success({
|
||||||
|
title: "查看证书",
|
||||||
|
maskClosable: true,
|
||||||
|
okText: "关闭",
|
||||||
|
content: () => {
|
||||||
|
return (
|
||||||
|
<div class={"view-cert"}>
|
||||||
|
<div class={"cert-txt"}>
|
||||||
|
<h3>fullchain.pem</h3>
|
||||||
|
<div>{cert.crt}</div>
|
||||||
|
</div>
|
||||||
|
<div class={"cert-txt"}>
|
||||||
|
<h3>private.pem</h3>
|
||||||
|
<div>{cert.key}</div>
|
||||||
|
</div>
|
||||||
|
<div class={"cert-txt"}>
|
||||||
|
<h3>中间证书.pem</h3>
|
||||||
|
<div>{cert.ic}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadCert = async (row: any) => {
|
||||||
|
const files = await api.GetFiles(row.id);
|
||||||
|
Modal.success({
|
||||||
|
title: "文件下载",
|
||||||
|
maskClosable: true,
|
||||||
|
okText: "↑↑↑ 点击上面链接下载",
|
||||||
|
content: () => {
|
||||||
|
const children = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const downloadUrl = `${env.API}/pi/history/download?pipelineId=${row.id}&fileId=${file.id}`;
|
||||||
|
children.push(
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<a href={downloadUrl} target={"_blank"}>
|
||||||
|
{file.filename}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a-button type={"primary"} onClick={viewCert}>
|
||||||
|
直接查看证书
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div class={"mt-3"}>{children}</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
return {
|
return {
|
||||||
|
@ -243,27 +306,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
||||||
type: "link",
|
type: "link",
|
||||||
icon: "ant-design:download-outlined",
|
icon: "ant-design:download-outlined",
|
||||||
async click({ row }) {
|
async click({ row }) {
|
||||||
const files = await api.GetFiles(row.id);
|
downloadCert(row);
|
||||||
Modal.success({
|
|
||||||
title: "文件下载",
|
|
||||||
maskClosable: true,
|
|
||||||
okText: "↑↑↑ 点击上面链接下载",
|
|
||||||
content: () => {
|
|
||||||
const children = [];
|
|
||||||
for (const file of files) {
|
|
||||||
const downloadUrl = `${env.API}/pi/history/download?pipelineId=${row.id}&fileId=${file.id}`;
|
|
||||||
children.push(
|
|
||||||
<p>
|
|
||||||
<a href={downloadUrl} target={"_blank"}>
|
|
||||||
{file.filename}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div class={"mt-3"}>{children}</div>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
remove: {
|
remove: {
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
||||||
|
import { PipelineService } from '../../modules/pipeline/service/pipeline-service.js';
|
||||||
|
import { BaseController, Constants } from '@certd/lib-server';
|
||||||
|
import { StorageService } from '../../modules/pipeline/service/storage-service.js';
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
@Controller('/api/pi/cert')
|
||||||
|
export class CertController extends BaseController {
|
||||||
|
@Inject()
|
||||||
|
pipelineService: PipelineService;
|
||||||
|
@Inject()
|
||||||
|
storeService: StorageService;
|
||||||
|
|
||||||
|
@Post('/get', { summary: Constants.per.authOnly })
|
||||||
|
async getCert(@Query('id') id: number) {
|
||||||
|
const userId = this.getUserId();
|
||||||
|
await this.pipelineService.checkUserId(id, userId);
|
||||||
|
const privateVars = this.storeService.getPipelinePrivateVars(id);
|
||||||
|
return this.ok(privateVars);
|
||||||
|
}
|
||||||
|
}
|
|
@ -80,8 +80,8 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
|
||||||
@Post('/getSysSettings', { summary: 'sys:settings:edit' })
|
@Post('/getSysSettings', { summary: 'sys:settings:edit' })
|
||||||
async getSysSettings() {
|
async getSysSettings() {
|
||||||
const publicSettings = await this.service.getPublicSettings();
|
const publicSettings = await this.service.getPublicSettings();
|
||||||
const privateSettings = await this.service.getPrivateSettings();
|
let privateSettings = await this.service.getPrivateSettings();
|
||||||
privateSettings.removeSecret();
|
privateSettings = privateSettings.removeSecret();
|
||||||
return this.ok({ public: publicSettings, private: privateSettings });
|
return this.ok({ public: publicSettings, private: privateSettings });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,8 @@ export class AutoInitSite {
|
||||||
await this.sysSettingsService.saveSetting(privateInfo);
|
await this.sysSettingsService.saveSetting(privateInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.sysSettingsService.backupSecret();
|
||||||
|
|
||||||
await this.sysSettingsService.reloadPrivateSettings();
|
await this.sysSettingsService.reloadPrivateSettings();
|
||||||
|
|
||||||
// 授权许可
|
// 授权许可
|
||||||
|
|
|
@ -44,6 +44,9 @@ export class StorageService extends BaseService<StorageEntity> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async findPipelineVars(pipelineIds: number[]) {
|
async findPipelineVars(pipelineIds: number[]) {
|
||||||
|
if (pipelineIds == null || pipelineIds.length === 0) {
|
||||||
|
throw new Error('pipelineIds 不能为空');
|
||||||
|
}
|
||||||
return await this.repository.find({
|
return await this.repository.find({
|
||||||
where: {
|
where: {
|
||||||
scope: 'pipeline',
|
scope: 'pipeline',
|
||||||
|
@ -52,4 +55,17 @@ export class StorageService extends BaseService<StorageEntity> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPipelinePrivateVars(pipelineId: number) {
|
||||||
|
if (pipelineId == null) {
|
||||||
|
throw new Error('pipelineId 不能为空');
|
||||||
|
}
|
||||||
|
return await this.repository.find({
|
||||||
|
where: {
|
||||||
|
scope: 'pipeline',
|
||||||
|
namespace: pipelineId + '',
|
||||||
|
key: 'privateVars',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue