mirror of
https://github.com/certd/certd.git
synced 2026-04-20 17:57:41 +08:00
Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev
This commit is contained in:
@@ -67,8 +67,9 @@
|
||||
| 63.| **猫云授权** | |
|
||||
| 64.| **EAB授权** | ZeroSSL证书申请需要EAB授权 |
|
||||
| 65.| **google cloud** | 谷歌云授权 |
|
||||
| 66.| **雨云授权** | https://app.rainyun.com/ |
|
||||
| 67.| **GoEdge授权** | |
|
||||
| 66.| **Zenlayer授权** | Zenlayer授权 |
|
||||
| 67.| **雨云授权** | https://app.rainyun.com/ |
|
||||
| 68.| **GoEdge授权** | |
|
||||
|
||||
<style module>
|
||||
table th:first-of-type {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 任务插件
|
||||
共 `109` 款任务插件
|
||||
共 `111` 款任务插件
|
||||
## 1. 证书申请
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
@@ -45,6 +45,7 @@
|
||||
| 21.| **白山云-更新证书** | |
|
||||
| 22.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
|
||||
| 23.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 24.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
|
||||
## 4. 面板
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
@@ -63,10 +64,11 @@
|
||||
| 12.| **K8S-Ingress 证书部署** | 部署证书到k8s的Ingress |
|
||||
| 13.| **K8S-Apply自定义yaml** | apply自定义yaml到k8s |
|
||||
| 14.| **1Panel-部署证书到1Panel** | 更新1Panel的证书 |
|
||||
| 15.| **Plesk-部署Plesk网站证书** | |
|
||||
| 16.| **雷池-更新证书** | 更新长亭雷池WAF的证书 |
|
||||
| 17.| **lucky-更新Lucky证书** | |
|
||||
| 18.| **uniCloud-部署到服务空间** | 部署到服务空间 |
|
||||
| 15.| **1Panel-部署面板证书** | 更新1Panel的面板证书 |
|
||||
| 16.| **Plesk-部署Plesk网站证书** | |
|
||||
| 17.| **雷池-更新证书** | 更新长亭雷池WAF的证书 |
|
||||
| 18.| **lucky-更新Lucky证书** | |
|
||||
| 19.| **uniCloud-部署到服务空间** | 部署到服务空间 |
|
||||
## 5. 阿里云
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
|
||||
@@ -11,6 +11,10 @@ function hmacSha256(data: string, digest: BinaryToTextEncoding = "base64") {
|
||||
return crypto.createHmac("sha256", data).update(Buffer.alloc(0)).digest(digest);
|
||||
}
|
||||
|
||||
function hmacSha256WithKey(key: string, data: string, digest: BinaryToTextEncoding = "base64") {
|
||||
return crypto.createHmac("sha256", key).update(data).digest(digest);
|
||||
}
|
||||
|
||||
function base64(data: string) {
|
||||
return Buffer.from(data).toString("base64");
|
||||
}
|
||||
@@ -39,4 +43,5 @@ export const hashUtils = {
|
||||
toHex,
|
||||
hexToStr,
|
||||
hexToNumber,
|
||||
hmacSha256WithKey,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ export class SysPublicSettings extends BaseSettings {
|
||||
usernameRegisterEnabled = true;
|
||||
mobileRegisterEnabled = false;
|
||||
smsLoginEnabled = false;
|
||||
useSmsLoginDefault = true;
|
||||
emailRegisterEnabled = false;
|
||||
selfServicePasswordRetrievalEnabled = false;
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ export default {
|
||||
alignLeft: "Align Left",
|
||||
alignRight: "Align Right",
|
||||
},
|
||||
loginType: {
|
||||
password: "Password Login",
|
||||
sms: "Mobile Login",
|
||||
},
|
||||
usernamePlaceholder: "Please enter username/email/phone number",
|
||||
passwordPlaceholder: "Please enter your password",
|
||||
mobilePlaceholder: "Please enter your mobile number",
|
||||
|
||||
@@ -53,6 +53,10 @@ export default {
|
||||
alignLeft: "居左",
|
||||
alignRight: "居右",
|
||||
},
|
||||
loginType: {
|
||||
password: "密码登录",
|
||||
sms: "手机号登录",
|
||||
},
|
||||
usernamePlaceholder: "请输入用户名/邮箱/手机号",
|
||||
passwordPlaceholder: "请输入密码",
|
||||
mobilePlaceholder: "请输入手机号",
|
||||
|
||||
@@ -620,6 +620,7 @@ export default {
|
||||
proFeature: "专业版功能",
|
||||
emailServerSetup: "设置邮箱服务器",
|
||||
enableSmsLoginRegister: "开启手机号登录、注册",
|
||||
defaultLoginType: "默认登录方式",
|
||||
commFeature: "商业版功能",
|
||||
smsProvider: "短信提供商",
|
||||
aliyunSms: "阿里云短信",
|
||||
|
||||
@@ -36,6 +36,7 @@ export type SysPublicSetting = {
|
||||
emailRegisterEnabled?: boolean;
|
||||
passwordLoginEnabled?: boolean;
|
||||
smsLoginEnabled?: boolean;
|
||||
defaultLoginType?: string;
|
||||
selfServicePasswordRetrievalEnabled?: boolean;
|
||||
|
||||
limitUserPipelineCount?: number;
|
||||
|
||||
@@ -4,25 +4,6 @@
|
||||
<!-- <div class="login-title">登录</div>-->
|
||||
<template v-if="!isOauthOnly">
|
||||
<a-tabs v-model:active-key="formState.loginType" :tab-bar-style="{ textAlign: 'center', borderBottom: 'unset' }">
|
||||
<a-tab-pane v-if="sysPublicSettings.smsLoginEnabled === true" key="sms" :tab="t('authentication.smsTab')">
|
||||
<template v-if="formState.loginType === 'sms'">
|
||||
<a-form-item has-feedback name="mobile" :rules="rules.mobile">
|
||||
<a-input v-model:value="formState.mobile" :placeholder="t('authentication.mobilePlaceholder')" autocomplete="off">
|
||||
<template #prefix>
|
||||
<fs-icon icon="ion:phone-portrait-outline"></fs-icon>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item has-feedback name="smsCaptcha">
|
||||
<CaptchaInput v-model:model-value="formState.smsCaptcha" @keydown.enter="handleFinish"></CaptchaInput>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="smsCode" :rules="rules.smsCode">
|
||||
<sms-code v-model:value="formState.smsCode" :captcha="formState.smsCaptcha" :mobile="formState.mobile" :phone-code="formState.phoneCode" @error="formState.smsCaptcha = null" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="password" :tab="t('authentication.passwordTab')" :disabled="sysPublicSettings.passwordLoginEnabled !== true">
|
||||
<template v-if="formState.loginType === 'password'">
|
||||
<!-- <div class="login-title">登录</div>-->
|
||||
@@ -46,6 +27,25 @@
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane v-if="sysPublicSettings.smsLoginEnabled === true" key="sms" :tab="t('authentication.smsTab')">
|
||||
<template v-if="formState.loginType === 'sms'">
|
||||
<a-form-item has-feedback name="mobile" :rules="rules.mobile">
|
||||
<a-input v-model:value="formState.mobile" :placeholder="t('authentication.mobilePlaceholder')" autocomplete="off">
|
||||
<template #prefix>
|
||||
<fs-icon icon="ion:phone-portrait-outline"></fs-icon>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item has-feedback name="smsCaptcha">
|
||||
<CaptchaInput v-model:model-value="formState.smsCaptcha" @keydown.enter="handleFinish"></CaptchaInput>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="smsCode" :rules="rules.smsCode">
|
||||
<sms-code v-model:value="formState.smsCode" :captcha="formState.smsCaptcha" :mobile="formState.mobile" :phone-code="formState.phoneCode" @error="formState.smsCaptcha = null" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<a-form-item>
|
||||
<a-button type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish">
|
||||
@@ -120,12 +120,18 @@ export default defineComponent({
|
||||
const userStore = useUserStore();
|
||||
const settingStore = useSettingStore();
|
||||
const formRef = ref();
|
||||
let defaultLoginType = settingStore.sysPublic.defaultLoginType || "password";
|
||||
if (defaultLoginType === "sms") {
|
||||
if (!settingStore.sysPublic.smsLoginEnabled || !settingStore.isComm) {
|
||||
defaultLoginType = "password";
|
||||
}
|
||||
}
|
||||
const formState = reactive({
|
||||
username: "",
|
||||
phoneCode: "86",
|
||||
mobile: "",
|
||||
password: "",
|
||||
loginType: urlLoginType || settingStore.sysPublic.smsLoginEnabled ? "sms" : "password", //password
|
||||
loginType: urlLoginType || defaultLoginType, //password
|
||||
smsCode: "",
|
||||
captcha: null,
|
||||
smsCaptcha: null,
|
||||
|
||||
@@ -55,7 +55,7 @@ async function handleOauthToken() {
|
||||
//需要绑定
|
||||
bindCode.value = res.validationCode;
|
||||
//如果开启了自动注册,默认自动注册账号
|
||||
if (settingStore.sysPublic.registerEnabled) {
|
||||
if (settingStore.sysPublic.oauthAutoRegister) {
|
||||
autoRegister();
|
||||
} else {
|
||||
bindRequired.value = true;
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
<router-link to="/sys/settings/email">{{ t("certd.emailServerSetup") }}</router-link>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.defaultLoginType')" :name="['public', 'defaultLoginType']" required>
|
||||
<div class="flex-o">
|
||||
<a-radio-group v-model:value="formState.public.defaultLoginType" :options="loginTypeOptions" :title="t('certd.commFeature')" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.enableSmsLoginRegister')" :name="['public', 'smsLoginEnabled']">
|
||||
<div class="flex-o">
|
||||
<a-switch v-model:checked="formState.public.smsLoginEnabled" :disabled="!settingsStore.isComm" :title="t('certd.commFeature')" />
|
||||
@@ -65,7 +70,7 @@
|
||||
<script setup lang="tsx">
|
||||
import { notification } from "ant-design-vue";
|
||||
import { merge } from "lodash-es";
|
||||
import { reactive, ref, Ref } from "vue";
|
||||
import { computed, reactive, ref, Ref } from "vue";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import * as api from "/@/views/sys/settings/api";
|
||||
import { SysSettings } from "/@/views/sys/settings/api";
|
||||
@@ -187,6 +192,18 @@ const onFinish = async (form: any) => {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loginTypeOptions = computed(() => [
|
||||
{
|
||||
label: t("authentication.loginType.password"),
|
||||
value: "password",
|
||||
},
|
||||
{
|
||||
label: t("authentication.loginType.sms"),
|
||||
value: "sms",
|
||||
disabled: !formState.public.smsLoginEnabled,
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<style lang="less">
|
||||
.sys-settings-register {
|
||||
|
||||
@@ -15,19 +15,21 @@ input:
|
||||
required: true
|
||||
encrypt: false
|
||||
publicKey:
|
||||
title: 公钥
|
||||
title: 证书公钥
|
||||
component:
|
||||
name: a-textarea
|
||||
rows: 3
|
||||
placeholder: MIIBIjANBg...
|
||||
placeholder: '-----BEGIN CERTIFICATE-----'
|
||||
helper: 微信商户平台—>账户设置—>API安全—>验证商户身份—>商户API证书—>管理证书—>apiclient_cert.pem
|
||||
required: true
|
||||
encrypt: true
|
||||
privateKey:
|
||||
title: 私钥
|
||||
component:
|
||||
placeholder: MIIEvQIBADANB...
|
||||
placeholder: '-----BEGIN PRIVATE KEY-----'
|
||||
name: a-textarea
|
||||
rows: 3
|
||||
helper: 证书私钥 apiclient_key.pem
|
||||
required: true
|
||||
encrypt: true
|
||||
key:
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: zenlayer
|
||||
title: Zenlayer授权
|
||||
icon: svg:icon-lucky
|
||||
desc: Zenlayer授权
|
||||
input:
|
||||
accessKeyId:
|
||||
title: AccessKeyId
|
||||
component:
|
||||
placeholder: 访问密钥ID
|
||||
helper: '[访问密钥管理](https://console.zenlayer.com/accessKey)获取'
|
||||
required: true
|
||||
encrypt: false
|
||||
accessKeyPassword:
|
||||
title: AccessKey Password
|
||||
component:
|
||||
placeholder: 访问密钥密码
|
||||
required: true
|
||||
encrypt: true
|
||||
testRequest:
|
||||
title: 测试
|
||||
component:
|
||||
name: api-test
|
||||
action: TestRequest
|
||||
helper: 点击测试接口是否正常
|
||||
pluginType: access
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-zenlayer/access.js
|
||||
@@ -0,0 +1,77 @@
|
||||
showRunStrategy: false
|
||||
default:
|
||||
strategy:
|
||||
runStrategy: 1
|
||||
name: 1PanelDeployToPanel
|
||||
title: 1Panel-部署面板证书
|
||||
icon: svg:icon-onepanel
|
||||
desc: 更新1Panel的面板证书
|
||||
group: panel
|
||||
needPlus: false
|
||||
input:
|
||||
cert:
|
||||
title: 域名证书
|
||||
helper: 请选择前置任务输出的域名证书
|
||||
component:
|
||||
name: output-selector
|
||||
from:
|
||||
- ':cert:'
|
||||
required: true
|
||||
order: 0
|
||||
certDomains:
|
||||
title: 当前证书域名
|
||||
component:
|
||||
name: cert-domains-getter
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
component:{
|
||||
inputKey: ctx.compute(({form})=>{
|
||||
return form.cert
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
template: false
|
||||
required: true
|
||||
order: 0
|
||||
accessId:
|
||||
title: 1Panel授权
|
||||
helper: 1Panel授权
|
||||
component:
|
||||
name: access-selector
|
||||
type: 1panel
|
||||
required: true
|
||||
order: 0
|
||||
currentNode:
|
||||
title: 1Panel节点
|
||||
component:
|
||||
name: remote-select
|
||||
vModel: value
|
||||
mode: tags
|
||||
type: plugin
|
||||
typeName: OnePanelDeployToPanelPlugin
|
||||
action: onGetNodes
|
||||
search: false
|
||||
pager: false
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
value: local
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
component:{
|
||||
form: ctx.compute(({form})=>{
|
||||
return form
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
helper: 要更新的1Panel证书的节点信息,目前只有v2存在此概念
|
||||
order: 0
|
||||
output: {}
|
||||
pluginType: deploy
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-plus/1panel/plugins/deploy-to-panel.js
|
||||
@@ -0,0 +1,73 @@
|
||||
showRunStrategy: false
|
||||
default:
|
||||
strategy:
|
||||
runStrategy: 1
|
||||
name: ZenlayerRefreshCert
|
||||
title: Zenlayer-刷新证书
|
||||
desc: 刷新Zenlayer CDN证书
|
||||
icon: svg:icon-lucky
|
||||
group: cdn
|
||||
needPlus: false
|
||||
input:
|
||||
cert:
|
||||
title: 域名证书
|
||||
helper: 请选择前置任务输出的域名证书
|
||||
component:
|
||||
name: output-selector
|
||||
from:
|
||||
- ':cert:'
|
||||
order: 0
|
||||
certDomains:
|
||||
title: 当前证书域名
|
||||
component:
|
||||
name: cert-domains-getter
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
component:{
|
||||
inputKey: ctx.compute(({form})=>{
|
||||
return form.cert
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
template: false
|
||||
required: false
|
||||
order: 0
|
||||
accessId:
|
||||
title: Zenlayer授权
|
||||
component:
|
||||
name: access-selector
|
||||
type: zenlayer
|
||||
required: true
|
||||
order: 0
|
||||
certList:
|
||||
title: 证书ID列表
|
||||
component:
|
||||
name: remote-select
|
||||
vModel: value
|
||||
mode: tags
|
||||
type: plugin
|
||||
action: onGetCertList
|
||||
search: false
|
||||
pager: false
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
component:{
|
||||
form: ctx.compute(({form})=>{
|
||||
return form
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
helper: 要更新的Zenlayer证书ID列表
|
||||
order: 0
|
||||
output: {}
|
||||
pluginType: deploy
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-zenlayer/plugins/plugin-refresh-cert.js
|
||||
@@ -130,7 +130,9 @@
|
||||
"@google-cloud/publicca": "^1.3.0",
|
||||
"jsrsasign": "^11.1.0",
|
||||
"ssh2": "^1.17.0",
|
||||
"@alicloud/openapi-util": "^0.3.2"
|
||||
"@alicloud/openapi-util": "^0.3.2",
|
||||
"wechatpay-node-v3": "^2.2.1",
|
||||
"alipay-sdk": "^4.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@midwayjs/mock": "3.20.11",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { logger } from "@certd/basic";
|
||||
import { PluginService } from "../plugin/service/plugin-service.js";
|
||||
import { registerPaymentProviders } from "../suite/payments/index.js";
|
||||
|
||||
@Autoload()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
@@ -19,6 +20,8 @@ export class AutoBLoadPlugins {
|
||||
}
|
||||
// await import("../../plugins/index.js")
|
||||
await this.pluginService.registerFromDb()
|
||||
|
||||
await registerPaymentProviders();
|
||||
logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { IPaymentProvider, TradeEntity, UpdateTrade, UpdateTradeInfo } from "@certd/commercial-core";
|
||||
import dayjs from "dayjs";
|
||||
import { logger, utils } from "@certd/basic";
|
||||
import { AlipayAccess } from "../../../plugins/plugin-plus/alipay/access.js";
|
||||
|
||||
export class PaymentAlipay implements IPaymentProvider {
|
||||
access: AlipayAccess;
|
||||
|
||||
constructor(access: AlipayAccess) {
|
||||
this.access = access;
|
||||
}
|
||||
|
||||
async getDetail(tradeNo: string): Promise<UpdateTradeInfo> {
|
||||
const alipaySdk = await this.createAlipaySdk();
|
||||
|
||||
const result: any = await alipaySdk.curl("POST", "/v3/alipay/trade/query", {
|
||||
body: {
|
||||
out_trade_no: tradeNo,
|
||||
},
|
||||
});
|
||||
logger.info("获取支付宝订单详情", JSON.stringify(result));
|
||||
if (result.responseHttpStatus !== 200) {
|
||||
throw new Error("请求支付宝失败:" + result.responseHttpStatus);
|
||||
}
|
||||
const data = result.data;
|
||||
|
||||
let status: string = undefined;
|
||||
let payTime: number = undefined;
|
||||
if (data.trade_status === "TRADE_SUCCESS") {
|
||||
status = "paid";
|
||||
payTime = dayjs(data.send_pay_date).valueOf();
|
||||
} else if (data.trade_status === "TRADE_CLOSED") {
|
||||
status = "closed";
|
||||
} else {
|
||||
logger.info("支付宝订单状态为:" + data.trade_status);
|
||||
}
|
||||
|
||||
return {
|
||||
tradeNo,
|
||||
status: status,
|
||||
amount: utils.amount.toCent(parseFloat(data.total_amount)),
|
||||
payNo: data.trade_no,
|
||||
payTime: payTime,
|
||||
};
|
||||
}
|
||||
|
||||
async createOrder(trade: TradeEntity, opts: { bindUrl: string; clientIp: string }) {
|
||||
const return_url = `${opts.bindUrl}/#/certd/payment/return/alipay`;
|
||||
const notify_url = `${opts.bindUrl}/api/payment/notify/alipay`;
|
||||
|
||||
const alipaySdk = await this.createAlipaySdk();
|
||||
const url = alipaySdk.pageExec("alipay.trade.page.pay", "GET", {
|
||||
return_url,
|
||||
notify_url,
|
||||
bizContent: {
|
||||
out_trade_no: trade.tradeNo,
|
||||
total_amount: utils.amount.toYuan(trade.amount),
|
||||
subject: trade.title,
|
||||
product_code: "FAST_INSTANT_TRADE_PAY",
|
||||
// qr_pay_mode: "1",
|
||||
// qrcode_width: "100",
|
||||
// time_expire: "2016-12-31+10:05:01",
|
||||
// sub_merchant: {
|
||||
// merchant_id: "2088000603999128",
|
||||
// merchant_type: "alipay",
|
||||
// },
|
||||
// extend_params: {
|
||||
// sys_service_provider_id: "2088511833207846",
|
||||
// hb_fq_seller_percent: "100",
|
||||
// hb_fq_num: "3",
|
||||
// industry_reflux_info: '{\\"scene_code\\":\\"metro_tradeorder\\",\\"channel\\":\\"xxxx\\",\\"scene_data\\":{\\"asset_name\\":\\"ALIPAY\\"}}',
|
||||
// specified_seller_name: "XXX的跨境小铺",
|
||||
// royalty_freeze: "true",
|
||||
// card_type: "S0JP0000",
|
||||
// },
|
||||
// business_params: '{"mc_create_trade_ip":"127.0.0.1"}',
|
||||
// promo_params: '{"storeIdType":"1"}',
|
||||
// integration_type: "PCWEB",
|
||||
// request_from_url: "https://",
|
||||
// store_id: "NJ_001",
|
||||
// merchant_order_no: "20161008001",
|
||||
// ext_user_info: {
|
||||
// cert_type: "IDENTITY_CARD",
|
||||
// cert_no: "362334768769238881",
|
||||
// name: "李明",
|
||||
// mobile: "16587658765",
|
||||
// min_age: "18",
|
||||
// need_check_info: "F",
|
||||
// identity_hash: "27bfcd1dee4f22c8fe8a2374af9b660419d1361b1c207e9b41a754a113f38fcc",
|
||||
// },
|
||||
// invoice_info: {
|
||||
// key_info: {
|
||||
// tax_num: "1464888883494",
|
||||
// is_support_invoice: "true",
|
||||
// invoice_merchant_name: "ABC|003",
|
||||
// },
|
||||
// details: '[{"code":"100294400","name":"服饰","num":"2","sumPrice":"200.00","taxRate":"6%"}]',
|
||||
// },
|
||||
},
|
||||
});
|
||||
return {
|
||||
url,
|
||||
body: {},
|
||||
};
|
||||
}
|
||||
|
||||
private async createAlipaySdk() {
|
||||
const AlipaySdk = await import("alipay-sdk");
|
||||
|
||||
const alipaySdk = new AlipaySdk.AlipaySdk({
|
||||
appId: this.access.appId,
|
||||
privateKey: this.access.privateKey,
|
||||
alipayPublicKey: this.access.alipayPublicKey,
|
||||
gateway: "https://openapi.alipay.com/gateway.do",
|
||||
});
|
||||
return alipaySdk;
|
||||
}
|
||||
|
||||
async onNotify(data: any, updateTrade: UpdateTrade) {
|
||||
const alipaySdk = await this.createAlipaySdk();
|
||||
logger.info(`支付宝notify:${JSON.stringify(data)}`);
|
||||
// true | false
|
||||
let success = alipaySdk.checkNotifySign(data);
|
||||
if (!success) {
|
||||
success = alipaySdk.checkNotifySignV2(data);
|
||||
if (!success) {
|
||||
throw new Error("签名验证失败");
|
||||
}
|
||||
}
|
||||
if (data.trade_status === "TRADE_SUCCESS") {
|
||||
await updateTrade({
|
||||
tradeNo: data.out_trade_no,
|
||||
status: "paid",
|
||||
amount: utils.amount.toCent(parseFloat(data.total_amount)),
|
||||
payNo: data.trade_no,
|
||||
payTime: dayjs().valueOf(),
|
||||
});
|
||||
} else if (data.trade_status === "TRADE_CLOSED") {
|
||||
await updateTrade({
|
||||
tradeNo: data.out_trade_no,
|
||||
status: "closed",
|
||||
payNo: data.trade_no,
|
||||
});
|
||||
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {paymentProviderFactory} from "@certd/commercial-core"
|
||||
export function registerPaymentProviders() {
|
||||
|
||||
paymentProviderFactory.registerProvider("alipay", async () => (await import("./alipay.js")).PaymentAlipay);
|
||||
paymentProviderFactory.registerProvider("wxpay", async () => (await import("./wxpay.js")).PaymentWxpay);
|
||||
paymentProviderFactory.registerProvider("yizhifu", async () => (await import("./yizhifu.js")).PaymentYizhifu);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { IPaymentProvider, TradeEntity, UpdateTrade, UpdateTradeInfo } from "@certd/commercial-core";
|
||||
import WxPay from "wechatpay-node-v3";
|
||||
import dayjs from "dayjs";
|
||||
import { logger } from "@certd/basic"; // 支持使用require
|
||||
import { WxpayAccess } from "../../../plugins/plugin-plus/wxpay/access.js";
|
||||
export class PaymentWxpay implements IPaymentProvider {
|
||||
access: WxpayAccess;
|
||||
constructor(access: WxpayAccess) {
|
||||
this.access = access;
|
||||
}
|
||||
async getDetail(tradeNo: string): Promise<UpdateTradeInfo> {
|
||||
/**
|
||||
* const result = await pay.query({out_trade_no: '1609914303237'});
|
||||
* # 或者 const result = await pay.query({transaction_id: ''});
|
||||
* console.log(result);
|
||||
* {
|
||||
* status: 200,
|
||||
* appid: 'appid',
|
||||
* attach: '',
|
||||
* mchid: '商户号',
|
||||
* out_trade_no: '1609899981750',
|
||||
* payer: {},
|
||||
* promotion_detail: [],
|
||||
* trade_state: 'CLOSED',
|
||||
* trade_state_desc: '订单已关闭'
|
||||
* }
|
||||
*/
|
||||
|
||||
const pay = this.createSdk();
|
||||
|
||||
const result: any = await pay.query({ out_trade_no: tradeNo });
|
||||
logger.info(`微信支付查询订单返回:${JSON.stringify(result)}`);
|
||||
if (result.status !== 200) {
|
||||
throw new Error("查询微信支付订单失败:" + result.status);
|
||||
}
|
||||
const data = result.data;
|
||||
let status: string = undefined;
|
||||
let payTime: number = undefined;
|
||||
let amount: number = undefined;
|
||||
if (data.trade_state === "SUCCESS") {
|
||||
status = "paid";
|
||||
payTime = dayjs(data.success_time).valueOf();
|
||||
amount = data.amount.total;
|
||||
} else if (data.trade_state === "CLOSED") {
|
||||
status = "closed";
|
||||
} else {
|
||||
logger.info("微信支付订单状态为:" + data.trade_state);
|
||||
}
|
||||
|
||||
return {
|
||||
tradeNo: data.out_trade_no,
|
||||
status,
|
||||
amount,
|
||||
payNo: data.transaction_id,
|
||||
payTime,
|
||||
};
|
||||
}
|
||||
async createOrder(trade: TradeEntity, opts: { bindUrl: string; clientIp: string }) {
|
||||
const notify_url = `${opts.bindUrl}/api/payment/notify/wxpay`;
|
||||
|
||||
const pay = this.createSdk();
|
||||
|
||||
const params = {
|
||||
description: trade.title,
|
||||
out_trade_no: trade.tradeNo,
|
||||
notify_url,
|
||||
amount: {
|
||||
total: trade.amount,
|
||||
},
|
||||
scene_info: {
|
||||
payer_client_ip: "ip",
|
||||
},
|
||||
};
|
||||
|
||||
logger.info(`微信支付下单请求:${JSON.stringify(params)}`);
|
||||
const result: any = await pay.transactions_native(params);
|
||||
logger.info(`微信支付下单返回:${JSON.stringify(result)}`);
|
||||
if (result.status !== 200) {
|
||||
throw new Error("请求微信支付失败:" + result.status);
|
||||
}
|
||||
return {
|
||||
qrcode: result.data.code_url,
|
||||
};
|
||||
}
|
||||
|
||||
private createSdk() {
|
||||
const pay = new WxPay({
|
||||
appid: this.access.appId,
|
||||
mchid: this.access.mchid,
|
||||
publicKey: Buffer.from(this.access.publicKey), // 公钥
|
||||
privateKey: Buffer.from(this.access.privateKey), // 秘钥
|
||||
});
|
||||
return pay;
|
||||
}
|
||||
|
||||
async onNotify(notifyData: any, updateTrade: UpdateTrade) {
|
||||
const pay = this.createSdk();
|
||||
const { ciphertext, associated_data, nonce } = notifyData.resource;
|
||||
logger.info(`微信支付notify:${JSON.stringify(notifyData)}`);
|
||||
const key = this.access.key;
|
||||
const result: any = pay.decipher_gcm(ciphertext, associated_data, nonce, key);
|
||||
logger.info(`微信支付解析结果:${JSON.stringify(result)}`);
|
||||
/**
|
||||
* mchid: '商户号',
|
||||
* # appid: 'appid',
|
||||
* # out_trade_no: '1610419296553',
|
||||
* # transaction_id: '4200000848202101120290526543',
|
||||
* # trade_type: 'NATIVE',
|
||||
* # trade_state: 'SUCCESS',
|
||||
* # trade_state_desc: '支付成功',
|
||||
* # bank_type: 'OTHERS',
|
||||
* # attach: '',
|
||||
* # success_time: '2021-01-12T10:43:43+08:00',
|
||||
* # payer: { openid: '' },
|
||||
* # amount: { total: 1, payer_total: 1, currency: 'CNY', payer_currency: 'CNY' }
|
||||
*/
|
||||
const data: any = result;
|
||||
if (data.trade_state === "SUCCESS") {
|
||||
await updateTrade({
|
||||
tradeNo: data.out_trade_no,
|
||||
status: "paid",
|
||||
amount: data.amount.total,
|
||||
payNo: data.transaction_id,
|
||||
payTime: dayjs(data.success_time).valueOf(),
|
||||
});
|
||||
} else if (data.trade_state === "CLOSED") {
|
||||
await updateTrade({
|
||||
tradeNo: data.out_trade_no,
|
||||
status: "closed",
|
||||
payNo: data.transaction_id,
|
||||
});
|
||||
}
|
||||
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { logger, utils } from "@certd/basic";
|
||||
import { IPaymentProvider, TradeEntity, UpdateTrade, UpdateTradeInfo } from "@certd/commercial-core";
|
||||
import dayjs from "dayjs";
|
||||
import { YizhifuAccess } from "../../../plugins/plugin-plus/yizhifu/access.js";
|
||||
|
||||
export class PaymentYizhifu implements IPaymentProvider {
|
||||
access: YizhifuAccess;
|
||||
constructor(access: YizhifuAccess) {
|
||||
this.access = access;
|
||||
}
|
||||
|
||||
async getDetail(tradeNo: string): Promise<UpdateTradeInfo> {
|
||||
/**
|
||||
* http://pay.docmirror.cn/api.php?act=order&pid={商户ID}&key={商户密钥}&out_trade_no={商户订单号}
|
||||
*
|
||||
* 请求参数说明:
|
||||
*
|
||||
* 字段名 变量名 必填 类型 示例值 描述
|
||||
* 操作类型 act 是 String order 此API固定值
|
||||
* 商户ID pid 是 Int 1001
|
||||
* 商户密钥 key 是 String 89unJUB8HZ54Hj7x4nUj56HN4nUzUJ8i
|
||||
* 系统订单号 trade_no 选择 String 20160806151343312
|
||||
* 商户订单号 out_trade_no 选择 String 20160806151343349
|
||||
*/
|
||||
|
||||
const paymentReq = {
|
||||
pid: this.access.pid,
|
||||
act: "order",
|
||||
key: this.access.key,
|
||||
out_trade_no: tradeNo,
|
||||
};
|
||||
let url = this.access.url;
|
||||
if (url.endsWith("/")) {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
const res = await utils.http.request({
|
||||
url: url + "/api.php",
|
||||
method: "get",
|
||||
params: paymentReq,
|
||||
});
|
||||
|
||||
if (res.code !== 1) {
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
|
||||
if (res.status !== "1") {
|
||||
throw new Error("该订单还未支付");
|
||||
}
|
||||
/**
|
||||
* 易支付订单号 trade_no String 2016080622555342651 袖手科技聚合支付平台订单号
|
||||
* 商户订单号 out_trade_no String 20160806151343349 商户系统内部的订单号
|
||||
* 第三方订单号 api_trade_no String 20160806151343349 支付宝微信等接口方订单号
|
||||
* 支付方式 type String alipay 支付方式列表
|
||||
* 商户ID pid Int 1001 发起支付的商户ID
|
||||
* 创建订单时间 addtime String 2016-08-06 22:55:52
|
||||
* 完成交易时间 endtime String 2016-08-06 22:55:52
|
||||
* 商品名称 name String VIP会员
|
||||
* 商品金额 money String 1.00
|
||||
* 支付状态 status Int 0 1为支付成功,0为未支付
|
||||
* 业务扩展参数 param String 默认留空
|
||||
* 支付者账号 buyer String 默认留空
|
||||
*/
|
||||
|
||||
let status: string = null;
|
||||
let payTime: number = null;
|
||||
if (res.status === "1") {
|
||||
status = "paid";
|
||||
payTime = dayjs(res.endtime).valueOf();
|
||||
} else {
|
||||
throw new Error("订单未支付");
|
||||
}
|
||||
|
||||
return {
|
||||
tradeNo: res.out_trade_no,
|
||||
payNo: res.trade_no,
|
||||
remark: "支付类型:" + res.type,
|
||||
amount: utils.amount.toCent(parseFloat(res.money)),
|
||||
status: status,
|
||||
payTime: payTime,
|
||||
};
|
||||
}
|
||||
sign(paymentReq: any): any {
|
||||
const keys = Object.keys(paymentReq);
|
||||
if (paymentReq.pid && paymentReq.pid + "" !== this.access.pid + "") {
|
||||
throw new Error("pid not match");
|
||||
}
|
||||
paymentReq.pid = this.access.pid;
|
||||
const params: any[] = [];
|
||||
|
||||
for (const key of keys) {
|
||||
const value = paymentReq[key];
|
||||
if (value != null && value !== "" && key !== "sign" && key !== "sign_type") {
|
||||
params.push({ name: key, value: value });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 1、将发送或接收到的所有参数按照参数名ASCII码从小到大排序(a-z),sign、sign_type、和空值不参与签名!
|
||||
* 2、将排序后的参数拼接成URL键值对的格式,例如 a=b&c=d&e=f,参数值不要进行url编码。
|
||||
* 3、再将拼接好的字符串与商户密钥KEY进行MD5加密得出sign签名参数,sign = md5 ( a=b&c=d&e=f + KEY ) (注意:+ 为各语言的拼接符,不是字符!),md5结果为小写。
|
||||
*/
|
||||
//sort
|
||||
const sortedParams = params.sort((a, b) => a.name.localeCompare(b.name));
|
||||
//join
|
||||
const signStr = sortedParams.map(p => `${p.name}=${p.value}`).join("&");
|
||||
//md5
|
||||
const signType = this.access.signType;
|
||||
|
||||
let sign = "";
|
||||
if (signType === "MD5") {
|
||||
sign = utils.hash.md5(signStr + this.access.key);
|
||||
} else if (signType === "SHA256") {
|
||||
sign = utils.hash.sha256(signStr + this.access.key);
|
||||
} else {
|
||||
throw new Error("不支持的签名方式");
|
||||
}
|
||||
|
||||
sign = sign.toLowerCase();
|
||||
|
||||
params.push({ name: "sign", value: sign });
|
||||
|
||||
params.push({ name: "sign_type", value: signType });
|
||||
|
||||
const body = {};
|
||||
params.forEach(p => {
|
||||
body[p.name] = p.value;
|
||||
});
|
||||
|
||||
return body;
|
||||
}
|
||||
async createOrder(trade: TradeEntity, opts: { bindUrl: string; clientIp: string }) {
|
||||
const { bindUrl } = opts;
|
||||
const paymentReq: any = {
|
||||
pid: this.access.pid,
|
||||
out_trade_no: trade.tradeNo,
|
||||
return_url: `${bindUrl}/#/certd/payment/return/yizhifu`,
|
||||
notify_url: `${bindUrl}/api/payment/notify/yizhifu`,
|
||||
name: trade.title,
|
||||
money: utils.amount.toYuan(trade.amount),
|
||||
};
|
||||
|
||||
if (this.access.payType) {
|
||||
paymentReq.type = this.access.payType;
|
||||
}
|
||||
|
||||
const body = this.sign(paymentReq);
|
||||
|
||||
let url = this.access.url;
|
||||
if (url.endsWith("/")) {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
return {
|
||||
url: url + "/submit.php",
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
checkSign(paymentRes: any) {
|
||||
// const { pid, trade_no, out_trade_no, type, name, money, trade_status, param, sign, sign_type } = paymentRes;
|
||||
const body = this.sign(paymentRes);
|
||||
const pass = body.sign === paymentRes.sign;
|
||||
if (!pass) {
|
||||
throw new Error("签名校验失败");
|
||||
}
|
||||
return pass;
|
||||
}
|
||||
|
||||
async onNotify(paymentRes: any, updateTrade: UpdateTrade) {
|
||||
logger.info(`易支付notify:${JSON.stringify(paymentRes)}`);
|
||||
this.checkSign(paymentRes);
|
||||
|
||||
const success = paymentRes.trade_status === "TRADE_SUCCESS";
|
||||
if (success) {
|
||||
await updateTrade({
|
||||
tradeNo: paymentRes.out_trade_no,
|
||||
status: "paid",
|
||||
amount: utils.amount.toCent(parseFloat(paymentRes.money)),
|
||||
remark: "支付类型:" + paymentRes.type,
|
||||
payNo: paymentRes.trade_no,
|
||||
payTime: dayjs().valueOf(),
|
||||
});
|
||||
}
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
@@ -43,4 +43,5 @@ export * from './plugin-ucloud/index.js'
|
||||
export * from './plugin-goedge/index.js'
|
||||
export * from './plugin-lib/index.js'
|
||||
export * from './plugin-plus/index.js'
|
||||
export * from './plugin-cert/index.js'
|
||||
export * from './plugin-cert/index.js'
|
||||
export * from './plugin-zenlayer/index.js'
|
||||
@@ -0,0 +1,155 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { OnePanelAccess } from "../access.js";
|
||||
import { CertReader, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { OnePanelClient } from "../client.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "1PanelDeployToPanel",
|
||||
title: "1Panel-部署面板证书",
|
||||
icon: "svg:icon-onepanel",
|
||||
desc: "更新1Panel的面板证书",
|
||||
group: pluginGroups.panel.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class OnePanelDeployToPanelPlugin extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine())
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "1Panel授权",
|
||||
helper: "1Panel授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "1panel",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "1Panel节点",
|
||||
helper: "要更新的1Panel证书的节点信息,目前只有v2存在此概念",
|
||||
typeName: "OnePanelDeployToPanelPlugin",
|
||||
action: OnePanelDeployToPanelPlugin.prototype.onGetNodes.name,
|
||||
value: "local",
|
||||
required: true,
|
||||
})
|
||||
)
|
||||
currentNode!: string;
|
||||
|
||||
|
||||
access: OnePanelAccess;
|
||||
async onInstance() {
|
||||
this.access = await this.getAccess(this.accessId);
|
||||
}
|
||||
//http://xxx:xxxx/1panel/swagger/index.html#/App/get_apps__key
|
||||
async execute(): Promise<void> {
|
||||
|
||||
const client = new OnePanelClient({
|
||||
access: this.access,
|
||||
http: this.http,
|
||||
logger: this.logger,
|
||||
utils: this.ctx.utils,
|
||||
});
|
||||
|
||||
|
||||
const certReader = new CertReader(this.cert);
|
||||
const domain = certReader.getMainDomain();
|
||||
|
||||
if (this.access.apiVersion === "v1") {
|
||||
const uploadRes = await client.doRequest({
|
||||
// api/v1/settings/ssl/update
|
||||
url: `/api/v1/settings/ssl/update`,
|
||||
method: "post",
|
||||
data: {
|
||||
cert: this.cert.crt,
|
||||
key: this.cert.key,
|
||||
domain: domain,
|
||||
ssl: "enable",
|
||||
sslID: null,
|
||||
sslType: "import-paste",
|
||||
},
|
||||
currentNode: this.currentNode,
|
||||
});
|
||||
console.log("uploadRes", JSON.stringify(uploadRes));
|
||||
} else {
|
||||
const uploadRes = await client.doRequest({
|
||||
// api/v2/core/settings/ssl/update
|
||||
url: `/api/v2/core/settings/ssl/update`,
|
||||
method: "post",
|
||||
data: {
|
||||
cert: this.cert.crt,
|
||||
key: this.cert.key,
|
||||
domain: domain,
|
||||
ssl: "Enable",
|
||||
sslID: null,
|
||||
sslType: "import-paste",
|
||||
},
|
||||
currentNode: this.currentNode,
|
||||
});
|
||||
console.log("uploadRes", JSON.stringify(uploadRes));
|
||||
}
|
||||
|
||||
await this.ctx.utils.sleep(10000);
|
||||
this.logger.info(`证书更新完成`);
|
||||
|
||||
}
|
||||
|
||||
isNeedUpdate(certRes: any) {
|
||||
if (certRes.pem === this.cert.crt && certRes.key === this.cert.key) {
|
||||
this.logger.info(`证书(id:${certRes.id})已经是最新的了,不需要更新`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async onGetNodes() {
|
||||
const options = [{ label: "主节点", value: "local" }];
|
||||
if (this.access.apiVersion === "v1") {
|
||||
return options;
|
||||
}
|
||||
if (!this.access) {
|
||||
throw new Error("请先选择授权");
|
||||
}
|
||||
const client = new OnePanelClient({
|
||||
access: this.access,
|
||||
http: this.http,
|
||||
logger: this.logger,
|
||||
utils: this.ctx.utils,
|
||||
});
|
||||
|
||||
const resp = await client.doRequest({
|
||||
url: `/api/${this.access.apiVersion}/core/nodes/list`,
|
||||
method: "post",
|
||||
data: {},
|
||||
});
|
||||
|
||||
// console.log('resp', resp)
|
||||
return [...options, ...(resp?.map(item => ({ label: `${item.addr}(${item.name})`, value: item.name })) || [])];
|
||||
}
|
||||
|
||||
}
|
||||
new OnePanelDeployToPanelPlugin();
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./deploy-to-website.js";
|
||||
export * from "./deploy-to-panel.js";
|
||||
@@ -0,0 +1,194 @@
|
||||
import { HttpRequestConfig } from '@certd/basic';
|
||||
import { IsAccess, AccessInput, BaseAccess, PageSearch } from '@certd/pipeline';
|
||||
import qs from 'qs';
|
||||
|
||||
export type ZenlayerRequest = HttpRequestConfig & {
|
||||
action: string;
|
||||
version?: string;
|
||||
}
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: 'zenlayer',
|
||||
title: 'Zenlayer授权',
|
||||
icon: 'svg:icon-lucky',
|
||||
desc: 'Zenlayer授权',
|
||||
})
|
||||
export class ZenlayerAccess extends BaseAccess {
|
||||
|
||||
/**
|
||||
* 授权属性配置
|
||||
*/
|
||||
@AccessInput({
|
||||
title: 'AccessKeyId',
|
||||
component: {
|
||||
placeholder: '访问密钥ID',
|
||||
},
|
||||
helper: "[访问密钥管理](https://console.zenlayer.com/accessKey)获取",
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
accessKeyId = '';
|
||||
|
||||
|
||||
/**
|
||||
* 授权属性配置
|
||||
*/
|
||||
@AccessInput({
|
||||
title: 'AccessKey Password',
|
||||
component: {
|
||||
placeholder: '访问密钥密码',
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
accessKeyPassword = '';
|
||||
|
||||
|
||||
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "TestRequest"
|
||||
},
|
||||
helper: "点击测试接口是否正常"
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
client: any;
|
||||
|
||||
async onTestRequest() {
|
||||
const res = await this.getCertList({ pageSize: 1 });
|
||||
this.ctx.logger.info(res);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
|
||||
async getCertList(req: PageSearch = {}):Promise<{totalCount:number,dataSet:{sans:string[],certificateId:string,certificateLabel:string,common:string}[]}> {
|
||||
const pageNo = req.pageNo ?? 1;
|
||||
const pageSize = req.pageSize ?? 100;
|
||||
const res = await this.doRequest({
|
||||
url: "/api/v2/cdn",
|
||||
action: "DescribeCertificates",
|
||||
data: {
|
||||
PageNum: pageNo,
|
||||
PageSize: pageSize
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
async getAuthorizationHeaders(req: ZenlayerRequest) {
|
||||
|
||||
/**
|
||||
* CanonicalRequest =
|
||||
HTTPRequestMethod + '\n' +
|
||||
CanonicalURI + '\n' +
|
||||
CanonicalQueryString + '\n' +
|
||||
CanonicalHeaders + '\n' +
|
||||
SignedHeaders + '\n' +
|
||||
HexEncode(Hash(RequestPayload))
|
||||
*/
|
||||
if (!req.headers) {
|
||||
req.headers = {};
|
||||
}
|
||||
if (!req.headers['content-type']) {
|
||||
req.headers['content-type'] = "application/json; charset=utf-8";
|
||||
}
|
||||
if (!req.headers['host']) {
|
||||
req.headers['host'] = "console.zenlayer.com";
|
||||
}
|
||||
|
||||
if (!req.method) {
|
||||
req.method = "POST";
|
||||
}
|
||||
// this.accessKeyPassword="Gu5t9xGARNpq86cd98joQYCN3"
|
||||
// req.data = {"pageSize":10,"pageNum":1,"zoneId":"HKG-A"}
|
||||
const CanonicalQueryString = req.method === 'POST' ? '' : qs.stringify(req.params);
|
||||
const SignedHeaders = "content-type;host";
|
||||
const CanonicalHeaders = `content-type:${req.headers['content-type']}\nhost:${req.headers['host']}\n`;
|
||||
const HashedRequestPayload = this.ctx.utils.hash.sha256(JSON.stringify(req.data || {}), "hex");
|
||||
const CanonicalRequest = `${req.method}\n/\n${CanonicalQueryString}\n${CanonicalHeaders}\n${SignedHeaders}\n${HashedRequestPayload}`;
|
||||
let HashedCanonicalRequest = this.ctx.utils.hash.sha256(CanonicalRequest, "hex");
|
||||
// HashedCanonicalRequest = "29396f9dfa0f03820b931e8aa06e20cda197e73285ebd76aceb83f7dede493ee"
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
// const timestamp= 1673361177
|
||||
const signMethod = "ZC2-HMAC-SHA256";
|
||||
const StringToSign = `${signMethod}\n${timestamp}\n${HashedCanonicalRequest}`;
|
||||
const signature = this.ctx.utils.hash.hmacSha256WithKey(this.accessKeyPassword, StringToSign, "hex");
|
||||
const authorization = `${signMethod} Credential=${this.accessKeyId}, SignedHeaders=${SignedHeaders}, Signature=${signature}`;
|
||||
|
||||
|
||||
/**
|
||||
* X-ZC-Timestamp
|
||||
|
||||
请求的时间戳,精确到秒
|
||||
|
||||
1673361177
|
||||
|
||||
X-ZC-Version
|
||||
|
||||
请求的API版本
|
||||
|
||||
2022-11-20
|
||||
|
||||
X-ZC-Action
|
||||
|
||||
请求的动作
|
||||
|
||||
DescribeInstances
|
||||
|
||||
X-ZC-Signature-Method
|
||||
|
||||
签名方法
|
||||
|
||||
ZC2-HMAC-SHA256
|
||||
|
||||
Authorization
|
||||
|
||||
签名认证
|
||||
*/
|
||||
return {
|
||||
...req.headers,
|
||||
'X-ZC-Timestamp': timestamp.toString(),
|
||||
'X-ZC-Action': req.action,
|
||||
'X-ZC-Version': req.version || "2022-11-20",
|
||||
'X-ZC-Signature-Method': signMethod,
|
||||
'Authorization': authorization,
|
||||
};
|
||||
}
|
||||
|
||||
async doRequest(req: ZenlayerRequest) {
|
||||
const headers = await this.getAuthorizationHeaders(req);
|
||||
req.headers = headers
|
||||
let res :any = undefined;
|
||||
try{
|
||||
res = await this.ctx.http.request({
|
||||
baseURL: req.baseURL || "https://console.zenlayer.com",
|
||||
...req
|
||||
});
|
||||
} catch (error) {
|
||||
const resData = error.response?.data;
|
||||
if (resData){
|
||||
let desc = ""
|
||||
if (resData.code === "CERTIFICATE_NOT_COVER_ALL_DOMAIN"){
|
||||
desc = `证书未覆盖所有域名`;
|
||||
}
|
||||
throw new Error(`[code=${resData.code}] ${desc} ${resData.message} [requestId:${resData.requestId}]`);
|
||||
}
|
||||
throw error
|
||||
}
|
||||
if (res.code) {
|
||||
throw new Error(`[${res.code}]:${res.message} [requestId:${res.requestId}]`);
|
||||
}
|
||||
return res.response;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
new ZenlayerAccess();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './access.js';
|
||||
export * from './plugins/index.js';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './plugin-refresh-cert.js';
|
||||
@@ -0,0 +1,139 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { ZenlayerAccess } from "../access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
//命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名
|
||||
name: "ZenlayerRefreshCert",
|
||||
title: "Zenlayer-刷新证书",
|
||||
desc: "刷新Zenlayer CDN证书",
|
||||
icon: "svg:icon-lucky",
|
||||
//插件分组
|
||||
group: pluginGroups.cdn.key,
|
||||
needPlus: false,
|
||||
default: {
|
||||
//默认值配置照抄即可
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed
|
||||
}
|
||||
}
|
||||
})
|
||||
//类名规范,跟上面插件名称(name)一致
|
||||
export class ZenlayerRefreshCert extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames]
|
||||
}
|
||||
// required: true, // 必填
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "Zenlayer授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "zenlayer" //固定授权类型
|
||||
},
|
||||
required: true //必填
|
||||
})
|
||||
accessId!: string;
|
||||
//
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "证书ID列表",
|
||||
helper: "要更新的Zenlayer证书ID列表",
|
||||
|
||||
action: ZenlayerRefreshCert.prototype.onGetCertList.name
|
||||
})
|
||||
)
|
||||
certList!: string[];
|
||||
|
||||
//插件实例化时执行的方法
|
||||
async onInstance() {
|
||||
}
|
||||
|
||||
//插件执行方法
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<ZenlayerAccess>(this.accessId);
|
||||
|
||||
for (const certId of this.certList) {
|
||||
await this.updateCert({
|
||||
access: access,
|
||||
certId: certId,
|
||||
cert: this.cert
|
||||
});
|
||||
this.logger.info(`刷新证书${certId}成功`);
|
||||
await this.ctx.utils.sleep(1000);
|
||||
}
|
||||
|
||||
this.logger.info("部署完成");
|
||||
}
|
||||
|
||||
async updateCert(req:{access:ZenlayerAccess,certId:string, cert: CertInfo}){
|
||||
const {access,certId, cert} = req;
|
||||
// ModifyCertificate
|
||||
await access.doRequest({
|
||||
url: "/api/v2/cdn",
|
||||
action: "ModifyCertificate",
|
||||
data: {
|
||||
/**
|
||||
* certificateId
|
||||
certificateContent
|
||||
certificateKey
|
||||
*/
|
||||
certificateId: certId,
|
||||
certificateContent: cert.crt,
|
||||
certificateKey: cert.key,
|
||||
}
|
||||
});
|
||||
}
|
||||
async onGetCertList(req: PageSearch = {}) {
|
||||
const access = await this.getAccess<ZenlayerAccess>(this.accessId);
|
||||
|
||||
const pageNo = req.pageNo ?? 1;
|
||||
const pageSize = req.pageSize ?? 100;
|
||||
const res = await access.getCertList(
|
||||
{
|
||||
pageNo: pageNo,
|
||||
pageSize: pageSize
|
||||
}
|
||||
);
|
||||
const total = res.totalCount;
|
||||
const list = res.dataSet || [];
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("没有找到Zenlayer证书,请先在控制台CDN证书管理创建证书");
|
||||
}
|
||||
|
||||
/**
|
||||
* "Domain": "ucloud.certd.handsfree.work",
|
||||
"DomainId": "ucdn-1kwdtph5ygbb"
|
||||
*/
|
||||
const options = list.map((item: any) => {
|
||||
return {
|
||||
label: `${item.certificateLabel}<${item.certificateId}-${item.common}>`,
|
||||
value: `${item.certificateId}`,
|
||||
domain: item.sans
|
||||
};
|
||||
});
|
||||
return {
|
||||
list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains),
|
||||
total: total,
|
||||
pageNo: pageNo,
|
||||
pageSize: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//实例化一下,注册插件
|
||||
new ZenlayerRefreshCert();
|
||||
Generated
+38
-134
@@ -788,9 +788,6 @@ importers:
|
||||
'@certd/pipeline':
|
||||
specifier: ^1.37.17
|
||||
version: link:../../core/pipeline
|
||||
'@certd/plugin-plus':
|
||||
specifier: ^1.37.17
|
||||
version: link:../plugin-plus
|
||||
'@certd/plus-core':
|
||||
specifier: ^1.37.17
|
||||
version: link:../plus-core
|
||||
@@ -806,18 +803,12 @@ importers:
|
||||
'@midwayjs/typeorm':
|
||||
specifier: 3.20.11
|
||||
version: 3.20.11
|
||||
alipay-sdk:
|
||||
specifier: ^4.13.0
|
||||
version: 4.14.0
|
||||
dayjs:
|
||||
specifier: ^1.11.7
|
||||
version: 1.11.13
|
||||
typeorm:
|
||||
specifier: ^0.3.20
|
||||
version: 0.3.24(better-sqlite3@11.10.0)(mysql2@3.14.1)(pg@8.16.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@18.19.100)(typescript@5.8.3))
|
||||
wechatpay-node-v3:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
devDependencies:
|
||||
'@rollup/plugin-json':
|
||||
specifier: ^6.0.0
|
||||
@@ -868,130 +859,6 @@ importers:
|
||||
specifier: ^5.4.2
|
||||
version: 5.8.3
|
||||
|
||||
packages/pro/plugin-plus:
|
||||
dependencies:
|
||||
'@alicloud/pop-core':
|
||||
specifier: ^1.7.10
|
||||
version: 1.8.0
|
||||
'@baiducloud/sdk':
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.3
|
||||
'@certd/basic':
|
||||
specifier: ^1.37.17
|
||||
version: link:../../core/basic
|
||||
'@certd/lib-k8s':
|
||||
specifier: ^1.37.17
|
||||
version: link:../../libs/lib-k8s
|
||||
'@certd/pipeline':
|
||||
specifier: ^1.37.17
|
||||
version: link:../../core/pipeline
|
||||
'@certd/plugin-cert':
|
||||
specifier: ^1.37.17
|
||||
version: link:../../plugins/plugin-cert
|
||||
'@certd/plugin-lib':
|
||||
specifier: ^1.37.17
|
||||
version: link:../../plugins/plugin-lib
|
||||
'@certd/plus-core':
|
||||
specifier: ^1.37.17
|
||||
version: link:../plus-core
|
||||
ali-oss:
|
||||
specifier: ^6.21.0
|
||||
version: 6.23.0
|
||||
baidu-aip-sdk:
|
||||
specifier: ^4.16.16
|
||||
version: 4.16.16
|
||||
basic-ftp:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
cos-nodejs-sdk-v5:
|
||||
specifier: ^2.14.6
|
||||
version: 2.14.7
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
dayjs:
|
||||
specifier: ^1.11.7
|
||||
version: 1.11.13
|
||||
form-data:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.2
|
||||
https-proxy-agent:
|
||||
specifier: ^7.0.5
|
||||
version: 7.0.6
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
jsencrypt:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
jsrsasign:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
qiniu:
|
||||
specifier: ^7.12.0
|
||||
version: 7.14.0
|
||||
tencentcloud-sdk-nodejs:
|
||||
specifier: ^4.0.44
|
||||
version: 4.1.112(encoding@0.1.13)
|
||||
devDependencies:
|
||||
'@rollup/plugin-json':
|
||||
specifier: ^6.0.0
|
||||
version: 6.1.0(rollup@3.29.5)
|
||||
'@rollup/plugin-terser':
|
||||
specifier: ^0.4.3
|
||||
version: 0.4.4(rollup@3.29.5)
|
||||
'@rollup/plugin-typescript':
|
||||
specifier: ^11.0.0
|
||||
version: 11.1.6(rollup@3.29.5)(tslib@2.8.1)(typescript@5.8.3)
|
||||
'@types/ali-oss':
|
||||
specifier: ^6.16.11
|
||||
version: 6.16.11
|
||||
'@types/chai':
|
||||
specifier: ^4.3.10
|
||||
version: 4.3.20
|
||||
'@types/mocha':
|
||||
specifier: ^10.0.7
|
||||
version: 10.0.10
|
||||
'@types/node':
|
||||
specifier: ^18
|
||||
version: 18.19.100
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^8.26.1
|
||||
version: 8.32.1(@typescript-eslint/parser@8.32.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0)(typescript@5.8.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^8.26.1
|
||||
version: 8.32.1(eslint@8.57.0)(typescript@5.8.3)
|
||||
chai:
|
||||
specifier: 4.3.10
|
||||
version: 4.3.10
|
||||
eslint:
|
||||
specifier: ^8.41.0
|
||||
version: 8.57.0
|
||||
eslint-config-prettier:
|
||||
specifier: ^8.8.0
|
||||
version: 8.10.0(eslint@8.57.0)
|
||||
eslint-plugin-prettier:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8)
|
||||
mocha:
|
||||
specifier: ^10.2.0
|
||||
version: 10.8.2
|
||||
prettier:
|
||||
specifier: ^2.8.8
|
||||
version: 2.8.8
|
||||
rimraf:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.10
|
||||
rollup:
|
||||
specifier: ^3.7.4
|
||||
version: 3.29.5
|
||||
tslib:
|
||||
specifier: ^2.8.1
|
||||
version: 2.8.1
|
||||
typescript:
|
||||
specifier: ^5.4.2
|
||||
version: 5.8.3
|
||||
|
||||
packages/pro/plus-core:
|
||||
dependencies:
|
||||
'@certd/basic':
|
||||
@@ -1520,7 +1387,7 @@ importers:
|
||||
version: link:../../plugins/plugin-lib
|
||||
'@certd/plugin-plus':
|
||||
specifier: ^1.37.17
|
||||
version: link:../../pro/plugin-plus
|
||||
version: 1.37.17(encoding@0.1.13)
|
||||
'@certd/plus-core':
|
||||
specifier: ^1.37.17
|
||||
version: link:../../pro/plus-core
|
||||
@@ -1581,6 +1448,9 @@ importers:
|
||||
ali-oss:
|
||||
specifier: ^6.21.0
|
||||
version: 6.23.0
|
||||
alipay-sdk:
|
||||
specifier: ^4.13.0
|
||||
version: 4.14.0
|
||||
axios:
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0(debug@4.4.1)
|
||||
@@ -1731,6 +1601,9 @@ importers:
|
||||
uuid:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
wechatpay-node-v3:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
xml2js:
|
||||
specifier: ^0.6.2
|
||||
version: 0.6.2
|
||||
@@ -2888,6 +2761,9 @@ packages:
|
||||
'@certd/cv4pve-api-javascript@8.4.2':
|
||||
resolution: {integrity: sha512-udGce7ewrVl4DmZvX+17PjsnqsdDIHEDatr8QP0AVrY2p+8JkaSPW4mXCKiLGf82C9K2+GXgT+qNIqgW7tfF9Q==}
|
||||
|
||||
'@certd/plugin-plus@1.37.17':
|
||||
resolution: {integrity: sha512-yIBCdHc+vdOQa9cMRRKHvyLgVCQvwqYzCzOFzODEWL7zZ3e6T3YLueG49vqyr0dyEEcY/JXfqT4vDo+r3Zs1zw==}
|
||||
|
||||
'@certd/vue-js-cron-core@6.0.3':
|
||||
resolution: {integrity: sha512-kqzoAMhYz9j6FGNWEODRYtt4NpUEUwjpkU89z5WVg2tCtOcI5VhwyUGOd8AxiBCRfd6PtXvzuqw85PaOps9wrQ==}
|
||||
|
||||
@@ -15446,6 +15322,34 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@certd/plugin-plus@1.37.17(encoding@0.1.13)':
|
||||
dependencies:
|
||||
'@alicloud/pop-core': 1.8.0
|
||||
'@baiducloud/sdk': 1.0.3
|
||||
'@certd/basic': link:packages/core/basic
|
||||
'@certd/lib-k8s': link:packages/libs/lib-k8s
|
||||
'@certd/pipeline': link:packages/core/pipeline
|
||||
'@certd/plugin-cert': link:packages/plugins/plugin-cert
|
||||
'@certd/plugin-lib': link:packages/plugins/plugin-lib
|
||||
'@certd/plus-core': link:packages/pro/plus-core
|
||||
ali-oss: 6.23.0
|
||||
baidu-aip-sdk: 4.16.16
|
||||
basic-ftp: 5.0.5
|
||||
cos-nodejs-sdk-v5: 2.14.7
|
||||
crypto-js: 4.2.0
|
||||
dayjs: 1.11.13
|
||||
form-data: 4.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
js-yaml: 4.1.0
|
||||
jsencrypt: 3.3.2
|
||||
jsrsasign: 11.1.0
|
||||
qiniu: 7.14.0
|
||||
tencentcloud-sdk-nodejs: 4.1.112(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- proxy-agent
|
||||
- supports-color
|
||||
|
||||
'@certd/vue-js-cron-core@6.0.3':
|
||||
dependencies:
|
||||
mustache: 4.2.0
|
||||
|
||||
Reference in New Issue
Block a user