From 3635fb391059fbcff1244d0fbb5fac57e7970da2 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Thu, 11 Sep 2025 00:19:38 +0800 Subject: [PATCH] chore: --- .../core/pipeline/src/registry/registry.ts | 13 +- .../src/system/settings/service/models.ts | 10 +- .../libs/lib-server/src/user/addon/api/api.ts | 96 +++++++ .../src/user/addon/api/decorator.ts | 65 +++++ .../lib-server/src/user/addon/api/index.ts | 3 + .../lib-server/src/user/addon/api/registry.ts | 3 + .../lib-server/src/user/addon/entity/addon.ts | 44 +++ .../libs/lib-server/src/user/addon/index.ts | 5 + .../src/user/addon/service/addon-getter.ts | 18 ++ .../src/user/addon/service/addon-service.ts | 217 ++++++++++++++ .../user/addon/service/addon-sys-getter.ts | 17 ++ packages/libs/lib-server/src/user/index.ts | 1 + packages/ui/certd-client/index.html | 1 + .../src/store/settings/api.basic.ts | 4 + .../certd-client/src/views/certd/addon/api.ts | 117 ++++++++ .../src/views/certd/addon/common.tsx | 270 ++++++++++++++++++ .../src/views/certd/addon/crud.tsx | 54 ++++ .../src/views/certd/addon/index.vue | 41 +++ .../src/views/framework/login/captcha.vue | 46 +++ .../src/views/framework/login/index.vue | 5 + .../controller/user/addon/addon-controller.ts | 200 +++++++++++++ .../controller/user/login/login-controller.ts | 1 + .../modules/login/service/login-service.ts | 30 +- packages/ui/certd-server/src/plugins/index.ts | 1 + .../plugins/plugin-captcha/geetest/index.ts | 109 +++++++ .../src/plugins/plugin-captcha/index.ts | 1 + 26 files changed, 1368 insertions(+), 4 deletions(-) create mode 100644 packages/libs/lib-server/src/user/addon/api/api.ts create mode 100644 packages/libs/lib-server/src/user/addon/api/decorator.ts create mode 100644 packages/libs/lib-server/src/user/addon/api/index.ts create mode 100644 packages/libs/lib-server/src/user/addon/api/registry.ts create mode 100644 packages/libs/lib-server/src/user/addon/entity/addon.ts create mode 100644 packages/libs/lib-server/src/user/addon/index.ts create mode 100644 packages/libs/lib-server/src/user/addon/service/addon-getter.ts create mode 100644 packages/libs/lib-server/src/user/addon/service/addon-service.ts create mode 100644 packages/libs/lib-server/src/user/addon/service/addon-sys-getter.ts create mode 100644 packages/ui/certd-client/src/views/certd/addon/api.ts create mode 100644 packages/ui/certd-client/src/views/certd/addon/common.tsx create mode 100644 packages/ui/certd-client/src/views/certd/addon/crud.tsx create mode 100644 packages/ui/certd-client/src/views/certd/addon/index.vue create mode 100644 packages/ui/certd-client/src/views/framework/login/captcha.vue create mode 100644 packages/ui/certd-server/src/controller/user/addon/addon-controller.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-captcha/geetest/index.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-captcha/index.ts diff --git a/packages/core/pipeline/src/registry/registry.ts b/packages/core/pipeline/src/registry/registry.ts index b466bcc9..3322d708 100644 --- a/packages/core/pipeline/src/registry/registry.ts +++ b/packages/core/pipeline/src/registry/registry.ts @@ -69,9 +69,15 @@ export class Registry { return this.storage; } - getDefineList() { + getDefineList(prefix?: string) { let list = []; + if (prefix) { + prefix = prefix + ":"; + } for (const key in this.storage) { + if (prefix && !key.startsWith(prefix)) { + continue; + } const define = this.getDefine(key); if (define) { if (define?.deprecated) { @@ -90,7 +96,10 @@ export class Registry { return list; } - getDefine(key: string) { + getDefine(key: string, prefix?: string) { + if (prefix) { + key = prefix + ":" + key; + } const item = this.storage[key]; if (!item) { return; diff --git a/packages/libs/lib-server/src/system/settings/service/models.ts b/packages/libs/lib-server/src/system/settings/service/models.ts index c31f14fb..08431bbd 100644 --- a/packages/libs/lib-server/src/system/settings/service/models.ts +++ b/packages/libs/lib-server/src/system/settings/service/models.ts @@ -30,6 +30,12 @@ export class SysPublicSettings extends BaseSettings { mpsNo?: string; robots?: boolean = true; aiChatEnabled = true; + + + //验证码是否开启 + captchaEnabled = false; + //验证码类型 + captchaType?: string; } export class SysPrivateSettings extends BaseSettings { @@ -44,6 +50,9 @@ export class SysPrivateSettings extends BaseSettings { dnsResultOrder? = ''; commonCnameEnabled?: boolean = true; + //验证码配置id + captchaAddonId?: number; + sms?: { type?: string; config?: any; @@ -207,4 +216,3 @@ export class SysSafeSetting extends BaseSettings { }; } - diff --git a/packages/libs/lib-server/src/user/addon/api/api.ts b/packages/libs/lib-server/src/user/addon/api/api.ts new file mode 100644 index 00000000..560bccf2 --- /dev/null +++ b/packages/libs/lib-server/src/user/addon/api/api.ts @@ -0,0 +1,96 @@ +import { HttpClient, ILogger, utils } from "@certd/basic"; +import {upperFirst} from "lodash-es"; +import { FormItemProps, PluginRequestHandleReq, Registrable } from "@certd/pipeline"; + + +export type AddonRequestHandleReqInput = { + id?: number; + title?: string; + addon: T; +}; + +export type AddonRequestHandleReq = { + addonType: string; +} &PluginRequestHandleReq>; + +export type AddonInputDefine = FormItemProps & { + title: string; + required?: boolean; +}; +export type AddonDefine = Registrable & { + addonType: string; + needPlus?: boolean; + input?: { + [key: string]: AddonInputDefine; + }; +}; + +export type AddonInstanceConfig = { + id: number; + addonType: string; + type: string; + name: string; + userId: number; + setting: { + [key: string]: any; + }; +}; + + + +export interface IAddon { + ctx: AddonContext; + [key: string]: any; +} + +export type AddonContext = { + http: HttpClient; + logger: ILogger; + utils: typeof utils; +}; + +export abstract class BaseAddon implements IAddon { + define!: AddonDefine; + ctx!: AddonContext; + http!: HttpClient; + logger!: ILogger; + + + + // eslint-disable-next-line @typescript-eslint/no-empty-function + async onInstance() {} + setCtx(ctx: AddonContext) { + this.ctx = ctx; + this.http = ctx.http; + this.logger = ctx.logger; + } + setDefine = (define:AddonDefine) => { + this.define = define; + }; + + async onRequest(req:AddonRequestHandleReq) { + if (!req.action) { + throw new Error("action is required"); + } + + let methodName = req.action; + if (!req.action.startsWith("on")) { + methodName = `on${upperFirst(req.action)}`; + } + + // @ts-ignore + const method = this[methodName]; + if (method) { + // @ts-ignore + return await this[methodName](req.data); + } + throw new Error(`action ${req.action} not found`); + } + +} + + +export interface IAddonGetter { + getById(id: any): Promise; + getCommonById(id: any): Promise; +} diff --git a/packages/libs/lib-server/src/user/addon/api/decorator.ts b/packages/libs/lib-server/src/user/addon/api/decorator.ts new file mode 100644 index 00000000..6a96b49a --- /dev/null +++ b/packages/libs/lib-server/src/user/addon/api/decorator.ts @@ -0,0 +1,65 @@ +// src/decorator/memoryCache.decorator.ts +import * as _ from "lodash-es"; +import { merge } from "lodash-es"; +import { addonRegistry } from "./registry.js"; +import { AddonContext, AddonDefine, AddonInputDefine } from "./api.js"; +import { Decorator } from "@certd/pipeline"; + +// 提供一个唯一 key +export const ADDON_CLASS_KEY = "pipeline:addon"; +export const ADDON_INPUT_KEY = "pipeline:addon:input"; + +export function IsAddon(define: AddonDefine): ClassDecorator { + return (target: any) => { + target = Decorator.target(target); + + const inputs: any = {}; + const properties = Decorator.getClassProperties(target); + for (const property in properties) { + const input = Reflect.getMetadata(ADDON_INPUT_KEY, target, property); + if (input) { + inputs[property] = input; + } + } + _.merge(define, { input: inputs }); + Reflect.defineMetadata(ADDON_CLASS_KEY, define, target); + target.define = define; + const key = `${define.addonType}:${define.name}`; + addonRegistry.register(key, { + define, + target: async () => { + return target; + }, + }); + }; +} + +export function AddonInput(input?: AddonInputDefine): PropertyDecorator { + return (target, propertyKey) => { + target = Decorator.target(target, propertyKey); + // const _type = Reflect.getMetadata("design:type", target, propertyKey); + Reflect.defineMetadata(ADDON_INPUT_KEY, input, target, propertyKey); + }; +} + +export async function newAddon(addonType:string,type: string, input: any, ctx: AddonContext) { + const key = `${addonType}:${type}` + const register = addonRegistry.get(key); + if (register == null) { + throw new Error(`${addonType} ${type} not found`); + } + // @ts-ignore + const pluginCls = await register.target(); + // @ts-ignore + const plugin = new pluginCls(); + merge(plugin, input); + if (!ctx) { + throw new Error("ctx is required"); + } + plugin.setDefine(register.define); + plugin.setCtx(ctx); + await plugin.onInstance(); + return plugin; +} + + diff --git a/packages/libs/lib-server/src/user/addon/api/index.ts b/packages/libs/lib-server/src/user/addon/api/index.ts new file mode 100644 index 00000000..9b9e3a48 --- /dev/null +++ b/packages/libs/lib-server/src/user/addon/api/index.ts @@ -0,0 +1,3 @@ +export * from "./api.js"; +export * from "./registry.js"; +export * from "./decorator.js"; diff --git a/packages/libs/lib-server/src/user/addon/api/registry.ts b/packages/libs/lib-server/src/user/addon/api/registry.ts new file mode 100644 index 00000000..643de99c --- /dev/null +++ b/packages/libs/lib-server/src/user/addon/api/registry.ts @@ -0,0 +1,3 @@ +import { createRegistry } from "@certd/pipeline"; + +export const addonRegistry = createRegistry("addon"); diff --git a/packages/libs/lib-server/src/user/addon/entity/addon.ts b/packages/libs/lib-server/src/user/addon/entity/addon.ts new file mode 100644 index 00000000..f62de283 --- /dev/null +++ b/packages/libs/lib-server/src/user/addon/entity/addon.ts @@ -0,0 +1,44 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +/** + */ +@Entity('cd_addon') +export class AddonEntity { + @PrimaryGeneratedColumn() + id: number; + @Column({ name: 'user_id', comment: '用户id' }) + userId: number; + @Column({ comment: '名称', length: 100 }) + name: string; + + + @Column({ comment: 'addon类型', length: 100 }) + addonType: string; + + + @Column({ comment: '类型', length: 100 }) + type: string; + + @Column({ name: 'setting', comment: '设置', length: 10240, nullable: true }) + setting: string; + + @Column({ name: 'is_system', comment: '是否系统级别', nullable: false, default: false }) + isSystem: boolean; + + @Column({ name: 'is_default', comment: '是否默认', nullable: false, default: false }) + isDefault: boolean; + + + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; +} diff --git a/packages/libs/lib-server/src/user/addon/index.ts b/packages/libs/lib-server/src/user/addon/index.ts new file mode 100644 index 00000000..727232b4 --- /dev/null +++ b/packages/libs/lib-server/src/user/addon/index.ts @@ -0,0 +1,5 @@ +export * from './api/index.js' +export * from './entity/addon.js' +export * from './service/addon-service.js' +export * from './service/addon-getter.js' +export * from './service/addon-sys-getter.js' diff --git a/packages/libs/lib-server/src/user/addon/service/addon-getter.ts b/packages/libs/lib-server/src/user/addon/service/addon-getter.ts new file mode 100644 index 00000000..8e91ad7e --- /dev/null +++ b/packages/libs/lib-server/src/user/addon/service/addon-getter.ts @@ -0,0 +1,18 @@ +import { IAddonGetter } from "../api/index.js"; + +export class AddonGetter implements IAddonGetter { + userId: number; + getter: (id: any, userId?: number) => Promise; + constructor(userId: number, getter: (id: any, userId: number) => Promise) { + this.userId = userId; + this.getter = getter; + } + + async getById(id: any) { + return await this.getter(id, this.userId); + } + + async getCommonById(id: any) { + return await this.getter(id, 0); + } +} diff --git a/packages/libs/lib-server/src/user/addon/service/addon-service.ts b/packages/libs/lib-server/src/user/addon/service/addon-service.ts new file mode 100644 index 00000000..f4834c27 --- /dev/null +++ b/packages/libs/lib-server/src/user/addon/service/addon-service.ts @@ -0,0 +1,217 @@ +import { Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { In, Repository } from "typeorm"; +import { AddonDefine, BaseService, PageReq, PermissionException, ValidateException } from "../../../index.js"; +import { addonRegistry, newAddon } from "../api/index.js"; +import { AddonEntity } from "../entity/addon.js"; +import { http, logger, utils } from "@certd/basic"; + +/** + * Addon + */ +@Provide() +@Scope(ScopeEnum.Request, {allowDowngrade: true}) +export class AddonService extends BaseService { + @InjectEntityModel(AddonEntity) + repository: Repository; + + //@ts-ignore + getRepository() { + return this.repository; + } + + async page(pageReq: PageReq) { + const res = await super.page(pageReq); + res.records = res.records.map(item => { + return item; + }); + return res; + } + + async add(param) { + let oldEntity = null; + if (param._copyFrom){ + oldEntity = await this.info(param._copyFrom); + if (oldEntity == null) { + throw new ValidateException('该Addon配置不存在,请确认是否已被删除'); + } + if (oldEntity.userId !== param.userId) { + throw new ValidateException('您无权查看该Addon配置'); + } + } + delete param._copyFrom + return await super.add(param); + } + + + /** + * 修改 + * @param param 数据 + */ + async update(param) { + const oldEntity = await this.info(param.id); + if (oldEntity == null) { + throw new ValidateException('该Addon配置不存在,请确认是否已被删除'); + } + return await super.update(param); + } + + async getSimpleInfo(id: number) { + const entity = await this.info(id); + if (entity == null) { + throw new ValidateException('该Addon配置不存在,请确认是否已被删除'); + } + return { + id: entity.id, + name: entity.name, + userId: entity.userId, + }; + } + + async getAddonById(id: any, checkUserId: boolean, userId?: number): Promise { + const entity = await this.info(id); + if (entity == null) { + throw new Error(`该Addon配置不存在,请确认是否已被删除:id=${id}`); + } + if (checkUserId) { + if (userId == null) { + throw new ValidateException('userId不能为空'); + } + if (userId !== entity.userId) { + throw new PermissionException('您对该Addon无访问权限'); + } + } + + // const access = accessRegistry.get(entity.type); + const setting = JSON.parse(entity.setting ??"{}") + const input = { + id: entity.id, + ...setting, + }; + const ctx = { + http: http, + logger: logger, + utils: utils, + }; + return await newAddon(entity.addonType, entity.type, input,ctx); + } + + async getById(id: any, userId: number): Promise { + return await this.getAddonById(id, true, userId); + } + + + getDefineList(addonType: string) { + return addonRegistry.getDefineList(); + } + + getDefineByType(type: string,prefix?: string) { + return addonRegistry.getDefine(type,prefix) as AddonDefine; + } + + + async getSimpleByIds(ids: number[], userId: any) { + if (ids.length === 0) { + return []; + } + if (!userId) { + return []; + } + return await this.repository.find({ + where: { + id: In(ids), + userId, + }, + select: { + id: true, + name: true, + addonType: true, + type: true, + userId:true, + isSystem: true, + }, + }); + + } + + + + async getDefault(userId: number,addonType: string): Promise { + const res = await this.repository.findOne({ + where: { + userId, + addonType + }, + order: { + isDefault: 'DESC', + }, + }); + if (!res) { + return null; + } + return this.buildAddonInstanceConfig(res); + } + + private buildAddonInstanceConfig(res: AddonEntity) { + const setting = JSON.parse(res.setting); + return { + id: res.id, + addonType: res.addonType, + type: res.type, + name: res.name, + userId: res.userId, + setting, + }; + } + + async setDefault(id: number, userId: number,addonType:string) { + if (!id) { + throw new ValidateException('id不能为空'); + } + if (!userId) { + throw new ValidateException('userId不能为空'); + } + await this.repository.update( + { + userId, + addonType + }, + { + isDefault: false, + } + ); + await this.repository.update( + { + id, + userId, + addonType + }, + { + isDefault: true, + } + ); + } + + async getOrCreateDefault(opts:{addonType:string,type:string, inputs: any, userId: any}) { + const {addonType,type,inputs,userId} = opts; + + const addonDefine = this.getDefineByType( type,addonType) + + const defaultConfig = await this.getDefault(userId); + if (defaultConfig) { + return defaultConfig; + } + const setting = { + ...inputs, + }; + const res = await this.repository.save({ + userId, + addonType, + type: type, + name: addonDefine.title, + setting: JSON.stringify(setting), + isDefault: true, + }); + return this.buildAddonInstanceConfig(res); + } +} diff --git a/packages/libs/lib-server/src/user/addon/service/addon-sys-getter.ts b/packages/libs/lib-server/src/user/addon/service/addon-sys-getter.ts new file mode 100644 index 00000000..773e1a7a --- /dev/null +++ b/packages/libs/lib-server/src/user/addon/service/addon-sys-getter.ts @@ -0,0 +1,17 @@ +import { IAccessService } from '@certd/pipeline'; +import { AddonService } from './addon-service.js'; + +export class AddonSysGetter implements IAccessService { + addonService: AddonService; + constructor(addonService: AddonService) { + this.addonService = addonService; + } + + async getById(id: any) { + return await this.addonService.getById(id, 0); + } + + async getCommonById(id: any) { + return await this.addonService.getById(id, 0); + } +} diff --git a/packages/libs/lib-server/src/user/index.ts b/packages/libs/lib-server/src/user/index.ts index 17e3af2c..f0fce929 100644 --- a/packages/libs/lib-server/src/user/index.ts +++ b/packages/libs/lib-server/src/user/index.ts @@ -1 +1,2 @@ export * from './access/index.js'; +export * from './addon/index.js'; diff --git a/packages/ui/certd-client/index.html b/packages/ui/certd-client/index.html index 1740a304..d760b2f5 100644 --- a/packages/ui/certd-client/index.html +++ b/packages/ui/certd-client/index.html @@ -23,5 +23,6 @@ + diff --git a/packages/ui/certd-client/src/store/settings/api.basic.ts b/packages/ui/certd-client/src/store/settings/api.basic.ts index ceaa1681..ebe7b691 100644 --- a/packages/ui/certd-client/src/store/settings/api.basic.ts +++ b/packages/ui/certd-client/src/store/settings/api.basic.ts @@ -46,6 +46,10 @@ export type SysPublicSetting = { aiChatEnabled?: boolean; showRunStrategy?: boolean; + + captchaEnabled?: boolean; + captchaType?: number; + captchaAddonId?: number; }; export type SuiteSetting = { enabled?: boolean; diff --git a/packages/ui/certd-client/src/views/certd/addon/api.ts b/packages/ui/certd-client/src/views/certd/addon/api.ts new file mode 100644 index 00000000..818a49c1 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/addon/api.ts @@ -0,0 +1,117 @@ +import { request } from "/src/api/service"; +import { RequestHandleReq } from "/@/components/plugins/lib"; + +export function createAddonApi() { + const apiPrefix = "/addon"; + return { + async GetList(query: any) { + return await request({ + url: apiPrefix + "/page", + method: "post", + data: query, + }); + }, + + async AddObj(obj: any) { + return await request({ + url: apiPrefix + "/add", + method: "post", + data: obj, + }); + }, + + async UpdateObj(obj: any) { + return await request({ + url: apiPrefix + "/update", + method: "post", + data: obj, + }); + }, + + async DelObj(id: number) { + return await request({ + url: apiPrefix + "/delete", + method: "post", + params: { id }, + }); + }, + + async GetObj(id: number) { + return await request({ + url: apiPrefix + "/info", + method: "post", + params: { id }, + }); + }, + + async GetOptions(id: number) { + return await request({ + url: apiPrefix + "/options", + method: "post", + }); + }, + + async SetDefault(id: number) { + return await request({ + url: apiPrefix + "/setDefault", + method: "post", + params: { id }, + }); + }, + + async GetDefaultId() { + return await request({ + url: apiPrefix + "/getDefaultId", + method: "post", + }); + }, + + async GetSimpleInfo(id: number) { + return await request({ + url: apiPrefix + "/simpleInfo", + method: "post", + params: { id }, + }); + }, + + async GetDefineTypes(addonType: string) { + return await request({ + url: apiPrefix + `/getTypeDict?addonType=${addonType}`, + method: "post", + }); + }, + + async GetProviderDefine(type: string) { + return await request({ + url: apiPrefix + "/define", + method: "post", + params: { type }, + }); + }, + + async GetProviderDefineByType(type: string) { + return await request({ + url: apiPrefix + "/defineByType", + method: "post", + params: { type }, + }); + }, + + async Handle(req: RequestHandleReq, opts: any = {}) { + const url = `/pi/handle/${req.type}`; + const { typeName, action, data, input } = req; + const res = await request({ + url, + method: "post", + data: { + typeName, + action, + data, + input, + }, + ...opts, + }); + return res; + }, + }; +} diff --git a/packages/ui/certd-client/src/views/certd/addon/common.tsx b/packages/ui/certd-client/src/views/certd/addon/common.tsx new file mode 100644 index 00000000..7d123a3b --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/addon/common.tsx @@ -0,0 +1,270 @@ +import { ColumnCompositionProps, compute, dict } from "@fast-crud/fast-crud"; +import { computed, provide, ref, toRef } from "vue"; +import { useReference } from "/@/use/use-refrence"; +import { forEach, get, merge, set } from "lodash-es"; +import { Modal } from "ant-design-vue"; +import { mitter } from "/@/utils/util.mitt"; +import { useI18n } from "/src/locales"; + +export function addonProvide(api: any) { + provide("addonApi", api); + provide("get:plugin:type", () => { + return "addon"; + }); +} + +export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) { + const { t } = useI18n(); + const addonTypeTypeDictRef = dict({ + data: [{ value: "captcha", label: "验证码" }], + }); + const addonTypeDictRef = dict({ + url: "/addon/getTypeDict?addonType=captcha", + }); + const defaultPluginConfig = { + component: { + name: "a-input", + vModel: "value", + }, + }; + + function buildDefineFields(define: any, form: any, mode: string) { + const formWrapperRef = crudExpose.getFormWrapperRef(); + const columnsRef = toRef(formWrapperRef.formOptions, "columns"); + + for (const key in columnsRef.value) { + if (key.indexOf(".") >= 0) { + delete columnsRef.value[key]; + } + } + console.log('crudBinding.value[mode + "Form"].columns', columnsRef.value); + forEach(define.input, (value: any, mapKey: any) => { + const key = "body." + mapKey; + const field = { + ...value, + key, + }; + const column = merge({ title: key }, defaultPluginConfig, field); + //eval + useReference(column); + + if (column.required) { + if (!column.rules) { + column.rules = []; + } + column.rules.push({ required: true, message: t("certd.requiredField") }); + } + + //设置默认值 + if (column.value != null && get(form, key) == null) { + set(form, key, column.value); + } + //字段配置赋值 + columnsRef.value[key] = column; + console.log("form", columnsRef.value, form); + }); + } + + const currentDefine = ref(); + + return { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 100, + }, + form: { + show: false, + }, + }, + addonType: { + title: "Addon类型", + type: "dict-select", + dict: addonTypeTypeDictRef, + search: { + show: false, + }, + column: { + width: 200, + component: { + color: "auto", + }, + }, + form: { + onChange(ctx: { value: any }) { + addonTypeDictRef.url = `/addon/getTypeDict?addonType=${ctx.value}`; + }, + }, + editForm: { + component: { + disabled: false, + }, + }, + }, + type: { + title: t("certd.notificationType"), + type: "dict-select", + dict: addonTypeDictRef, + search: { + show: false, + }, + column: { + width: 200, + component: { + color: "auto", + }, + }, + editForm: { + component: { + disabled: false, + }, + }, + form: { + component: { + disabled: false, + showSearch: true, + filterOption: (input: string, option: any) => { + input = input?.toLowerCase(); + return option.value.toLowerCase().indexOf(input) >= 0 || option.label.toLowerCase().indexOf(input) >= 0; + }, + renderLabel(item: any) { + return ( + + {item.label} + {item.needPlus && } + + ); + }, + }, + rules: [{ required: true, message: t("certd.selectNotificationType") }], + valueChange: { + immediate: true, + async handle({ value, mode, form, immediate }) { + if (value == null) { + return; + } + const lastTitle = currentDefine.value?.title; + const define = await api.GetProviderDefine(value); + currentDefine.value = define; + console.log("define", define); + + if (!immediate) { + form.body = {}; + if (define.needPlus) { + mitter.emit("openVipModal"); + } + } + + if (!form.name || form.name === lastTitle) { + form.name = define.title; + } + buildDefineFields(define, form, mode); + }, + }, + helper: computed(() => { + const define = currentDefine.value; + if (define == null) { + return ""; + } + return define.desc; + }), + }, + } as ColumnCompositionProps, + name: { + title: t("certd.notificationName"), + search: { + show: true, + }, + type: ["text"], + form: { + rules: [{ required: true, message: t("certd.enterName") }], + helper: t("certd.helperNotificationName"), + }, + column: { + width: 200, + }, + }, + isDefault: { + title: t("certd.isDefault"), + type: "dict-switch", + dict: dict({ + data: [ + { label: t("certd.yes"), value: true, color: "success" }, + { label: t("certd.no"), value: false, color: "default" }, + ], + }), + form: { + value: false, + rules: [{ required: true, message: t("certd.selectIsDefault") }], + order: 999, + }, + column: { + align: "center", + width: 100, + component: { + name: "a-switch", + vModel: "checked", + disabled: compute(({ value }) => { + return value === true; + }), + on: { + change({ row }) { + Modal.confirm({ + title: t("certd.prompt"), + content: t("certd.confirmSetDefaultNotification"), + onOk: async () => { + await api.SetDefault(row.id); + await crudExpose.doRefresh(); + }, + onCancel: async () => { + await crudExpose.doRefresh(); + }, + }); + }, + }, + }, + }, + } as ColumnCompositionProps, + test: { + title: t("certd.test"), + form: { + show: compute(({ form }) => { + return !!form.type; + }), + component: { + name: "api-test", + action: "TestRequest", + }, + order: 990, + col: { + span: 24, + }, + }, + column: { + show: false, + }, + }, + setting: { + column: { show: false }, + form: { + show: false, + valueBuilder({ value, form }) { + form.body = {}; + if (!value) { + return; + } + const setting = JSON.parse(value); + for (const key in setting) { + form.body[key] = setting[key]; + } + }, + valueResolve({ form }) { + const setting = form.body; + form.setting = JSON.stringify(setting); + }, + }, + } as ColumnCompositionProps, + }; +} diff --git a/packages/ui/certd-client/src/views/certd/addon/crud.tsx b/packages/ui/certd-client/src/views/certd/addon/crud.tsx new file mode 100644 index 00000000..5bc56a03 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/addon/crud.tsx @@ -0,0 +1,54 @@ +import { ref } from "vue"; +import { getCommonColumnDefine } from "./common"; +import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; +import { createAddonApi } from "/@/views/certd/addon/api"; +const api = createAddonApi(); +export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { + const pageRequest = async (query: UserPageQuery): Promise => { + return await api.GetList(query); + }; + const editRequest = async (req: EditReq) => { + const { form, row } = req; + form.id = row.id; + const res = await api.UpdateObj(form); + return res; + }; + const delRequest = async (req: DelReq) => { + const { row } = req; + return await api.DelObj(row.id); + }; + + const addRequest = async (req: AddReq) => { + const { form } = req; + const res = await api.AddObj(form); + return res; + }; + + const typeRef = ref(); + const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api); + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + form: { + labelCol: { + //固定label宽度 + span: null, + style: { + width: "145px", + }, + }, + }, + rowHandle: { + width: 200, + }, + columns: { + ...commonColumnsDefine, + }, + }, + }; +} diff --git a/packages/ui/certd-client/src/views/certd/addon/index.vue b/packages/ui/certd-client/src/views/certd/addon/index.vue new file mode 100644 index 00000000..059404bb --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/addon/index.vue @@ -0,0 +1,41 @@ + + + diff --git a/packages/ui/certd-client/src/views/framework/login/captcha.vue b/packages/ui/certd-client/src/views/framework/login/captcha.vue new file mode 100644 index 00000000..2d9d2fdd --- /dev/null +++ b/packages/ui/certd-client/src/views/framework/login/captcha.vue @@ -0,0 +1,46 @@ + + diff --git a/packages/ui/certd-client/src/views/framework/login/index.vue b/packages/ui/certd-client/src/views/framework/login/index.vue index 604aa0d3..ff38ca56 100644 --- a/packages/ui/certd-client/src/views/framework/login/index.vue +++ b/packages/ui/certd-client/src/views/framework/login/index.vue @@ -20,6 +20,10 @@ + + + + @@ -111,6 +115,7 @@ export default defineComponent({ imgCode: "", smsCode: "", randomStr: "", + captcha: {}, }); const rules = { diff --git a/packages/ui/certd-server/src/controller/user/addon/addon-controller.ts b/packages/ui/certd-server/src/controller/user/addon/addon-controller.ts new file mode 100644 index 00000000..de370ef4 --- /dev/null +++ b/packages/ui/certd-server/src/controller/user/addon/addon-controller.ts @@ -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 { + @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); + } +} diff --git a/packages/ui/certd-server/src/controller/user/login/login-controller.ts b/packages/ui/certd-server/src/controller/user/login/login-controller.ts index 2877c257..7d2dfc33 100644 --- a/packages/ui/certd-server/src/controller/user/login/login-controller.ts +++ b/packages/ui/certd-server/src/controller/user/login/login-controller.ts @@ -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); diff --git a/packages/ui/certd-server/src/modules/login/service/login-service.ts b/packages/ui/certd-server/src/modules/login/service/login-service.ts index ed7cc39b..fe02f93a 100644 --- a/packages/ui/certd-server/src/modules/login/service/login-service.ts +++ b/packages/ui/certd-server/src/modules/login/service/login-service.ts @@ -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 }) { diff --git a/packages/ui/certd-server/src/plugins/index.ts b/packages/ui/certd-server/src/plugins/index.ts index 92da08bc..85e4d93a 100644 --- a/packages/ui/certd-server/src/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/index.ts @@ -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' diff --git a/packages/ui/certd-server/src/plugins/plugin-captcha/geetest/index.ts b/packages/ui/certd-server/src/plugins/plugin-captcha/geetest/index.ts new file mode 100644 index 00000000..033b4dae --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-captcha/geetest/index.ts @@ -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; + } + + +} diff --git a/packages/ui/certd-server/src/plugins/plugin-captcha/index.ts b/packages/ui/certd-server/src/plugins/plugin-captcha/index.ts new file mode 100644 index 00000000..3be7e371 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-captcha/index.ts @@ -0,0 +1 @@ +export * from './geetest/index.js';