perf: 验证码支持测试,登录验证码需要测试通过后才能开启

pull/543/head
xiaojunnuo 2025-09-26 01:21:01 +08:00
parent 03f317ffdb
commit 83e6476408
18 changed files with 485 additions and 60 deletions

View File

@ -24,5 +24,6 @@
</div> </div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
<script src="https://static.geetest.com/v4/gt4.js"></script> <script src="https://static.geetest.com/v4/gt4.js"></script>
<script src="https://turing.captcha.qcloud.com/TJCaptcha.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,208 @@
<template>
<div ref="captchaRef" class="tencent_captcha_wrapper" :class="{ tencent_captcha_ok: modelValue }" @click="triggerCaptcha">
<div class="validation-box" :class="{ validated: modelValue != null }">
<div class="sweep-animation"></div>
<div class="box-content">
<div class="box-icon"></div>
<span class="status-text">点击进行校验</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, defineProps, defineEmits, ref, onUnmounted, Ref, watch } from "vue";
import { notification } from "ant-design-vue";
defineOptions({
name: "TencentCaptcha",
});
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
modelValue: any;
captchaGet: () => Promise<any>;
}>();
const captchaRef = ref(null);
const captchaInstanceRef: Ref = ref({});
//
function callback(res: { ret: number; ticket: string; randstr: string; errorCode?: number; errorMessage?: string }) {
//
// ret Int 02
// ticket String ret = 0 ticket
// CaptchaAppId String ID
// bizState Any
// randstr String
// verifyDuration Int ms
// actionDuration Int +(ms)
// sid String sid
console.log("callback:", res);
// res= {ret: 2, ticket: null}
// res = {ret: 0, ticket: "String", randstr: "String"}
// restrerror_ = {ret: 0, ticket: "String", randstr: "String", errorCode: Number, errorMessage: "String"}
// ticketerrorCode
if (res.ret === 0) {
debugger;
emitChange({
ticket: res.ticket,
randstr: res.randstr,
});
} else if (res.ret === 2) {
console.log("用户主动关闭验证码");
}
}
// js
function loadErrorCallback(error: any) {
// var appid = "CaptchaAppId";
// //
// var ticket = "trerror_1001_" + appid + "_" + Math.floor(new Date().getTime() / 1000);
// callback({
// ret: 0,
// randstr: "@" + Math.random().toString(36).substr(2),
// ticket: ticket,
// errorCode: 1001,
// errorMessage: "jsload_error",
// });
notification.error({
message: `验证码加载失败:${error?.message || error}`,
});
}
async function triggerCaptcha() {
const { captchaAppId } = await props.captchaGet();
try {
//
// CaptchaAppId使CaptchaAppId
//callback
// @ts-ignore
var captcha = new TencentCaptcha(captchaAppId + "", callback, {
userLanguage: "zh-cn",
// showFn: (ret: any) => {
// const {
// duration, // (ms)
// sid, // sid
// } = ret;
// },
});
//
captcha.show();
} catch (error) {
// js
loadErrorCallback(error);
}
}
function emitChange(value: any) {
emit("update:modelValue", value);
emit("change", value);
}
function reset() {
captchaInstanceRef.value.instance.reset();
}
watch(
() => {
return props.modelValue;
},
value => {
if (value == null) {
reset();
}
}
);
defineExpose({
reset,
});
</script>
<style lang="less">
.tencent_captcha_wrapper {
.validation-box {
width: 100%;
height: 40px;
margin: 0 auto 30px;
border: 1px solid #ddd;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
background-color: #f9f9f9;
}
.validation-box:hover {
border-color: #aaa;
background-color: #f0f0f0;
}
.validation-box.validated {
border-color: #4caf50;
background-color: #f1f8e9;
}
.box-content {
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
position: relative;
}
.box-icon {
font-size: 18px;
color: #bbb;
margin-right: 15px;
transition: all 0.3s ease;
}
.validation-box.validated .box-icon {
color: #4caf50;
}
.status-text {
font-size: 14px;
font-weight: 500;
color: #888;
transition: all 0.3s ease;
}
.validation-box.validated .status-text {
color: #4caf50;
font-weight: 600;
}
/* 划过动画效果 */
.sweep-animation {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(76, 175, 80, 0.2), transparent);
z-index: 1;
opacity: 0;
transition: opacity 0.3s;
}
.validation-box.validated .sweep-animation {
animation: sweep 0.8s ease forwards;
opacity: 1;
}
@keyframes sweep {
0% {
left: -100%;
}
50% {
left: 0;
}
100% {
left: 100%;
}
}
}
</style>

View File

@ -45,6 +45,16 @@ export async function DoVerify(id: number) {
}); });
} }
export async function ResetStatus(id: number) {
return await request({
url: apiPrefix + "/resetStatus",
method: "post",
data: {
id,
},
});
}
export async function ParseDomain(fullDomain: string) { export async function ParseDomain(fullDomain: string) {
return await request({ return await request({
url: subDomainApiPrefix + "/parseDomain", url: subDomainApiPrefix + "/parseDomain",

View File

@ -16,6 +16,9 @@
<a-tooltip v-if="cnameRecord.error" :title="cnameRecord.error"> <a-tooltip v-if="cnameRecord.error" :title="cnameRecord.error">
<fs-icon class="ml-5 color-red" icon="ion:warning-outline"></fs-icon> <fs-icon class="ml-5 color-red" icon="ion:warning-outline"></fs-icon>
</a-tooltip> </a-tooltip>
<a-tooltip v-if="cnameRecord.status === 'valid'" title="重置校验状态,重新校验">
<fs-icon class="ml-5 color-red" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
</a-tooltip>
</td> </td>
<td class="center"> <td class="center">
<template v-if="cnameRecord.status !== 'valid'"> <template v-if="cnameRecord.status !== 'valid'">
@ -71,12 +74,15 @@ function onRecordChange() {
}); });
} }
async function loadRecord() {
cnameRecord.value = await GetByDomain(props.domain);
}
let refreshIntervalId: any = null; let refreshIntervalId: any = null;
async function doRefresh() { async function doRefresh() {
if (!props.domain) { if (!props.domain) {
return; return;
} }
cnameRecord.value = await GetByDomain(props.domain); await loadRecord();
onRecordChange(); onRecordChange();
if (cnameRecord.value.status === "validating") { if (cnameRecord.value.status === "validating") {
@ -114,6 +120,11 @@ async function doVerify() {
} }
await doRefresh(); await doRefresh();
} }
async function resetStatus() {
await api.ResetStatus(cnameRecord.value.id);
await loadRecord();
}
</script> </script>
<style lang="less"> <style lang="less">

View File

@ -723,7 +723,9 @@ export default {
captchaEnabled: "Enable Login Captcha", captchaEnabled: "Enable Login Captcha",
captchaHelper: "Whether to enable captcha verification for login", captchaHelper: "Whether to enable captcha verification for login",
captchaType: "Captcha Setting", captchaType: "Captcha Setting",
captchaTest: "Captcha Test",
// 保存后再点击测试,请务必测试通过了,再开启登录验证码
captchaTestHelper: "Save and click test, please make sure the test is passed before enabling login captcha",
baseSetting: "Base Settings", baseSetting: "Base Settings",
registerSetting: "Register Settings", registerSetting: "Register Settings",
safeSetting: "Safe Settings", safeSetting: "Safe Settings",

View File

@ -725,7 +725,8 @@ export default {
captchaEnabled: "启用登录验证码", captchaEnabled: "启用登录验证码",
captchaHelper: "登录时是否启用验证码", captchaHelper: "登录时是否启用验证码",
captchaType: "验证码配置", captchaType: "验证码配置",
captchaTest: "测试验证码",
captchaTestHelper: "保存后再点击测试,请务必测试通过了,再开启登录验证码",
baseSetting: "基本设置", baseSetting: "基本设置",
registerSetting: "注册设置", registerSetting: "注册设置",
safeSetting: "安全设置", safeSetting: "安全设置",

View File

@ -79,6 +79,14 @@ export async function SysSettingsSave(data: SysSettings) {
}); });
} }
export async function TestCaptcha(form: any) {
return await request({
url: apiPrefix + "/captchaTest",
method: "post",
data: form,
});
}
export async function TestProxy() { export async function TestProxy() {
return await request({ return await request({
url: apiPrefix + "/testProxy", url: apiPrefix + "/testProxy",

View File

@ -58,7 +58,7 @@ function onChange(value: string) {
<style lang="less"> <style lang="less">
.page-sys-settings { .page-sys-settings {
.sys-settings-form { .sys-settings-form {
width: 600px; width: 800px;
max-width: 100%; max-width: 100%;
padding: 20px; padding: 20px;
} }

View File

@ -54,6 +54,12 @@
<a-form-item :label="t('certd.sys.setting.captchaType')" :name="['public', 'captchaAddonId']"> <a-form-item :label="t('certd.sys.setting.captchaType')" :name="['public', 'captchaAddonId']">
<addon-selector v-model:model-value="formState.public.captchaAddonId" addon-type="captcha" from="sys" @selected-change="onAddonChanged" /> <addon-selector v-model:model-value="formState.public.captchaAddonId" addon-type="captcha" from="sys" @selected-change="onAddonChanged" />
</a-form-item> </a-form-item>
<a-form-item v-if="formState.public.captchaType" :label="t('certd.sys.setting.captchaTest')">
<div class="flex">
<CaptchaInput v-model:model-value="captchaTestForm.captcha" class="w-50%"></CaptchaInput>
<a-button class="ml-2" type="primary" @click="doCaptchaValidate"></a-button>
</div>
</a-form-item>
<a-form-item :name="['public', 'captchaType']" class="hidden"> <a-form-item :name="['public', 'captchaType']" class="hidden">
<a-input v-model:model-value="formState.public.captchaType"></a-input> <a-input v-model:model-value="formState.public.captchaType"></a-input>
@ -76,12 +82,32 @@ import { notification } from "ant-design-vue";
import { util } from "/@/utils"; import { util } from "/@/utils";
import { useI18n } from "/src/locales"; import { useI18n } from "/src/locales";
import AddonSelector from "../../../certd/addon/addon-selector/index.vue"; import AddonSelector from "../../../certd/addon/addon-selector/index.vue";
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
const { t } = useI18n(); const { t } = useI18n();
defineOptions({ defineOptions({
name: "SettingBase", name: "SettingBase",
}); });
const captchaTestForm = reactive({
captcha: null,
pass: false,
});
async function doCaptchaValidate() {
if (!captchaTestForm.captcha) {
notification.error({
message: "请进行验证码验证",
});
return;
}
await api.TestCaptcha(captchaTestForm.captcha);
notification.success({
message: "校验通过",
});
captchaTestForm.pass = true;
}
const formState = reactive<Partial<SysSettings>>({ const formState = reactive<Partial<SysSettings>>({
public: { public: {
icpNo: "", icpNo: "",
@ -106,6 +132,14 @@ const settingsStore = useSettingStore();
const onFinish = async (form: any) => { const onFinish = async (form: any) => {
try { try {
saveLoading.value = true; saveLoading.value = true;
if (form.public.captchaEnabled && !captchaTestForm.pass) {
notification.error({
message: "请先通过验证码测试之后再开启登录验证码",
});
return;
}
await api.SysSettingsSave(form); await api.SysSettingsSave(form);
await settingsStore.loadSysSettings(); await settingsStore.loadSysSettings();
notification.success({ notification.success({
@ -113,6 +147,7 @@ const onFinish = async (form: any) => {
}); });
} finally { } finally {
saveLoading.value = false; saveLoading.value = false;
captchaTestForm.pass = false;
} }
}; };

View File

@ -1,7 +1,6 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core'; import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { AccessService, Constants } from '@certd/lib-server'; import { AccessService, Constants } from "@certd/lib-server";
import { AccessController } from '../../user/pipeline/access-controller.js'; import { AccessController } from "../../user/pipeline/access-controller.js";
import { checkComm } from '@certd/plus-core';
/** /**
* *
@ -17,7 +16,7 @@ export class SysAccessController extends AccessController {
} }
getUserId() { getUserId() {
checkComm(); // checkComm();
return 0; return 0;
} }

View File

@ -192,4 +192,11 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
await this.service.saveSetting(blankSetting); await this.service.saveSetting(blankSetting);
return this.ok({}); return this.ok({});
} }
@Post("/captchaTest", { summary: "sys:settings:edit" })
async captchaTest(@Body(ALL) body: any) {
await this.codeService.checkCaptcha(body)
return this.ok({});
}
} }

View File

@ -11,56 +11,59 @@ import {
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js"; import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
import { checkPlus } from "@certd/plus-core"; import { checkPlus } from "@certd/plus-core";
import { http, logger, utils } from "@certd/basic"; import { http, logger, utils } from "@certd/basic";
import { TaskServiceBuilder } from "../../../modules/pipeline/service/getter/task-service-getter.js";
/** /**
* Addon * Addon
*/ */
@Provide() @Provide()
@Controller('/api/addon') @Controller("/api/addon")
export class AddonController extends CrudController<AddonService> { export class AddonController extends CrudController<AddonService> {
@Inject() @Inject()
service: AddonService; service: AddonService;
@Inject() @Inject()
authService: AuthService; authService: AuthService;
@Inject()
taskServiceBuilder:TaskServiceBuilder
getService(): AddonService { getService(): AddonService {
return this.service; return this.service;
} }
@Post('/page', { summary: Constants.per.authOnly }) @Post("/page", { summary: Constants.per.authOnly })
async page(@Body(ALL) body) { async page(@Body(ALL) body) {
body.query = body.query ?? {}; body.query = body.query ?? {};
delete body.query.userId; delete body.query.userId;
const buildQuery = qb => { const buildQuery = qb => {
qb.andWhere('user_id = :userId', { userId: this.getUserId() }); qb.andWhere("user_id = :userId", { userId: this.getUserId() });
}; };
const res = await this.service.page({ const res = await this.service.page({
query: body.query, query: body.query,
page: body.page, page: body.page,
sort: body.sort, sort: body.sort,
buildQuery, buildQuery
}); });
return this.ok(res); return this.ok(res);
} }
@Post('/list', { summary: Constants.per.authOnly }) @Post("/list", { summary: Constants.per.authOnly })
async list(@Body(ALL) body) { async list(@Body(ALL) body) {
body.query = body.query ?? {}; body.query = body.query ?? {};
body.query.userId = this.getUserId(); body.query.userId = this.getUserId();
return super.list(body); return super.list(body);
} }
@Post('/add', { summary: Constants.per.authOnly }) @Post("/add", { summary: Constants.per.authOnly })
async add(@Body(ALL) bean) { async add(@Body(ALL) bean) {
bean.userId = this.getUserId(); bean.userId = this.getUserId();
const type = bean.type; const type = bean.type;
const addonType = bean.addonType; const addonType = bean.addonType;
if (! type || !addonType){ if (!type || !addonType) {
throw new ValidateException('请选择Addon类型'); throw new ValidateException("请选择Addon类型");
} }
const define: AddonDefine = this.service.getDefineByType(type,addonType); const define: AddonDefine = this.service.getDefineByType(type, addonType);
if (!define) { if (!define) {
throw new ValidateException('Addon类型不存在'); throw new ValidateException("Addon类型不存在");
} }
if (define.needPlus) { if (define.needPlus) {
checkPlus(); checkPlus();
@ -68,19 +71,19 @@ export class AddonController extends CrudController<AddonService> {
return super.add(bean); return super.add(bean);
} }
@Post('/update', { summary: Constants.per.authOnly }) @Post("/update", { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) { async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId()); await this.service.checkUserId(bean.id, this.getUserId());
const old = await this.service.info(bean.id); const old = await this.service.info(bean.id);
if (!old) { if (!old) {
throw new ValidateException('Addon配置不存在'); throw new ValidateException("Addon配置不存在");
} }
if (old.type !== bean.type ) { if (old.type !== bean.type) {
const addonType = old.type; const addonType = old.type;
const type = bean.type; const type = bean.type;
const define: AddonDefine = this.service.getDefineByType(type,addonType); const define: AddonDefine = this.service.getDefineByType(type, addonType);
if (!define) { if (!define) {
throw new ValidateException('Addon类型不存在'); throw new ValidateException("Addon类型不存在");
} }
if (define.needPlus) { if (define.needPlus) {
checkPlus(); checkPlus();
@ -89,26 +92,27 @@ export class AddonController extends CrudController<AddonService> {
delete bean.userId; delete bean.userId;
return super.update(bean); return super.update(bean);
} }
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) { @Post("/info", { summary: Constants.per.authOnly })
async info(@Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId()); await this.service.checkUserId(id, this.getUserId());
return super.info(id); return super.info(id);
} }
@Post('/delete', { summary: Constants.per.authOnly }) @Post("/delete", { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) { async delete(@Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId()); await this.service.checkUserId(id, this.getUserId());
return super.delete(id); return super.delete(id);
} }
@Post('/define', { summary: Constants.per.authOnly }) @Post("/define", { summary: Constants.per.authOnly })
async define(@Query('type') type: string,@Query('addonType') addonType: string) { async define(@Query("type") type: string, @Query("addonType") addonType: string) {
const notification = this.service.getDefineByType(type,addonType); const notification = this.service.getDefineByType(type, addonType);
return this.ok(notification); return this.ok(notification);
} }
@Post('/getTypeDict', { summary: Constants.per.authOnly }) @Post("/getTypeDict", { summary: Constants.per.authOnly })
async getTypeDict(@Query('addonType') addonType: string) { async getTypeDict(@Query("addonType") addonType: string) {
const list: any = this.service.getDefineList(addonType); const list: any = this.service.getDefineList(addonType);
let dict = []; let dict = [];
for (const item of list) { for (const item of list) {
@ -116,7 +120,7 @@ export class AddonController extends CrudController<AddonService> {
value: item.name, value: item.name,
label: item.title, label: item.title,
needPlus: item.needPlus ?? false, needPlus: item.needPlus ?? false,
icon: item.icon, icon: item.icon
}); });
} }
dict = dict.sort(a => { dict = dict.sort(a => {
@ -125,13 +129,13 @@ export class AddonController extends CrudController<AddonService> {
return this.ok(dict); return this.ok(dict);
} }
@Post('/simpleInfo', { summary: Constants.per.authOnly }) @Post("/simpleInfo", { summary: Constants.per.authOnly })
async simpleInfo(@Query('addonType') addonType: string,@Query('id') id: number) { async simpleInfo(@Query("addonType") addonType: string, @Query("id") id: number) {
if (id === 0) { if (id === 0) {
//获取默认 //获取默认
const res = await this.service.getDefault(this.getUserId(),addonType); const res = await this.service.getDefault(this.getUserId(), addonType);
if (!res) { if (!res) {
throw new ValidateException('默认Addon配置不存在'); throw new ValidateException("默认Addon配置不存在");
} }
const simple = await this.service.getSimpleInfo(res.id); const simple = await this.service.getSimpleInfo(res.id);
return this.ok(simple); return this.ok(simple);
@ -141,27 +145,27 @@ export class AddonController extends CrudController<AddonService> {
return this.ok(res); return this.ok(res);
} }
@Post('/getDefaultId', { summary: Constants.per.authOnly }) @Post("/getDefaultId", { summary: Constants.per.authOnly })
async getDefaultId(@Query('addonType') addonType: string) { async getDefaultId(@Query("addonType") addonType: string) {
const res = await this.service.getDefault(this.getUserId(),addonType); const res = await this.service.getDefault(this.getUserId(), addonType);
return this.ok(res?.id); return this.ok(res?.id);
} }
@Post('/setDefault', { summary: Constants.per.authOnly }) @Post("/setDefault", { summary: Constants.per.authOnly })
async setDefault(@Query('addonType') addonType: string,@Query('id') id: number) { async setDefault(@Query("addonType") addonType: string, @Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId()); await this.service.checkUserId(id, this.getUserId());
const res = await this.service.setDefault(id, this.getUserId(),addonType); const res = await this.service.setDefault(id, this.getUserId(), addonType);
return this.ok(res); return this.ok(res);
} }
@Post('/options', { summary: Constants.per.authOnly }) @Post("/options", { summary: Constants.per.authOnly })
async options(@Query('addonType') addonType: string) { async options(@Query("addonType") addonType: string) {
const res = await this.service.list({ const res = await this.service.list({
query: { query: {
userId: this.getUserId(), userId: this.getUserId(),
addonType addonType
}, }
}); });
for (const item of res) { for (const item of res) {
delete item.setting; delete item.setting;
@ -170,7 +174,7 @@ export class AddonController extends CrudController<AddonService> {
} }
@Post('/handle', { summary: Constants.per.authOnly }) @Post("/handle", { summary: Constants.per.authOnly })
async handle(@Body(ALL) body: AddonRequestHandleReq) { async handle(@Body(ALL) body: AddonRequestHandleReq) {
const userId = this.getUserId(); const userId = this.getUserId();
let inputAddon = body.input.addon; let inputAddon = body.input.addon;
@ -178,21 +182,24 @@ export class AddonController extends CrudController<AddonService> {
const oldEntity = await this.service.info(body.input.id); const oldEntity = await this.service.info(body.input.id);
if (oldEntity) { if (oldEntity) {
if (oldEntity.userId !== userId) { if (oldEntity.userId !== userId) {
throw new Error('addon not found'); throw new Error("addon not found");
} }
// const param: any = { // const param: any = {
// type: body.typeName, // type: body.typeName,
// setting: JSON.stringify(body.input.access), // setting: JSON.stringify(body.input.access),
// }; // };
inputAddon = JSON.parse( oldEntity.setting) inputAddon = JSON.parse(oldEntity.setting);
} }
} }
const serviceGetter = this.taskServiceBuilder.create({ userId });
const ctx = { const ctx = {
http: http, http: http,
logger:logger, logger: logger,
utils:utils, utils: utils,
} serviceGetter
const addon = await newAddon(body.addonType,body.typeName, inputAddon,ctx); };
const addon = await newAddon(body.addonType, body.typeName, inputAddon, ctx);
const res = await addon.onRequest(body); const res = await addon.onRequest(body);
return this.ok(res); return this.ok(res);
} }

View File

@ -85,10 +85,18 @@ export class CnameRecordController extends CrudController<CnameRecordService> {
} }
@Post('/verify', { summary: Constants.per.authOnly }) @Post('/verify', { summary: Constants.per.authOnly })
async verify(@Body(ALL) body: { id: string }) { async verify(@Body(ALL) body: { id: number }) {
const userId = this.getUserId(); const userId = this.getUserId();
await this.service.checkUserId(body.id, userId); await this.service.checkUserId(body.id, userId);
const res = await this.service.verify(body.id); const res = await this.service.verify(body.id);
return this.ok(res); return this.ok(res);
} }
@Post('/resetStatus', { summary: Constants.per.authOnly })
async resetStatus(@Body(ALL) body: { id: number }) {
const userId = this.getUserId();
await this.service.checkUserId(body.id, userId);
const res = await this.service.resetStatus(body.id);
return this.ok(res);
}
} }

View File

@ -13,6 +13,8 @@ export class CnameRecordEntity {
@Column({ comment: '证书申请域名', length: 100 }) @Column({ comment: '证书申请域名', length: 100 })
domain: string; domain: string;
@Column({ comment: '主域名', name: 'main_domain', length: 100 })
mainDomain:string;
@Column({ comment: '主机记录', name: 'host_record', length: 100 }) @Column({ comment: '主机记录', name: 'host_record', length: 100 })
hostRecord: string; hostRecord: string;

View File

@ -115,6 +115,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
hostRecord = hostRecord.substring(0, hostRecord.length - 1); hostRecord = hostRecord.substring(0, hostRecord.length - 1);
} }
param.hostRecord = hostRecord; param.hostRecord = hostRecord;
param.mainDomain = realDomain;
const randomKey = utils.id.simpleNanoId(6).toLowerCase(); const randomKey = utils.id.simpleNanoId(6).toLowerCase();
@ -191,6 +192,19 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
throw new ValidateException(`找不到${domain}的CNAME记录`); throw new ValidateException(`找不到${domain}的CNAME记录`);
} }
} }
if (!record.mainDomain){
let domainPrefix = record.hostRecord.replace("_acme-challenge", "");
if (domainPrefix.startsWith(".")) {
domainPrefix = domainPrefix.substring(1);
}
record.mainDomain = record.domain.replace(domainPrefix, "");
await this.update({
id: record.id,
mainDomain: domainPrefix,
})
}
const provider = await this.cnameProviderService.info(record.cnameProviderId); const provider = await this.cnameProviderService.info(record.cnameProviderId);
if (provider == null) { if (provider == null) {
throw new ValidateException(`找不到${domain}的CNAME服务`); throw new ValidateException(`找不到${domain}的CNAME服务`);
@ -208,7 +222,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
* cname * cname
* @param id * @param id
*/ */
async verify(id: string) { async verify(id: number) {
const bean = await this.info(id); const bean = await this.info(id);
if (!bean) { if (!bean) {
throw new ValidateException(`CnameRecord:${id} 不存在`); throw new ValidateException(`CnameRecord:${id} 不存在`);
@ -440,4 +454,11 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
} }
} }
async resetStatus (id: number) {
if (!id) {
throw new ValidateException('id不能为空');
}
await this.getRepository().update(id, {status: 'cname',mainDomain: ""});
}
} }

View File

@ -68,10 +68,6 @@ export class TaskServiceGetter implements IServiceGetter{
return new DomainVerifierGetter(this.userId, domainService); return new DomainVerifierGetter(this.userId, domainService);
} }
} }
export type TaskServiceCreateReq = {
userId: number;
}
@Provide() @Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TaskServiceBuilder { export class TaskServiceBuilder {
@ -84,6 +80,10 @@ export class TaskServiceBuilder {
} }
} }
export type TaskServiceCreateReq = {
userId: number;
}

View File

@ -1,2 +1,3 @@
export * from './geetest/index.js'; export * from './geetest/index.js';
export * from './image/index.js'; export * from './image/index.js';
export * from './tencent/index.js';

View File

@ -0,0 +1,104 @@
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
import { ICaptchaAddon } from "../api.js";
import { TencentAccess } from "@certd/plugin-lib";
@IsAddon({
addonType:"captcha",
name: 'tencent',
title: '腾讯云验证码',
desc: '',
showTest:false,
})
export class TencentCaptcha extends BaseAddon implements ICaptchaAddon{
@AddonInput({
title: '腾讯云授权',
helper: '腾讯云授权',
component: {
name: 'access-selector',
vModel:"modelValue",
from: "sys",
type: 'tencent', //固定授权类型
},
required: true,
})
accessId :number;
@AddonInput({
title: '验证ID',
component: {
name:"a-input-number",
placeholder: 'CaptchaAppId',
},
helper:"[腾讯云验证码](https://cloud.tencent.com/act/cps/redirect?redirect=37716&cps_key=b3ef73330335d7a6efa4a4bbeeb6b2c9)",
required: true,
})
captchaAppId:number;
@AddonInput({
title: '验证Key',
component: {
placeholder: 'AppSecretKey',
},
required: true,
})
appSecretKey = '';
async onValidate(data?:any) {
if (!data) {
return false
}
const access = await this.getAccess<TencentAccess>(this.accessId)
const sdk =await import("tencentcloud-sdk-nodejs/tencentcloud/services/captcha/v20190722/index.js");
const CaptchaClient = sdk.v20190722.Client;
const clientConfig = {
credential: {
secretId: access.secretId,
secretKey: access.secretKey,
},
region: "",
profile: {
httpProfile: {
endpoint: "captcha.tencentcloudapi.com",
},
},
};
// 实例化要请求产品的client对象,clientProfile是可选的
const client = new CaptchaClient(clientConfig);
const params = {
"CaptchaType": 9, //固定值9
"UserIp": "127.0.0.1",
"Ticket": data.ticket,
"Randstr": data.randstr,
"AppSecretKey": this.appSecretKey,
"CaptchaAppId": this.captchaAppId,
};
const res = await client.DescribeCaptchaResult(params)
if (res.CaptchaCode == 1) {
// 验证成功
// verification successful
return true;
} else {
// 验证失败
// verification failed
this.logger.error("腾讯云验证码验证失败",res.CaptchaMsg)
return false;
}
}
async getCaptcha(): Promise<any> {
return {
captchaAppId: this.captchaAppId,
}
}
}