chore: license说明

pull/148/head
xiaojunnuo 2024-08-14 21:24:12 +08:00
parent 746bb9d385
commit db9d27468e
25 changed files with 483 additions and 114 deletions

30
LICENSE.md Normal file
View File

@ -0,0 +1,30 @@
# Certd Open Source License
- This project is licensed under the **GNU Affero General Public License (AGPL)** with the following additional terms.
- 本项目遵循 GNU Affero General Public LicenseAGPL并附加以下条款。
## 1. License Terms ( 许可证条款 )
1. **Freedom to Use** (自由使用)
- You are free to use, copy, modify, and distribute the source code of this project for personal or organizational use, provided that you comply with the terms of this license.
- 您可以自由使用、复制、修改和分发本项目的源代码,前提是您遵循本许可证的条款。
2. **Modification for Personal Use** (个人使用的修改)
- Individuals and companies are allowed to modify the project according to their needs for non-commercial purposes. However, modifications to the logo, copyright information, or any code related to licensing are strictly prohibited.
- 个人和公司允许根据自身需求对本项目进行修改以供非商业用途。但任何对logo、版权信息或与许可相关代码的修改都是严格禁止的。
3. **Commercial Authorization** (商业授权)
- If you wish to make any form of monetary gain from this project, you must first obtain commercial authorization from the original author. Users should contact the author directly to negotiate the relevant licensing terms.
- 如果您希望从本项目获得任何形式的经济收益,您必须首先从原作者处获得商业授权,用户应直接与作者联系,以协商相关许可条款。
4. **Retention of Rights** (保留权利)
- All rights, title, and interest in the project remain with the original author.
- 本项目的所有权利、标题和利益仍归原作者所有。
## 2. As a contributor ( 作为贡献者 )
- you should agree that your contributed code:
- 您应同意您贡献的代码:
1. - The original author can adjust the open-source agreement to be more strict or relaxed.
- 原作者可以调整开源协议以使其更严格或更宽松。
2. - Can be used for commercial purposes.
- 可用于商业用途。

View File

@ -1,7 +1,7 @@
# CertD
# Certd
CertD 是一个免费全自动申请和自动部署更新SSL证书的工具。
后缀D取自linux守护进程的命名风格意为证书守护进程。
Certd 是一个免费全自动申请和自动部署更新SSL证书的工具。
后缀d取自linux守护进程的命名风格意为证书守护进程。
关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签
@ -180,26 +180,37 @@ docker compose up -d
</p>
## 十、捐赠
媳妇儿说:“一天到晚搞开源,也不管管老婆孩子!😡😡😡”
拜托各位捐赠支持一下,让媳妇儿开心开心,我也能有更多时间进行开源项目,感谢🙏🙏🙏
<p align="center">
<img height="380" src="./doc/images/donate.png">
</p>
支持开源,为爱发电,我已入驻爱发电
https://afdian.com/a/greper
发电权益:
1. 可加入发电专属群(先加我好友,发送发电截图,我拉你进群)
2. 你的需求优先实现
3. 可以获得作者一对一技术支持
4. 更多权益陆续增加中...
## 十一、贡献代码
[贡献插件教程](./plugin.md)
1. [贡献插件教程](./plugin.md)
2. 作为贡献者,代表您同意您贡献的代码如下许可:
1. 可以调整开源协议以使其更严格或更宽松。
2. 可以用于商业用途。
## 十二、 开源许可
* 本项目遵循 GNU Affero General Public LicenseAGPL开源协议。
* 允许个人和公司使用、复制、修改和分发本项目,禁止任何形式的商业用途
* 未获得商业授权情况下禁止任何对logo、版权信息及授权许可相关代码的修改。
* 如需商业授权,请联系作者。
## 十二、我的其他项目求Star
## 十、我的其他项目求Star
* [袖手GPT](https://ai.handsfree.work/) ChatGPT国内可用无需FQ每日免费额度
* [fast-crud](https://gitee.com/fast-crud/fast-crud/) 基于vue3的crud快速开发框架
* [dev-sidecar](https://github.com/docmirror/dev-sidecar/) 直连访问github工具无需FQ解决github无法访问的问题
## 十、更新日志
## 十、更新日志
更新日志:[CHANGELOG](./CHANGELOG.md)

View File

@ -3,7 +3,6 @@ import { equal } from "assert";
describe("license", function () {
it("#license", async function () {
const req = {
appKey: "z4nXOeTeSnnpUpnmsV",
subjectId: "999",
license: "",
};

View File

@ -2,8 +2,8 @@ import { createVerify } from "node:crypto";
import { logger } from "../utils/index.js";
const SecreteKey =
"LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXY3TGtMaUp1dGM0NzhTU3RaTExjajVGZXh1YjJwR2NLMGxwa0hwVnlZWjhMY29rRFhuUlAKUGQ5UlJSTVRTaGJsbFl2Mzd4QUhOV1ZIQ0ZsWHkrQklVU001bUlBU1NDQTV0azlJNmpZZ2F4bEFDQm1BY0lGMwozKzBjeGZIYVkrVW9YdVluMkZ6YUt2Ym5GdFZIZ0lkMDg4a3d4clZTZzlCT3BDRVZIR1pxR2I5TWN5MXVHVXhUClFTVENCbmpoTWZlZ0p6cXVPYWVOY0ZPSE5tbmtWRWpLTythbTBPeEhNS1lyS3ZnQnVEbzdoVnFENlBFMUd6V3AKZHdwZUV4QXZDSVJxL2pWTkdRK3FtMkRWOVNJZ3U5bmF4MktmSUtFeU50dUFFS1VpekdqL0VmRFhDM1cxMExhegpKaGNYNGw1SUFZU1o3L3JWVmpGbExWSVl0WDU1T054L1Z3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K";
const appKey = "z4nXOeTeSnnpUpnmsV";
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQkNnS0NBUUVBMjdoZDM0NjRYbyt3TkpmTTNCWjE5MXlQK2NLaTd3ck9CbXdjTWJPZUdsNlJOMUVtTGhyMgplOFdvOGpmMW9IVXc5RFV6L2I2ZHU3Q3ZXMXZNUDA1Q3dSS3lNd2U3Q1BYRGQ2U01mSkwxRFZyUkw5Ylh0cEYzCjJkQVA5UENrakFJcFMvRE5jVkhLRXk1QW8yMnFFenpTKzlUT0JVY2srREdZcmo4KzI5U3h2aEZDRE5ZbEE2d1EKbEkyRWc5TWNBV2xDU3p1S1JWa2ZWUWdYVlU3SmE5OXp1Um1oWWtYZjFxQzBLcVAwQkpDakdDNEV6ZHorMmwyaAo2T3RxVHVVLzRkemlYYnRMUS8vU0JqNEgxdi9PZ3dUZjJkSVBjUnRHOXlWVTB2ZlQzVzdUTkdlMjU3em5ESDBYCkd6Wm4zdWJxTXJuL084b2ltMHRrS3ZHZXZ1V2ZraWNwVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==";
export const appKey = "GGtrKRWRknFdIID0rW";
export type LicenseVerifyReq = {
subjectId: string;
license: string;
@ -18,11 +18,15 @@ type License = {
duration: number;
version: number;
secret: string;
level: number;
signature: string;
};
class LicenseHolder {
isPlus = false;
expireTime = 0;
level = 1;
message?: string = undefined;
}
const holder = new LicenseHolder();
holder.isPlus = false;
@ -35,9 +39,20 @@ class LicenseVerifier {
return await this.verify(req);
}
setPlus(value: boolean) {
holder.isPlus = value;
return value;
setPlus(value: boolean, info: any = {}) {
if (value && !info) {
holder.isPlus = true;
holder.expireTime = info.expireTime;
holder.level = info.level;
} else {
holder.isPlus = false;
holder.expireTime = 0;
holder.level = 1;
holder.message = info.message;
}
return {
...holder,
};
}
async verify(req: LicenseVerifyReq) {
this.licenseReq = req;
@ -54,7 +69,7 @@ class LicenseVerifier {
const json: License = JSON.parse(licenseJson);
if (json.expireTime < Date.now()) {
logger.warn("授权已过期");
return this.setPlus(false);
return this.setPlus(false, { message: "授权已过期" });
}
const content = `${appKey},${this.licenseReq.subjectId},${json.code},${json.secret},${json.activeTime},${json.duration},${json.expireTime},${json.version}`;
const publicKey = Buffer.from(SecreteKey, "base64").toString();
@ -62,9 +77,12 @@ class LicenseVerifier {
this.checked = true;
if (!res) {
logger.warn("授权校验失败");
return this.setPlus(false);
return this.setPlus(false, { message: "授权校验失败" });
}
return this.setPlus(true);
return this.setPlus(true, {
expireTime: json.expireTime,
level: json.level || 1,
});
}
verifySignature(content: string, signature: any, publicKey: string) {
@ -80,6 +98,14 @@ export function isPlus() {
return holder.isPlus;
}
export function getPlusInfo() {
return {
isPlus: holder.isPlus,
level: holder.level,
expireTime: holder.expireTime,
};
}
export async function verify(req: LicenseVerifyReq) {
return await verifier.reVerify(req);
}

View File

@ -60,7 +60,14 @@ export async function mine(): Promise<UserInfoRes> {
});
}
return await request({
url: "/sys/authority/user/mine",
url: "/mine/info",
method: "post"
});
}
export async function getPlusInfo() {
return await request({
url: "/mine/plusInfo",
method: "post"
});
}

View File

@ -0,0 +1,9 @@
import { request } from "/@/api/service";
export async function doActive(form: any) {
return await request({
url: "/sys/plus/active",
method: "post",
data: form
});
}

View File

@ -0,0 +1,83 @@
<template>
<div class="layout-vip" :class="{ 'layout-plus': userStore.plusInfo?.isPlus }">
<contextHolder />
<fs-icon icon="mingcute:vip-1-line"></fs-icon>
<div class="text">
<span v-if="userStore.plusInfo?.isPlus">
<span>专业版</span>
<span>{{ expireTime }}</span>
</span>
<span v-else @click="openUpgrade"> </span>
</div>
</div>
</template>
<script lang="tsx" setup>
import { ref, reactive } from "vue";
import { useUserStore } from "/@/store/modules/user";
import dayjs from "dayjs";
import { message, Modal } from "ant-design-vue";
import * as api from "./api";
const userStore = useUserStore();
const expireTime = ref("");
if (userStore.plusInfo?.isPlus) {
expireTime.value = dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD");
}
const formState = reactive({
code: ""
});
async function doActive() {
if (!formState.code) {
message.error("请输入激活码");
throw new Error("请输入激活码");
}
const res = await api.doActive(formState);
if (res) {
await userStore.reInit();
Modal.success({
title: "激活成功",
content: `您已成功激活专业版,有效期至:${dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD")}`
});
}
}
const [modal, contextHolder] = Modal.useModal();
function openUpgrade() {
const placeholder = "请输入激活码";
modal.confirm({
title: "升级专业版",
async onOk() {
await doActive();
},
content: () => {
return (
<div class="mt-10 mb-10">
<a-input v-model:value={formState.code} placeholder={placeholder} />
<div class="mt-10">
<a href="https://afdian.com/a/greper" target="_blank">
爱发电赞助获取激活码
</a>
</div>
</div>
);
}
});
}
</script>
<style lang="less">
.layout-vip {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
&.isPlus {
color: #c5913f;
}
.text {
margin-left: 5px;
}
}
</style>

View File

@ -12,14 +12,14 @@
<a-layout class="layout-body">
<a-layout-header class="header">
<div class="header-buttons">
<div class="header-left header-buttons">
<div class="menu-fold" @click="asideCollapsedToggle">
<MenuUnfoldOutlined v-if="asideCollapsed" />
<MenuFoldOutlined v-else />
</div>
</div>
<fs-menu class="header-menu" mode="horizontal" :expand-selected="false" :selectable="false" :menus="frameworkMenus" />
<vip-info class="flex-center header-btn"></vip-info>
</div>
<div class="header-right header-buttons">
<!-- <button-->
<!-- w:bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"-->
@ -83,10 +83,11 @@ import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons-vue";
import FsThemeSet from "/@/layout/components/theme/index.vue";
import { env } from "../utils/util.env";
import FsThemeModeSet from "./components/theme/mode-set.vue";
import VipInfo from "./components/vip-info/index.vue";
export default {
name: "LayoutFramework",
// eslint-disable-next-line vue/no-unused-components
components: { FsThemeSet, MenuFoldOutlined, MenuUnfoldOutlined, FsMenu, FsLocale, FsSourceLink, FsUserInfo, FsTabs, FsThemeModeSet },
components: { FsThemeSet, MenuFoldOutlined, MenuUnfoldOutlined, FsMenu, FsLocale, FsSourceLink, FsUserInfo, FsTabs, FsThemeModeSet, VipInfo },
setup() {
const resourceStore = useResourceStore();
const frameworkMenus = computed(() => {
@ -133,6 +134,7 @@ export default {
.fs-framework {
height: 100%;
overflow-x: hidden;
min-width: 1200px;
.menu-fold {
display: flex;
justify-content: center;
@ -174,6 +176,11 @@ export default {
padding: 5px;
}
}
.ant-layout-header.header {
display: flex;
justify-content: space-between;
align-items: center;
.header-buttons {
display: flex;
align-items: center;
@ -202,6 +209,8 @@ export default {
.header-menu {
flex: 1;
}
}
.aside-menu {
flex: 1;
ui {

View File

@ -146,7 +146,6 @@ export default {
// position: absolute;
width: 100%;
bottom: 0;
padding: 0 16px;
margin: 48px 0 24px;
text-align: center;

View File

@ -33,6 +33,7 @@ router.beforeEach(async (to, from, next) => {
// 请根据自身业务需要修改
const token = userStore.getToken;
if (token) {
await userStore.init();
next();
} else {
// 没有登录的时候跳转到登录界面

View File

@ -1,21 +1,28 @@
import { defineStore } from "pinia";
import { store } from "../index";
import router from "../../router";
// @ts-ignore
import { LocalStorage } from "/src/utils/util.storage";
// @ts-ignore
import * as UserApi from "/src/api/modules/api.user";
import { RegisterReq } from "/src/api/modules/api.user";
// @ts-ignore
import { LoginReq, UserInfoRes } from "/@/api/modules/api.user";
import { Modal, notification } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { mitter } from "/src/utils/util.mitt";
import { RegisterReq } from "/src/api/modules/api.user";
interface UserState {
userInfo: Nullable<UserInfoRes>;
token?: string;
plusInfo?: PlusInfo;
inited: boolean;
}
interface PlusInfo {
level: number;
expireTime: number;
isPlus: boolean;
}
const USER_INFO_KEY = "USER_INFO";
@ -26,7 +33,10 @@ export const useUserStore = defineStore({
// user info
userInfo: null,
// token
token: undefined
token: undefined,
// plus
plusInfo: null,
inited: false
}),
getters: {
getUserInfo(): UserInfoRes {
@ -73,10 +83,7 @@ export const useUserStore = defineStore({
// save token
this.setToken(token, expire);
// get user info
const userInfo = await this.getUserInfoAction();
await router.replace("/");
mitter.emit("app.login", { userInfo, token: data });
return userInfo;
return await this.onLoginSuccess(data);
} catch (error) {
return null;
}
@ -86,6 +93,19 @@ export const useUserStore = defineStore({
this.setUserInfo(userInfo);
return userInfo;
},
async onLoginSuccess(loginData: any) {
await this.getUserInfoAction();
await this.loadPlusInfo();
const userInfo = await this.getUserInfoAction();
mitter.emit("app.login", { userInfo, token: loginData, plusInfo: this.plusInfo });
await router.replace("/");
return userInfo;
},
async loadPlusInfo() {
this.plusInfo = await UserApi.getPlusInfo();
},
/**
* @description: logout
*/
@ -108,6 +128,19 @@ export const useUserStore = defineStore({
await this.logout(true);
}
});
},
async init() {
if (this.inited) {
return;
}
if (this.getToken) {
await this.loadPlusInfo();
}
this.inited = true;
},
async reInit() {
this.inited = false;
await this.init();
}
}
});

View File

@ -14,9 +14,6 @@ html, body {
box-sizing: border-box;
}
body{
min-width: 1000px;
}
div#app {
height: 100%
}
@ -48,6 +45,11 @@ h1, h2, h3, h4, h5, h6 {
vertical-align: 0 !important;
}
.flex-center{
display: flex;
justify-content: center;
align-items: center;
}
.flex-o{
display: flex !important;
align-items: center;

View File

@ -1 +1,5 @@
alter table cd_access alter column setting type text using setting::text;
alter table sys_settings alter column setting type text using setting::text;
alter table user_settings alter column setting type text using setting::text;
alter table pi_history_log alter column logs type text using logs::text;
alter table pi_history alter column pipeline type text using pipeline::text;

View File

@ -31,6 +31,10 @@ export const Constants = {
code: 10,
message: '参数错误',
},
needvip: {
code: 88,
message: '需要VIP',
},
auth: {
code: 401,
message: '您还未登录或token已过期',

View File

@ -0,0 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
/**
* vip
*/
export class NeedVIPException extends BaseException {
constructor(message) {
super('NeedVIPException', Constants.res.needvip.code, message ? message : Constants.res.needvip.message);
}
}

View File

@ -8,7 +8,6 @@ import * as staticFile from '@midwayjs/static-file';
import * as cron from './modules/plugin/cron/index.js';
import * as flyway from '@certd/midway-flyway-js';
import cors from '@koa/cors';
import { ReportMiddleware } from './middleware/report.js';
import { GlobalExceptionMiddleware } from './middleware/global-exception.js';
import { PreviewMiddleware } from './middleware/preview.js';
import { AuthorityMiddleware } from './middleware/authority.js';
@ -60,7 +59,6 @@ export class MainConfiguration {
//this.app.use(bodyParser());
//请求日志打印
this.app.useMiddleware([
ReportMiddleware,
//统一异常处理
GlobalExceptionMiddleware,
//预览模式限制修改id<1000的数据

View File

@ -12,7 +12,7 @@ export class GlobalExceptionMiddleware implements IWebMiddleware {
logger.info('请求开始:', url);
try {
await next();
logger.info('请求完成', url, Date.now() - startTime + 'ms');
logger.info('请求完成:', url, Date.now() - startTime + 'ms');
} catch (err) {
logger.error('请求异常:', url, Date.now() - startTime + 'ms', err);
ctx.status = 200;

View File

@ -1,27 +0,0 @@
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
// 控制器前执行的逻辑
const startTime = Date.now();
// 执行下一个 Web 中间件,最后执行到控制器
// 这里可以拿到下一个中间件或者控制器的返回值
const result = await next();
// 控制器之后执行的逻辑
ctx.logger.info(
`Report in "src/middleware/report.middleware.ts", rt = ${
Date.now() - startTime
}ms`
);
// 返回给上一个中间件的结果
return result;
};
}
static getName(): string {
return 'report';
}
}

View File

@ -1,24 +0,0 @@
import { Provide } from '@midwayjs/core';
import { IWebMiddleware, IMidwayKoaContext, NextFunction } from '@midwayjs/koa';
import { logger } from '../utils/logger.js';
@Provide()
export class ReportMiddleware implements IWebMiddleware {
resolve() {
return async (ctx: IMidwayKoaContext, next: NextFunction) => {
const { url } = ctx;
logger.info('请求开始:', url);
const startTime = Date.now();
await next();
if (ctx.status !== 200) {
logger.error(
'请求失败:',
url,
ctx.status,
Date.now() - startTime + 'ms'
);
}
logger.info('请求完成:', url, ctx.status, Date.now() - startTime + 'ms');
};
}
}

View File

@ -2,6 +2,7 @@ import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { BaseController } from '../../../basic/base-controller.js';
import { Constants } from '../../../basic/constants.js';
import { UserService } from '../../authority/service/user-service.js';
import { getPlusInfo } from '@certd/pipeline';
/**
*/
@ -24,4 +25,12 @@ export class MineController extends BaseController {
await this.userService.changePassword(userId, body);
return this.ok({});
}
@Post('/plusInfo', { summary: Constants.per.authOnly })
async plusInfo(@Body(ALL) body) {
const info = getPlusInfo();
return this.ok({
...info,
});
}
}

View File

@ -4,7 +4,7 @@ import { In, Repository } from 'typeorm';
import { BaseService } from '../../../basic/base-service.js';
import { PipelineEntity } from '../entity/pipeline.js';
import { PipelineDetail } from '../entity/vo/pipeline-detail.js';
import { Executor, Pipeline, ResultType, RunHistory } from '@certd/pipeline';
import { Executor, isPlus, Pipeline, ResultType, RunHistory } from '@certd/pipeline';
import { AccessService } from './access-service.js';
import { DbStorage } from './db-storage.js';
import { StorageService } from './storage-service.js';
@ -15,9 +15,10 @@ import { HistoryLogEntity } from '../entity/history-log.js';
import { HistoryLogService } from './history-log-service.js';
import { logger } from '../../../utils/logger.js';
import { EmailService } from '../../basic/service/email-service.js';
import { NeedVIPException } from '../../../basic/exception/vip-exception.js';
const runningTasks: Map<string | number, Executor> = new Map();
const freeCount = 10;
/**
*
*/
@ -47,6 +48,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
return this.repository;
}
async add(bean: PipelineEntity) {
if (!isPlus()) {
const count = await this.repository.count();
if (count >= freeCount) {
throw new NeedVIPException('免费版最多只能创建10个pipeline');
}
}
await super.add(bean);
return bean;
}
async page(query: any, page: { offset: number; limit: number }, order: any, buildQuery: any) {
const result = await super.page(query, page, order, buildQuery);
const pipelineIds: number[] = [];
@ -93,6 +105,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
async save(bean: PipelineEntity) {
if (!isPlus()) {
const count = await this.repository.count();
if (count >= 10) {
throw new NeedVIPException('免费版最多只能创建10个pipeline');
}
}
await this.clearTriggers(bean.id);
if (bean.content) {
const pipeline = JSON.parse(bean.content);

View File

@ -0,0 +1,57 @@
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { SysSettingsService } from '../service/sys-settings-service.js';
import { BaseController } from '../../../basic/base-controller.js';
import { appKey, utils, verify } from '@certd/pipeline';
import { SysInstallInfo, SysLicenseInfo } from '../service/models.js';
import { logger } from '../../../utils/logger.js';
/**
*/
@Provide()
@Controller('/api/sys/plus')
export class SysPlusController extends BaseController {
@Inject()
sysSettingsService: SysSettingsService;
@Post('/active', { summary: 'sys:settings:edit' })
async active(@Body(ALL) body) {
const { code } = body;
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
const formData = {
appKey: appKey,
code,
subjectId: installInfo.siteId,
};
const res: any = await utils.http({
url: 'https://api.ai.handsfree.work/activation/active',
method: 'post',
data: formData,
});
if (res.code > 0) {
logger.error('激活失败', res.message);
return this.fail(res.message, 1);
}
const license = res.data.license;
let licenseInfo: SysLicenseInfo = await this.sysSettingsService.getSetting(SysLicenseInfo);
if (!licenseInfo) {
licenseInfo = new SysLicenseInfo();
}
licenseInfo.license = license;
await this.sysSettingsService.saveSetting(licenseInfo);
const verifyRes = await verify({
subjectId: installInfo.siteId,
license,
});
if (!verifyRes.isPlus) {
const message = verifyRes.message || '授权码校验失败';
logger.error(message);
return this.fail(message, 1);
}
return this.ok(res.data);
}
}

View File

@ -0,0 +1 @@
export * from './plugins/index.js';

View File

@ -0,0 +1 @@
export * from './plugin-k8s.js';

View File

@ -0,0 +1,109 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from '@certd/pipeline';
import { CertInfo } from '@certd/plugin-cert';
import { K8sClient } from '@certd/lib-k8s';
import { K8sAccess } from '../access/index.js';
import { appendTimeSuffix } from '../../plugin-aliyun/utils/index.js';
@IsTaskPlugin({
name: 'DeployToK8SIngress',
title: 'K8S Ingress证书部署',
group: pluginGroups.other.key,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class K8STestPlugin extends AbstractTaskPlugin {
@TaskInput({
title: '命名空间',
value: 'default',
component: {
placeholder: '命名空间',
},
required: true,
})
namespace!: string;
@TaskInput({
title: 'ingress名称',
value: '',
component: {
placeholder: 'ingress名称',
},
required: true,
helper: '可以传入一个数组',
})
ingressName!: string;
@TaskInput({
title: '保密字典Id',
component: {
placeholder: '保密字典Id',
},
required: true,
})
secretName!: string | string[];
@TaskInput({
title: 'k8s授权',
helper: 'kubeconfig',
component: {
name: 'pi-access-selector',
type: 'k8s',
},
required: true,
})
accessId!: string;
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'pi-output-selector',
},
required: true,
})
cert!: CertInfo;
async onInstance() {}
async execute(): Promise<void> {
const access: K8sAccess = await this.accessService.getById(this.accessId);
const k8sClient = new K8sClient({
kubeConfigStr: access.kubeconfig,
logger: this.logger,
});
await this.patchNginxCertSecret({ cert: this.cert, k8sClient });
await utils.sleep(3000); // 停留2秒等待secret部署完成
}
async patchNginxCertSecret(options: { cert: CertInfo; k8sClient: K8sClient }) {
const { cert, k8sClient } = options;
const crt = cert.crt;
const key = cert.key;
const crtBase64 = Buffer.from(crt).toString('base64');
const keyBase64 = Buffer.from(key).toString('base64');
const { namespace, secretName } = this;
const body: any = {
data: {
'tls.crt': crtBase64,
'tls.key': keyBase64,
},
metadata: {
labels: {
certd: appendTimeSuffix('certd'),
},
},
};
let secretNames: any = secretName;
if (typeof secretName === 'string') {
secretNames = [secretName];
}
for (const secret of secretNames) {
await k8sClient.patchSecret({ namespace, secretName: secret, body });
this.logger.info(`ingress cert Secret已更新:${secret}`);
}
}
}
new K8STestPlugin();