This commit is contained in:
xiaojunnuo
2025-09-11 00:19:38 +08:00
parent d2ecfe5491
commit 3635fb3910
26 changed files with 1368 additions and 4 deletions

View File

@@ -0,0 +1,200 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import {
AccessGetter,
AddonRequestHandleReq,
Constants,
CrudController,
newAddon,
ValidateException
} from "@certd/lib-server";
import { AuthService } from '../../../modules/sys/authority/service/auth-service.js';
import { checkPlus } from '@certd/plus-core';
import { AddonService } from "@certd/lib-server";
import { AddonDefine } from "@certd/lib-server";
import { AccessRequestHandleReq, newAccess } from "@certd/pipeline";
import { http, logger, utils } from "@certd/basic";
/**
* Addon
*/
@Provide()
@Controller('/api/addon')
export class AddonController extends CrudController<AddonService> {
@Inject()
service: AddonService;
@Inject()
authService: AuthService;
getService(): AddonService {
return this.service;
}
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body) {
body.query = body.query ?? {};
delete body.query.userId;
const buildQuery = qb => {
qb.andWhere('user_id = :userId', { userId: this.getUserId() });
};
const res = await this.service.page({
query: body.query,
page: body.page,
sort: body.sort,
buildQuery,
});
return this.ok(res);
}
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
return super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean) {
bean.userId = this.getUserId();
const type = bean.type;
const addonType = bean.addonType;
if (! type || !addonType){
throw new ValidateException('请选择Addon类型');
}
const define: AddonDefine = this.service.getDefineByType(type,addonType);
if (!define) {
throw new ValidateException('Addon类型不存在');
}
if (define.needPlus) {
checkPlus();
}
return super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
const old = await this.service.info(bean.id);
if (!old) {
throw new ValidateException('Addon配置不存在');
}
if (old.type !== bean.type ) {
const addonType = old.type;
const type = bean.type;
const define: AddonDefine = this.service.getDefineByType(type,addonType);
if (!define) {
throw new ValidateException('Addon类型不存在');
}
if (define.needPlus) {
checkPlus();
}
}
delete bean.userId;
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.delete(id);
}
@Post('/define', { summary: Constants.per.authOnly })
async define(@Query('type') type: string,@Query('addonType') addonType: string) {
const notification = this.service.getDefineByType(type,addonType);
return this.ok(notification);
}
@Post('/getTypeDict', { summary: Constants.per.authOnly })
async getTypeDict(@Query('addonType') addonType: string) {
const list: any = this.service.getDefineList(addonType);
let dict = [];
for (const item of list) {
dict.push({
value: item.name,
label: item.title,
needPlus: item.needPlus ?? false,
icon: item.icon,
});
}
dict = dict.sort(a => {
return a.needPlus ? 0 : -1;
});
return this.ok(dict);
}
@Post('/simpleInfo', { summary: Constants.per.authOnly })
async simpleInfo(@Query('addonType') addonType: string,@Query('id') id: number) {
if (id === 0) {
//获取默认
const res = await this.service.getDefault(this.getUserId(),addonType);
if (!res) {
throw new ValidateException('默认Addon配置不存在');
}
const simple = await this.service.getSimpleInfo(res.id);
return this.ok(simple);
}
await this.authService.checkEntityUserId(this.ctx, this.service, id);
const res = await this.service.getSimpleInfo(id);
return this.ok(res);
}
@Post('/getDefaultId', { summary: Constants.per.authOnly })
async getDefaultId(@Query('addonType') addonType: string) {
const res = await this.service.getDefault(this.getUserId(),addonType);
return this.ok(res?.id);
}
@Post('/setDefault', { summary: Constants.per.authOnly })
async setDefault(@Query('addonType') addonType: string,@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
const res = await this.service.setDefault(id, this.getUserId(),addonType);
return this.ok(res);
}
@Post('/options', { summary: Constants.per.authOnly })
async options(@Query('addonType') addonType: string) {
const res = await this.service.list({
query: {
userId: this.getUserId(),
addonType
},
});
for (const item of res) {
delete item.setting;
}
return this.ok(res);
}
@Post('/handle', { summary: Constants.per.authOnly })
async handle(@Body(ALL) body: AddonRequestHandleReq) {
const userId = this.getUserId();
let inputAddon = body.input.addon;
if (body.input.id > 0) {
const oldEntity = await this.service.info(body.input.id);
if (oldEntity) {
if (oldEntity.userId !== userId) {
throw new Error('addon not found');
}
// const param: any = {
// type: body.typeName,
// setting: JSON.stringify(body.input.access),
// };
inputAddon = JSON.parse( oldEntity.setting)
}
}
const ctx = {
http: http,
logger:logger,
utils:utils,
}
const addon = await newAddon(body.addonType,body.typeName, inputAddon,ctx);
const res = await addon.onRequest(body);
return this.ok(res);
}
}

View File

@@ -22,6 +22,7 @@ export class LoginController extends BaseController {
@Body(ALL)
user: any
) {
await this.loginService.doCaptchaValidate({form:user})
const token = await this.loginService.loginByPassword(user);
this.writeTokenCookie(token);
return this.ok(token);

View File

@@ -6,12 +6,13 @@ import {RoleService} from '../../sys/authority/service/role-service.js';
import {UserEntity} from '../../sys/authority/entity/user.js';
import {SysSettingsService} from '@certd/lib-server';
import {SysPrivateSettings} from '@certd/lib-server';
import {cache, utils} from '@certd/basic';
import { cache, logger, utils } from "@certd/basic";
import {LoginErrorException} from '@certd/lib-server/dist/basic/exception/login-error-exception.js';
import {CodeService} from '../../basic/service/code-service.js';
import {TwoFactorService} from "../../mine/service/two-factor-service.js";
import {UserSettingsService} from '../../mine/service/user-settings-service.js';
import {isPlus} from "@certd/plus-core";
import { AddonService } from "@certd/lib-server/dist/user/addon/service/addon-service.js";
/**
* 系统用户
@@ -35,6 +36,8 @@ export class LoginService {
userSettingsService: UserSettingsService;
@Inject()
twoFactorService: TwoFactorService;
@Inject()
addonService: AddonService;
checkIsBlocked(username: string) {
const blockDurationKey = `login_block_duration:${username}`;
@@ -97,6 +100,31 @@ export class LoginService {
throw new LoginErrorException(errorMessage, leftTimes);
}
async doCaptchaValidate(opts:{form:any}){
const pubSetting = await this.sysSettingsService.getPublicSettings()
if (pubSetting.captchaEnabled) {
const prvSetting = await this.sysSettingsService.getPrivateSettings()
const addon = await this.addonService.getById(prvSetting.captchaAddonId,0)
if (!addon) {
logger.warn('验证码插件还未配置,忽略验证码校验')
return true
}
if (addon.addonType !== pubSetting.captchaType) {
logger.warn('验证码插件类型错误,忽略验证码校验')
return true
}
return await addon.onValidate(opts.form)
}
return true
}
async loginBySmsCode(req: { mobile: string; phoneCode: string; smsCode: string; randomStr: string }) {

View File

@@ -35,3 +35,4 @@ export * from './plugin-ksyun/index.js'
export * from './plugin-apisix/index.js'
export * from './plugin-dokploy/index.js'
export * from './plugin-godaddy/index.js'
export * from './plugin-captcha/index.js'

View File

@@ -0,0 +1,109 @@
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server/dist/user/addon/api/index.js";
import crypto from 'crypto';
@IsAddon({
addonType:"captcha",
name: 'geetest',
title: '极验验证码',
desc: '',
})
export class GeeTestCaptcha extends BaseAddon {
@AddonInput({
title: 'captchaId',
component: {
placeholder: 'captchaId',
},
required: true,
})
captchaId = '';
@AddonInput({
title: 'captchaKey',
component: {
placeholder: 'captchaKey',
},
required: true,
})
captchaKey = '';
async onValidate(data?:any) {
// geetest 服务地址
// geetest server url
const API_SERVER = "http://gcaptcha4.geetest.com";
// geetest 验证接口
// geetest server interface
const API_URL = API_SERVER + "/validate" + "?captcha_id=" + this.captchaId;
// 前端参数
// web parameter
var lot_number = data['lot_number'];
var captcha_output = data['captcha_output'];
var pass_token = data['pass_token'];
var gen_time = data['gen_time'];
// 生成签名, 使用标准的hmac算法使用用户当前完成验证的流水号lot_number作为原始消息message使用客户验证私钥作为key
// 采用sha256散列算法将message和key进行单向散列生成最终的 “sign_token” 签名
// use lot_number + CAPTCHA_KEY, generate the signature
var sign_token = this.hmac_sha256_encode(lot_number, this.captchaKey);
// 向极验转发前端数据 + “sign_token” 签名
// send web parameter and “sign_token” to geetest server
var datas = {
'lot_number': lot_number,
'captcha_output': captcha_output,
'pass_token': pass_token,
'gen_time': gen_time,
'sign_token': sign_token
};
// post request
// 根据极验返回的用户验证状态, 网站主进行自己的业务逻辑
// According to the user authentication status returned by the geetest, the website owner carries out his own business logic
try{
const res = await this.doRequest(datas, API_URL)
if (res.result == "success") {
// 验证成功
// verification successful
return true;
} else {
// 验证失败
// verification failed
this.logger.error("极验验证不通过 ",res.reason)
return false;
}
}catch (e) {
this.ctx.logger.error("极验验证服务异常",e)
return true
}
}
// 生成签名
// Generate signature
hmac_sha256_encode(value, key){
var hash = crypto.createHmac("sha256", key)
.update(value, 'utf8')
.digest('hex');
return hash;
}
// 发送post请求, 响应json数据如{"result": "success", "reason": "", "captcha_args": {}}
// Send a post request and respond to JSON data, such as: {result ":" success "," reason ":" "," captcha_args ": {}}
async doRequest(datas, url){
var options = {
url: url,
method: "POST",
params: datas,
timeout: 5000
};
const result = await this.ctx.http.request(options);
return result.data;
}
}

View File

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