v2-dev-addon
xiaojunnuo 2025-09-11 00:19:38 +08:00
parent d2ecfe5491
commit 3635fb3910
26 changed files with 1368 additions and 4 deletions

View File

@ -69,9 +69,15 @@ export class Registry<T = any> {
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<T = any> {
return list;
}
getDefine(key: string) {
getDefine(key: string, prefix?: string) {
if (prefix) {
key = prefix + ":" + key;
}
const item = this.storage[key];
if (!item) {
return;

View File

@ -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 {
};
}

View File

@ -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<T = any> = {
id?: number;
title?: string;
addon: T;
};
export type AddonRequestHandleReq<T = any> = {
addonType: string;
} &PluginRequestHandleReq<AddonRequestHandleReqInput<T>>;
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<T = any>(id: any): Promise<T>;
getCommonById<T = any>(id: any): Promise<T>;
}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
export * from "./api.js";
export * from "./registry.js";
export * from "./decorator.js";

View File

@ -0,0 +1,3 @@
import { createRegistry } from "@certd/pipeline";
export const addonRegistry = createRegistry("addon");

View File

@ -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;
}

View File

@ -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'

View File

@ -0,0 +1,18 @@
import { IAddonGetter } from "../api/index.js";
export class AddonGetter implements IAddonGetter {
userId: number;
getter: <T>(id: any, userId?: number) => Promise<T>;
constructor(userId: number, getter: (id: any, userId: number) => Promise<any>) {
this.userId = userId;
this.getter = getter;
}
async getById<T = any>(id: any) {
return await this.getter<T>(id, this.userId);
}
async getCommonById<T = any>(id: any) {
return await this.getter<T>(id, 0);
}
}

View File

@ -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<AddonEntity> {
@InjectEntityModel(AddonEntity)
repository: Repository<AddonEntity>;
//@ts-ignore
getRepository() {
return this.repository;
}
async page(pageReq: PageReq<AddonEntity>) {
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<any> {
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<any> {
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<any> {
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);
}
}

View File

@ -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<T = any>(id: any) {
return await this.addonService.getById(id, 0);
}
async getCommonById<T = any>(id: any) {
return await this.addonService.getById(id, 0);
}
}

View File

@ -1 +1,2 @@
export * from './access/index.js';
export * from './addon/index.js';

View File

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

View File

@ -46,6 +46,10 @@ export type SysPublicSetting = {
aiChatEnabled?: boolean;
showRunStrategy?: boolean;
captchaEnabled?: boolean;
captchaType?: number;
captchaAddonId?: number;
};
export type SuiteSetting = {
enabled?: boolean;

View File

@ -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;
},
};
}

View File

@ -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 (
<span class={"flex-o flex-between"}>
{item.label}
{item.needPlus && <fs-icon icon={"mingcute:vip-1-line"} className={"color-plus"}></fs-icon>}
</span>
);
},
},
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,
};
}

View File

@ -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<UserPageRes> => {
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,
},
},
};
}

View File

@ -0,0 +1,41 @@
<template>
<fs-page>
<template #header>
<div class="title">
通知管理
<span class="sub">管理通知配置</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createNotificationApi } from "./api";
import { notificationProvide } from "/@/views/certd/notification/common";
export default defineComponent({
name: "NotificationManager",
setup() {
const api = createNotificationApi();
notificationProvide(api);
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api } });
//
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
return {
crudBinding,
crudRef,
};
},
});
</script>

View File

@ -0,0 +1,46 @@
<template>
<div class="captcha"></div>
</template>
<script setup lang="ts">
import { doRequest } from "/@/components/plugins/lib";
import { createAddonApi } from "/src/api/modules/addon";
import { useSettingStore } from "/@/store/settings";
const props = defineProps<{
modelValue?: any;
}>();
const emit = defineEmits(["update:modelValue", "change"]);
const addonApi = createAddonApi();
const settingStore = useSettingStore();
async function getCaptchaAddonDefine() {
const type = settingStore.public.captchaType;
const define = addonApi.getDefineByType("captcha", type);
const res = await doRequest(
{
addonId: settingStore.public.captchaAddonId
type: "captcha",
typeName: type,
action: "onGetParams",
},
);
}
function init() {
// @ts-ignore
initGeetest4(
{
captchaId: "您的captchaId",
},
(captcha: any) => {
// captcha
captcha.appendTo(".captcha"); // appendTo
}
);
}
function onChange(value: string) {
emit("update:modelValue", value);
emit("change", value);
}
</script>

View File

@ -20,6 +20,10 @@
</template>
</a-input-password>
</a-form-item>
<a-form-item required name="captcha">
<captcha v-model:model-value="formState.captcha"></captcha>
</a-form-item>
</template>
</a-tab-pane>
<a-tab-pane v-if="sysPublicSettings.smsLoginEnabled === true" key="sms" :tab="t('authentication.smsTab')">
@ -111,6 +115,7 @@ export default defineComponent({
imgCode: "",
smsCode: "",
randomStr: "",
captcha: {},
});
const rules = {

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';