mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 076f82b933 | |||
| 324e872849 | |||
| 64844c1aa6 | |||
| ddd6ffe8c6 |
@@ -0,0 +1,66 @@
|
|||||||
|
import { AuthStrategy, registries, type Authenticator } from "bknd";
|
||||||
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import { createApp } from "core/test/utils";
|
||||||
|
import { s } from "bknd/utils";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
|
||||||
|
const customStrategySchema = s.object({
|
||||||
|
name: s.string(),
|
||||||
|
});
|
||||||
|
type CustomStrategyOptions = s.Static<typeof customStrategySchema>;
|
||||||
|
|
||||||
|
class CustomStrategy extends AuthStrategy<typeof customStrategySchema> {
|
||||||
|
constructor(config: Partial<CustomStrategyOptions> = {}) {
|
||||||
|
super(config as any, "custom", "custom", "form");
|
||||||
|
}
|
||||||
|
|
||||||
|
getSchema() {
|
||||||
|
return customStrategySchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
getController(authenticator: Authenticator) {
|
||||||
|
return new Hono()
|
||||||
|
.post("/login", async (c) => {
|
||||||
|
try {
|
||||||
|
// do magic, and it succeeds, resolve login
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return await authenticator.resolveLogin();
|
||||||
|
} catch (e) {
|
||||||
|
// in case of error, respond with the error
|
||||||
|
return authenticator.respondWithError(c, e as any);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post("/register", async (c) => {
|
||||||
|
try {
|
||||||
|
// do magic, and it succeeds, resolve register
|
||||||
|
// @ts-ignore
|
||||||
|
return await authenticator.resolveRegister();
|
||||||
|
} catch (e) {
|
||||||
|
// in case of error, respond with the error
|
||||||
|
return authenticator.respondWithError(c, e as any);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("custom auth strategy", () => {
|
||||||
|
it("should be registered", async () => {
|
||||||
|
registries.auth.register("custom", CustomStrategy);
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
initialConfig: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
const strategies = app
|
||||||
|
.getSchema()
|
||||||
|
.auth.properties.strategies.additionalProperties.anyOf.map((s) => s.properties.type.const);
|
||||||
|
|
||||||
|
expect(strategies).toContain("custom");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -169,7 +169,6 @@ export function serveStaticViaImport(opts?: { manifest?: Manifest }) {
|
|||||||
if (files.includes(path)) {
|
if (files.includes(path)) {
|
||||||
try {
|
try {
|
||||||
const content = await import(`bknd/static/${path}?raw`, {
|
const content = await import(`bknd/static/${path}?raw`, {
|
||||||
/* @vite-ignore */
|
|
||||||
assert: { type: "text" },
|
assert: { type: "text" },
|
||||||
}).then((m) => m.default);
|
}).then((m) => m.default);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import type { Entity, EntityManager } from "data/entities";
|
|||||||
import { em, entity, enumm, type FieldSchema } from "data/prototype";
|
import { em, entity, enumm, type FieldSchema } from "data/prototype";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { AuthController } from "./api/AuthController";
|
import { AuthController } from "./api/AuthController";
|
||||||
import { type AppAuthSchema, authConfigSchema, STRATEGIES } from "./auth-schema";
|
import {
|
||||||
|
type AppAuthSchema,
|
||||||
|
authConfigSchema,
|
||||||
|
AuthStrategyRegistry,
|
||||||
|
buildAuthSchema,
|
||||||
|
} from "./auth-schema";
|
||||||
import { AppUserPool } from "auth/AppUserPool";
|
import { AppUserPool } from "auth/AppUserPool";
|
||||||
import type { AppEntity } from "core/config";
|
import type { AppEntity } from "core/config";
|
||||||
import { usersFields } from "./auth-entities";
|
import { usersFields } from "./auth-entities";
|
||||||
@@ -68,7 +73,8 @@ export class AppAuth extends Module<AppAuthSchema> {
|
|||||||
// build strategies
|
// build strategies
|
||||||
const strategies = transformObject(this.config.strategies ?? {}, (strategy, name) => {
|
const strategies = transformObject(this.config.strategies ?? {}, (strategy, name) => {
|
||||||
try {
|
try {
|
||||||
return new STRATEGIES[strategy.type].cls(strategy.config as any);
|
const cls = AuthStrategyRegistry.get(strategy.type as any).cls;
|
||||||
|
return new cls(strategy.config as any);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not build strategy ${String(
|
`Could not build strategy ${String(
|
||||||
@@ -108,7 +114,7 @@ export class AppAuth extends Module<AppAuthSchema> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return authConfigSchema;
|
return buildAuthSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
get authenticator(): Authenticator {
|
get authenticator(): Authenticator {
|
||||||
|
|||||||
+60
-57
@@ -1,40 +1,26 @@
|
|||||||
|
import { Registry, type Constructor } from "core/registry/Registry";
|
||||||
import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator";
|
import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator";
|
||||||
import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies";
|
|
||||||
import { objectTransform, s } from "bknd/utils";
|
import { objectTransform, s } from "bknd/utils";
|
||||||
|
import type { ProvidedOAuthConfig } from "./authenticate/strategies/oauth/OAuthStrategy";
|
||||||
|
import type { OAuthConfigCustom } from "./authenticate/strategies/oauth/CustomOAuthStrategy";
|
||||||
|
import { PasswordStrategy } from "./authenticate/strategies/PasswordStrategy";
|
||||||
|
import { OAuthStrategy } from "./authenticate/strategies/oauth/OAuthStrategy";
|
||||||
|
import { CustomOAuthStrategy } from "./authenticate/strategies/oauth/CustomOAuthStrategy";
|
||||||
|
import type { AuthStrategy } from "auth/authenticate/strategies/Strategy";
|
||||||
|
|
||||||
export const Strategies = {
|
export const AuthStrategyRegistry = new Registry<{
|
||||||
password: {
|
cls: Constructor<AuthStrategy>;
|
||||||
cls: PasswordStrategy,
|
schema: s.Schema;
|
||||||
schema: PasswordStrategy.prototype.getSchema(),
|
}>((cls: Constructor<AuthStrategy>) => ({
|
||||||
},
|
cls,
|
||||||
oauth: {
|
schema: cls.prototype.getSchema() as s.Schema,
|
||||||
cls: OAuthStrategy,
|
}))
|
||||||
schema: OAuthStrategy.prototype.getSchema(),
|
.register("password", PasswordStrategy)
|
||||||
},
|
.register("oauth", OAuthStrategy)
|
||||||
custom_oauth: {
|
.register("custom_oauth", CustomOAuthStrategy);
|
||||||
cls: CustomOAuthStrategy,
|
|
||||||
schema: CustomOAuthStrategy.prototype.getSchema(),
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const STRATEGIES = Strategies;
|
export type AppAuthOAuthStrategy = ProvidedOAuthConfig;
|
||||||
const strategiesSchemaObject = objectTransform(STRATEGIES, (strategy, name) => {
|
export type AppAuthCustomOAuthStrategy = OAuthConfigCustom;
|
||||||
return s.strictObject(
|
|
||||||
{
|
|
||||||
enabled: s.boolean({ default: true }).optional(),
|
|
||||||
type: s.literal(name),
|
|
||||||
config: strategy.schema,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const strategiesSchema = s.anyOf(Object.values(strategiesSchemaObject));
|
|
||||||
export type AppAuthStrategies = s.Static<typeof strategiesSchema>;
|
|
||||||
export type AppAuthOAuthStrategy = s.Static<typeof STRATEGIES.oauth.schema>;
|
|
||||||
export type AppAuthCustomOAuthStrategy = s.Static<typeof STRATEGIES.custom_oauth.schema>;
|
|
||||||
|
|
||||||
const guardConfigSchema = s.object({
|
const guardConfigSchema = s.object({
|
||||||
enabled: s.boolean({ default: false }).optional(),
|
enabled: s.boolean({ default: false }).optional(),
|
||||||
@@ -45,32 +31,49 @@ export const guardRoleSchema = s.strictObject({
|
|||||||
implicit_allow: s.boolean().optional(),
|
implicit_allow: s.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const authConfigSchema = s.strictObject(
|
export function buildAuthSchema() {
|
||||||
{
|
const strategiesSchemaObject = objectTransform(AuthStrategyRegistry.all(), (strategy, name) => {
|
||||||
enabled: s.boolean({ default: false }),
|
return s.strictObject(
|
||||||
basepath: s.string({ default: "/api/auth" }),
|
{
|
||||||
entity_name: s.string({ default: "users" }),
|
enabled: s.boolean({ default: true }).optional(),
|
||||||
allow_register: s.boolean({ default: true }).optional(),
|
type: s.literal(name),
|
||||||
jwt: jwtConfig,
|
config: strategy.schema,
|
||||||
cookie: cookieConfig,
|
},
|
||||||
strategies: s.record(strategiesSchema, {
|
{
|
||||||
title: "Strategies",
|
title: name,
|
||||||
default: {
|
},
|
||||||
password: {
|
);
|
||||||
type: "password",
|
});
|
||||||
enabled: true,
|
|
||||||
config: {
|
const strategiesSchema = s.anyOf(Object.values(strategiesSchemaObject));
|
||||||
hashing: "sha256",
|
return s.strictObject(
|
||||||
|
{
|
||||||
|
enabled: s.boolean({ default: false }),
|
||||||
|
basepath: s.string({ default: "/api/auth" }),
|
||||||
|
entity_name: s.string({ default: "users" }),
|
||||||
|
allow_register: s.boolean({ default: true }).optional(),
|
||||||
|
jwt: jwtConfig,
|
||||||
|
cookie: cookieConfig,
|
||||||
|
strategies: s.record(strategiesSchema, {
|
||||||
|
title: "Strategies",
|
||||||
|
default: {
|
||||||
|
password: {
|
||||||
|
type: "password",
|
||||||
|
enabled: true,
|
||||||
|
config: {
|
||||||
|
hashing: "sha256",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
}),
|
guard: guardConfigSchema.optional(),
|
||||||
guard: guardConfigSchema.optional(),
|
roles: s.record(guardRoleSchema, { default: {} }).optional(),
|
||||||
roles: s.record(guardRoleSchema, { default: {} }).optional(),
|
},
|
||||||
},
|
{ title: "Authentication" },
|
||||||
{ title: "Authentication" },
|
);
|
||||||
);
|
}
|
||||||
|
export const authConfigSchema = buildAuthSchema();
|
||||||
|
|
||||||
|
export type AppAuthStrategies = s.Static<(typeof authConfigSchema)["properties"]["strategies"]>;
|
||||||
export type AppAuthJWTConfig = s.Static<typeof jwtConfig>;
|
export type AppAuthJWTConfig = s.Static<typeof jwtConfig>;
|
||||||
|
|
||||||
export type AppAuthSchema = s.Static<typeof authConfigSchema>;
|
export type AppAuthSchema = s.Static<typeof authConfigSchema>;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const oauthSchemaCustom = s.strictObject(
|
|||||||
{ title: "Custom OAuth" },
|
{ title: "Custom OAuth" },
|
||||||
);
|
);
|
||||||
|
|
||||||
type OAuthConfigCustom = s.Static<typeof oauthSchemaCustom>;
|
export type OAuthConfigCustom = s.Static<typeof oauthSchemaCustom>;
|
||||||
|
|
||||||
export type UserProfile = {
|
export type UserProfile = {
|
||||||
sub: string;
|
sub: string;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const schemaProvided = s.object(
|
|||||||
},
|
},
|
||||||
{ title: "OAuth" },
|
{ title: "OAuth" },
|
||||||
);
|
);
|
||||||
type ProvidedOAuthConfig = s.Static<typeof schemaProvided>;
|
export type ProvidedOAuthConfig = s.Static<typeof schemaProvided>;
|
||||||
|
|
||||||
export type CustomOAuthConfig = {
|
export type CustomOAuthConfig = {
|
||||||
type: SupportedTypes;
|
type: SupportedTypes;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type Constructor<T> = new (...args: any[]) => T;
|
export type Constructor<T> = new (...args: any[]) => T;
|
||||||
|
export type ClassThatImplements<T> = Constructor<T> & { prototype: T };
|
||||||
export type RegisterFn<Item> = (unknown: any) => Item;
|
export type RegisterFn<Item> = (unknown: any) => Item;
|
||||||
|
|
||||||
export class Registry<
|
export class Registry<
|
||||||
|
|||||||
@@ -256,12 +256,10 @@ export class DataController extends Controller {
|
|||||||
// read many
|
// read many
|
||||||
const saveRepoQuery = s
|
const saveRepoQuery = s
|
||||||
.object({
|
.object({
|
||||||
...omitKeys(repoQuery.properties, ["with", "where"]),
|
...omitKeys(repoQuery.properties, ["with"]),
|
||||||
sort: s.string({ default: "id" }),
|
sort: s.string({ default: "id" }),
|
||||||
select: s.array(s.string()),
|
select: s.array(s.string()),
|
||||||
join: s.array(s.string()),
|
join: s.array(s.string()),
|
||||||
with: s.anyOf([s.array(s.string()), s.record(s.object({}))]),
|
|
||||||
where: s.object({}),
|
|
||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
const saveRepoQueryParams = (pick: string[] = Object.keys(repoQuery.properties)) => [
|
const saveRepoQueryParams = (pick: string[] = Object.keys(repoQuery.properties)) => [
|
||||||
@@ -275,15 +273,7 @@ export class DataController extends Controller {
|
|||||||
"/:entity",
|
"/:entity",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
summary: "Read many",
|
summary: "Read many",
|
||||||
parameters: saveRepoQueryParams([
|
parameters: saveRepoQueryParams(["limit", "offset", "sort", "select", "join"]),
|
||||||
"limit",
|
|
||||||
"offset",
|
|
||||||
"sort",
|
|
||||||
"select",
|
|
||||||
"join",
|
|
||||||
"with",
|
|
||||||
"where",
|
|
||||||
]),
|
|
||||||
tags: ["data"],
|
tags: ["data"],
|
||||||
}),
|
}),
|
||||||
permission(DataPermissions.entityRead),
|
permission(DataPermissions.entityRead),
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export type {
|
|||||||
UserPool,
|
UserPool,
|
||||||
AuthAction,
|
AuthAction,
|
||||||
AuthUserResolver,
|
AuthUserResolver,
|
||||||
|
Authenticator,
|
||||||
} from "auth/authenticate/Authenticator";
|
} from "auth/authenticate/Authenticator";
|
||||||
export { AuthStrategy } from "auth/authenticate/strategies/Strategy";
|
export { AuthStrategy } from "auth/authenticate/strategies/Strategy";
|
||||||
export * as AuthPermissions from "auth/auth-permissions";
|
export * as AuthPermissions from "auth/auth-permissions";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Storage } from "media/storage/Storage";
|
|||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { type FieldSchema, em, entity } from "../data/prototype";
|
import { type FieldSchema, em, entity } from "../data/prototype";
|
||||||
import { MediaController } from "./api/MediaController";
|
import { MediaController } from "./api/MediaController";
|
||||||
import { buildMediaSchema, registry, type TAppMediaConfig } from "./media-schema";
|
import { buildMediaSchema, MediaAdapterRegistry, type TAppMediaConfig } from "./media-schema";
|
||||||
import { mediaFields } from "./media-entities";
|
import { mediaFields } from "./media-entities";
|
||||||
import * as MediaPermissions from "media/media-permissions";
|
import * as MediaPermissions from "media/media-permissions";
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export class AppMedia extends Module<Required<TAppMediaConfig>> {
|
|||||||
let adapter: StorageAdapter;
|
let adapter: StorageAdapter;
|
||||||
try {
|
try {
|
||||||
const { type, config } = this.config.adapter;
|
const { type, config } = this.config.adapter;
|
||||||
const cls = registry.get(type as any).cls;
|
const cls = MediaAdapterRegistry.get(type as any).cls;
|
||||||
adapter = new cls(config as any);
|
adapter = new cls(config as any);
|
||||||
|
|
||||||
this._storage = new Storage(adapter, this.config.storage, this.ctx.emgr);
|
this._storage = new Storage(adapter, this.config.storage, this.ctx.emgr);
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { type Constructor, Registry } from "core/registry/Registry";
|
|
||||||
import type { StorageAdapter } from "./storage/StorageAdapter";
|
|
||||||
import type { s } from "bknd/utils";
|
|
||||||
import { StorageS3Adapter } from "./storage/adapters/s3/StorageS3Adapter";
|
|
||||||
import { StorageCloudinaryAdapter } from "./storage/adapters/cloudinary/StorageCloudinaryAdapter";
|
|
||||||
|
|
||||||
type ClassThatImplements<T> = Constructor<T> & { prototype: T };
|
|
||||||
|
|
||||||
export const MediaAdapterRegistry = new Registry<{
|
|
||||||
cls: ClassThatImplements<StorageAdapter>;
|
|
||||||
schema: s.Schema;
|
|
||||||
}>((cls: ClassThatImplements<StorageAdapter>) => ({
|
|
||||||
cls,
|
|
||||||
schema: cls.prototype.getSchema() as s.Schema,
|
|
||||||
}))
|
|
||||||
.register("s3", StorageS3Adapter)
|
|
||||||
.register("cloudinary", StorageCloudinaryAdapter);
|
|
||||||
|
|
||||||
export const MediaAdapters = {
|
|
||||||
s3: {
|
|
||||||
cls: StorageS3Adapter,
|
|
||||||
schema: StorageS3Adapter.prototype.getSchema(),
|
|
||||||
},
|
|
||||||
cloudinary: {
|
|
||||||
cls: StorageCloudinaryAdapter,
|
|
||||||
schema: StorageCloudinaryAdapter.prototype.getSchema(),
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
import { MediaAdapters } from "media/media-registry";
|
|
||||||
import { registries } from "modules/registries";
|
|
||||||
import { s, objectTransform } from "bknd/utils";
|
import { s, objectTransform } from "bknd/utils";
|
||||||
|
import { Registry, type ClassThatImplements } from "core/registry/Registry";
|
||||||
|
import type { StorageAdapter } from "./storage/StorageAdapter";
|
||||||
|
import { StorageS3Adapter } from "./storage/adapters/s3/StorageS3Adapter";
|
||||||
|
import { StorageCloudinaryAdapter } from "./storage/adapters/cloudinary/StorageCloudinaryAdapter";
|
||||||
|
|
||||||
export const ADAPTERS = {
|
export const MediaAdapterRegistry = new Registry<{
|
||||||
...MediaAdapters,
|
cls: ClassThatImplements<StorageAdapter>;
|
||||||
} as const;
|
schema: s.Schema;
|
||||||
|
}>((cls: ClassThatImplements<StorageAdapter>) => ({
|
||||||
export const registry = registries.media;
|
cls,
|
||||||
|
schema: cls.prototype.getSchema() as s.Schema,
|
||||||
|
}))
|
||||||
|
.register("s3", StorageS3Adapter)
|
||||||
|
.register("cloudinary", StorageCloudinaryAdapter);
|
||||||
|
|
||||||
export function buildMediaSchema() {
|
export function buildMediaSchema() {
|
||||||
const adapterSchemaObject = objectTransform(registry.all(), (adapter, name) => {
|
const adapterSchemaObject = objectTransform(MediaAdapterRegistry.all(), (adapter, name) => {
|
||||||
return s.strictObject(
|
return s.strictObject(
|
||||||
{
|
{
|
||||||
type: s.literal(name),
|
type: s.literal(name),
|
||||||
|
|||||||
@@ -696,13 +696,13 @@ export class ModuleManager {
|
|||||||
return transformObject(this.modules, (module) => module.toJSON(true)) as any;
|
return transformObject(this.modules, (module) => module.toJSON(true)) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSchema() {
|
getSchema(): { version: number } & ModuleSchemas {
|
||||||
const schemas = transformObject(this.modules, (module) => module.getSchema());
|
const schemas = transformObject(this.modules, (module) => module.getSchema());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: this.version(),
|
version: this.version(),
|
||||||
...schemas,
|
...schemas,
|
||||||
};
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(secrets?: boolean): { version: number } & ModuleConfigs {
|
toJSON(secrets?: boolean): { version: number } & ModuleConfigs {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { MediaAdapterRegistry } from "media/media-registry";
|
import { AuthStrategyRegistry } from "auth/auth-schema";
|
||||||
|
import { MediaAdapterRegistry } from "media/media-schema";
|
||||||
|
|
||||||
const registries = {
|
const registries = {
|
||||||
media: MediaAdapterRegistry,
|
media: MediaAdapterRegistry,
|
||||||
|
auth: AuthStrategyRegistry,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export { registries };
|
export { registries };
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
TbBrandX,
|
TbBrandX,
|
||||||
TbSettings,
|
TbSettings,
|
||||||
} from "react-icons/tb";
|
} from "react-icons/tb";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
@@ -76,7 +75,7 @@ function AuthStrategiesListInternal() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
schema={schema as any}
|
schema={JSON.parse(JSON.stringify(schema))}
|
||||||
initialValues={config}
|
initialValues={config}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
options={formOptions}
|
options={formOptions}
|
||||||
|
|||||||
Reference in New Issue
Block a user