feat: 修改实体,将provider的type定义改为string,以实现自定义的类型注入。

feat: 在OAuthApplication中新增强制PKCE参数
fix: 修复了oauth相关upsert组件的部分问题
This commit is contained in:
Pan Qiancheng 2025-10-28 10:38:29 +08:00
parent 8367447700
commit 0f90d5c360
47 changed files with 155 additions and 69 deletions

View File

@ -11,6 +11,7 @@ export default OakComponent({
isConfidential: 1, isConfidential: 1,
scopes: 1, scopes: 1,
ableState: 1, ableState: 1,
requirePKCE: 1,
}, },
filters: [{ filters: [{
filter() { filter() {

View File

@ -10,6 +10,7 @@ export default OakComponent({
isConfidential: 1, isConfidential: 1,
scopes: 1, // string[] scopes: 1, // string[]
ableState: 1, ableState: 1,
requirePKCE: 1,
}, },
formData({ data, features }) { formData({ data, features }) {
if (!data) { if (!data) {

View File

@ -18,5 +18,7 @@
"ableState": "启用状态", "ableState": "启用状态",
"noData": "无数据", "noData": "无数据",
"clientId": "客户端ID", "clientId": "客户端ID",
"clientIdPlaceholder": "自动生成的客户端ID" "clientIdPlaceholder": "自动生成的客户端ID",
"requirePKCE": "强制 PKCE",
"requirePKCETooltip": "启用后授权请求必须使用PKCE扩展以增强安全性。"
} }

View File

@ -62,6 +62,11 @@ const Upsert = (props) => {
<Form.Item label={t('ableState')} valuePropName="checked"> <Form.Item label={t('ableState')} valuePropName="checked">
<Switch checked={!!item.ableState} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}/> <Switch checked={!!item.ableState} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}/>
</Form.Item> </Form.Item>
{/* requirePKCE */}
<Form.Item label={t('requirePKCE')} valuePropName="checked" tooltip={t('requirePKCETooltip')}>
<Switch checked={!!item.requirePKCE} onChange={(checked) => update({ requirePKCE: checked })}/>
</Form.Item>
</Form> </Form>
</div>); </div>);
}; };

View File

@ -10,7 +10,7 @@ const OauthProvider = (props) => {
const attrs = [ const attrs = [
"id", "name", "description", "redirectUris", "id", "name", "description", "redirectUris",
"logo", "isConfidential", "scopes", "logo", "isConfidential", "scopes",
"ableState" "ableState", "requirePKCE",
]; ];
const [upsertId, setUpsertId] = React.useState(null); const [upsertId, setUpsertId] = React.useState(null);
const handleAction = (row, action) => { const handleAction = (row, action) => {

View File

@ -12,6 +12,7 @@ export default OakComponent({
revokeEndpoint: 1, revokeEndpoint: 1,
refreshEndpoint: 1, refreshEndpoint: 1,
clientId: 1, clientId: 1,
scopes: 1,
clientSecret: 1, clientSecret: 1,
redirectUri: 1, redirectUri: 1,
autoRegister: 1, autoRegister: 1,

View File

@ -5,6 +5,7 @@ export default OakComponent({
name: 1, name: 1,
type: 1, type: 1,
logo: 1, logo: 1,
scopes: 1,
authorizationEndpoint: 1, authorizationEndpoint: 1,
tokenEndpoint: 1, tokenEndpoint: 1,
userInfoEndpoint: 1, userInfoEndpoint: 1,

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Form, Input, Switch, Select } from 'antd'; import { Form, Input, Switch, Select, Typography } from 'antd';
import Styles from './styles.module.less'; import Styles from './styles.module.less';
const { Text } = Typography;
const Upsert = (props) => { const Upsert = (props) => {
const { item } = props.data; const { item } = props.data;
const { t, update } = props.methods; const { t, update } = props.methods;
@ -15,26 +16,19 @@ const Upsert = (props) => {
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('type')} rules={[{ required: true, message: t('typeRequired') }]}> <Form.Item label={t('type')} rules={[{ required: true, message: t('typeRequired') }]} extra={item.type && item.type !== 'oak' && item.type !== 'gitea' ? (<Text type="warning">
<Select placeholder={t('typePlaceholder')} value={item.type || ""} onChange={(v) => { {item.type}不是预设类型请自行注入 handler
update({ type: v }); </Text>) : undefined}>
}}> <Select mode="tags" placeholder={t('typePlaceholder')} value={item.type ? [item.type] : []} // 保持数组形式
<Select.Option value="oak">Oak</Select.Option> onChange={(v) => {
<Select.Option value="gitea">Gitea</Select.Option> // 只取最后一个输入或选择的值
<Select.Option value="github">GitHub</Select.Option> const last = v.slice(-1)[0];
<Select.Option value="google">Google</Select.Option> update({ type: last });
<Select.Option value="facebook">Facebook</Select.Option> }} tokenSeparators={[',']} maxTagCount={1} // 只显示一个标签
<Select.Option value="twitter">Twitter</Select.Option> options={[
<Select.Option value="linkedin">LinkedIn</Select.Option> { value: 'oak', label: 'Oak' },
<Select.Option value="custom">Custom</Select.Option> { value: 'gitea', label: 'Gitea' },
<Select.Option value="gitlab">GitLab</Select.Option> ]}/>
<Select.Option value="microsoft">Microsoft</Select.Option>
<Select.Option value="apple">Apple</Select.Option>
<Select.Option value="tencent">Tencent</Select.Option>
<Select.Option value="weixin">Weixin</Select.Option>
<Select.Option value="weibo">Weibo</Select.Option>
<Select.Option value="dingtalk">DingTalk</Select.Option>
</Select>
</Form.Item> </Form.Item>
<Form.Item label={t('logo')}> <Form.Item label={t('logo')}>

View File

@ -18,7 +18,8 @@ export const selectFreeEntities = [
'userEntityGrant', 'userEntityGrant',
'wechatMpJump', 'wechatMpJump',
'applicationPassport', 'applicationPassport',
'passport' 'passport',
'oauthProvider'
]; ];
// 可以自由更新的对象 // 可以自由更新的对象
export const updateFreeDict = { export const updateFreeDict = {

View File

@ -398,7 +398,9 @@ const i18ns = [
"ableState": "启用状态", "ableState": "启用状态",
"noData": "无数据", "noData": "无数据",
"clientId": "客户端ID", "clientId": "客户端ID",
"clientIdPlaceholder": "自动生成的客户端ID" "clientIdPlaceholder": "自动生成的客户端ID",
"requirePKCE": "强制 PKCE",
"requirePKCETooltip": "启用后授权请求必须使用PKCE扩展以增强安全性。"
} }
}, },
{ {

View File

@ -14,6 +14,7 @@ export interface Schema extends EntityShape {
logo?: String<512>; logo?: String<512>;
isConfidential: Boolean; isConfidential: Boolean;
scopes?: StringListJson; scopes?: StringListJson;
requirePKCE: Boolean;
} }
export type SecretAction = 'resetSecret'; export type SecretAction = 'resetSecret';
export type Action = AbleAction | SecretAction; export type Action = AbleAction | SecretAction;

View File

@ -16,6 +16,7 @@ export const entityDesc = {
logo: '应用 Logo', logo: '应用 Logo',
isConfidential: '是否保密', isConfidential: '是否保密',
scopes: '应用权限范围', scopes: '应用权限范围',
requirePKCE: '强制 PKCE',
}, },
action: { action: {
enable: '启用', enable: '启用',

View File

@ -8,7 +8,7 @@ import { StringListJson } from '../types/datatype';
export interface Schema extends EntityShape { export interface Schema extends EntityShape {
system: System; system: System;
name: String<64>; name: String<64>;
type: "oak" | "gitea" | "github" | "google" | "facebook" | "twitter" | "linkedin" | "custom" | "gitlab" | "microsoft" | "apple" | "tencent" | "weixin" | "weibo" | "dingtalk"; type: String<64>;
logo?: String<512>; logo?: String<512>;
authorizationEndpoint: String<512>; authorizationEndpoint: String<512>;
tokenEndpoint: String<512>; tokenEndpoint: String<512>;

View File

@ -40,6 +40,10 @@ export const desc = {
scopes: { scopes: {
type: "object" type: "object"
}, },
requirePKCE: {
notNull: true,
type: "boolean"
},
ableState: { ableState: {
type: "enum", type: "enum",
enumeration: ["enabled", "disabled"] enumeration: ["enabled", "disabled"]

View File

@ -14,6 +14,7 @@ export type OpSchema = EntityShape & {
logo?: String<512> | null; logo?: String<512> | null;
isConfidential: Boolean; isConfidential: Boolean;
scopes?: StringListJson | null; scopes?: StringListJson | null;
requirePKCE: Boolean;
ableState?: AbleState | null; ableState?: AbleState | null;
} & { } & {
[A in ExpressionKey]?: any; [A in ExpressionKey]?: any;
@ -32,6 +33,7 @@ export type OpFilter = {
logo: Q_StringValue; logo: Q_StringValue;
isConfidential: Q_BooleanValue; isConfidential: Q_BooleanValue;
scopes: JsonFilter<StringListJson>; scopes: JsonFilter<StringListJson>;
requirePKCE: Q_BooleanValue;
ableState: Q_EnumValue<AbleState>; ableState: Q_EnumValue<AbleState>;
} & ExprOp<OpAttr | string>; } & ExprOp<OpAttr | string>;
export type OpProjection = { export type OpProjection = {
@ -49,6 +51,7 @@ export type OpProjection = {
logo?: number; logo?: number;
isConfidential?: number; isConfidential?: number;
scopes?: number | JsonProjection<StringListJson>; scopes?: number | JsonProjection<StringListJson>;
requirePKCE?: number;
ableState?: number; ableState?: number;
} & Partial<ExprOp<OpAttr | string>>; } & Partial<ExprOp<OpAttr | string>>;
export type OpSortAttr = Partial<{ export type OpSortAttr = Partial<{
@ -64,6 +67,7 @@ export type OpSortAttr = Partial<{
logo: number; logo: number;
isConfidential: number; isConfidential: number;
scopes: number; scopes: number;
requirePKCE: number;
ableState: number; ableState: number;
[k: string]: any; [k: string]: any;
} | ExprOp<OpAttr | string>>; } | ExprOp<OpAttr | string>>;

View File

@ -9,7 +9,8 @@
"redirectUris": "重定向 URI", "redirectUris": "重定向 URI",
"logo": "应用 Logo", "logo": "应用 Logo",
"isConfidential": "是否保密", "isConfidential": "是否保密",
"scopes": "应用权限范围" "scopes": "应用权限范围",
"requirePKCE": "强制 PKCE"
}, },
"action": { "action": {
"enable": "启用", "enable": "启用",

View File

@ -15,8 +15,10 @@ export const desc = {
}, },
type: { type: {
notNull: true, notNull: true,
type: "enum", type: "varchar",
enumeration: ["oak", "gitea", "github", "google", "facebook", "twitter", "linkedin", "custom", "gitlab", "microsoft", "apple", "tencent", "weixin", "weibo", "dingtalk"] params: {
length: 64
}
}, },
logo: { logo: {
type: "varchar", type: "varchar",

View File

@ -8,7 +8,7 @@ import { StringListJson } from "../../types/datatype";
export type OpSchema = EntityShape & { export type OpSchema = EntityShape & {
systemId: ForeignKey<"system">; systemId: ForeignKey<"system">;
name: String<64>; name: String<64>;
type: "oak" | "gitea" | "github" | "google" | "facebook" | "twitter" | "linkedin" | "custom" | "gitlab" | "microsoft" | "apple" | "tencent" | "weixin" | "weibo" | "dingtalk"; type: String<64>;
logo?: String<512> | null; logo?: String<512> | null;
authorizationEndpoint: String<512>; authorizationEndpoint: String<512>;
tokenEndpoint: String<512>; tokenEndpoint: String<512>;
@ -32,7 +32,7 @@ export type OpFilter = {
$$updateAt$$: Q_DateValue; $$updateAt$$: Q_DateValue;
systemId: Q_StringValue; systemId: Q_StringValue;
name: Q_StringValue; name: Q_StringValue;
type: Q_EnumValue<"oak" | "gitea" | "github" | "google" | "facebook" | "twitter" | "linkedin" | "custom" | "gitlab" | "microsoft" | "apple" | "tencent" | "weixin" | "weibo" | "dingtalk">; type: Q_StringValue;
logo: Q_StringValue; logo: Q_StringValue;
authorizationEndpoint: Q_StringValue; authorizationEndpoint: Q_StringValue;
tokenEndpoint: Q_StringValue; tokenEndpoint: Q_StringValue;

View File

@ -1,2 +1,2 @@
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>>)[]; declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>>)[];
export default _default; export default _default;

View File

@ -13,6 +13,8 @@ const triggers = [
const systemId = context.getSystemId(); const systemId = context.getSystemId();
data.systemId = systemId; data.systemId = systemId;
data.clientSecret = randomUUID(); data.clientSecret = randomUUID();
// 默认不强制 PKCE
data.requirePKCE = data.requirePKCE ?? false;
return 0; // 没有引起数据库行修改 return 0; // 没有引起数据库行修改
} }
}, },

View File

@ -14,7 +14,7 @@ export declare function createToDo<ED extends EntityDict & BaseEntityDict, T ext
redirectTo: EntityDict['toDo']['OpSchema']['redirectTo']; redirectTo: EntityDict['toDo']['OpSchema']['redirectTo'];
entity: any; entity: any;
entityId: string; entityId: string;
}, userIds?: string[]): Promise<1 | 0>; }, userIds?: string[]): Promise<0 | 1>;
/** /**
* todo例程entity对象上进行action操作时filtertodo完成 * todo例程entity对象上进行action操作时filtertodo完成
* entity的action的后trigger中调用 * entity的action的后trigger中调用

View File

@ -21,7 +21,8 @@ exports.selectFreeEntities = [
'userEntityGrant', 'userEntityGrant',
'wechatMpJump', 'wechatMpJump',
'applicationPassport', 'applicationPassport',
'passport' 'passport',
'oauthProvider'
]; ];
// 可以自由更新的对象 // 可以自由更新的对象
exports.updateFreeDict = { exports.updateFreeDict = {

View File

@ -400,7 +400,9 @@ const i18ns = [
"ableState": "启用状态", "ableState": "启用状态",
"noData": "无数据", "noData": "无数据",
"clientId": "客户端ID", "clientId": "客户端ID",
"clientIdPlaceholder": "自动生成的客户端ID" "clientIdPlaceholder": "自动生成的客户端ID",
"requirePKCE": "强制 PKCE",
"requirePKCETooltip": "启用后授权请求必须使用PKCE扩展以增强安全性。"
} }
}, },
{ {

View File

@ -14,6 +14,7 @@ export interface Schema extends EntityShape {
logo?: String<512>; logo?: String<512>;
isConfidential: Boolean; isConfidential: Boolean;
scopes?: StringListJson; scopes?: StringListJson;
requirePKCE: Boolean;
} }
export type SecretAction = 'resetSecret'; export type SecretAction = 'resetSecret';
export type Action = AbleAction | SecretAction; export type Action = AbleAction | SecretAction;

View File

@ -19,6 +19,7 @@ exports.entityDesc = {
logo: '应用 Logo', logo: '应用 Logo',
isConfidential: '是否保密', isConfidential: '是否保密',
scopes: '应用权限范围', scopes: '应用权限范围',
requirePKCE: '强制 PKCE',
}, },
action: { action: {
enable: '启用', enable: '启用',

View File

@ -8,7 +8,7 @@ import { StringListJson } from '../types/datatype';
export interface Schema extends EntityShape { export interface Schema extends EntityShape {
system: System; system: System;
name: String<64>; name: String<64>;
type: "oak" | "gitea" | "github" | "google" | "facebook" | "twitter" | "linkedin" | "custom" | "gitlab" | "microsoft" | "apple" | "tencent" | "weixin" | "weibo" | "dingtalk"; type: String<64>;
logo?: String<512>; logo?: String<512>;
authorizationEndpoint: String<512>; authorizationEndpoint: String<512>;
tokenEndpoint: String<512>; tokenEndpoint: String<512>;

View File

@ -43,6 +43,10 @@ exports.desc = {
scopes: { scopes: {
type: "object" type: "object"
}, },
requirePKCE: {
notNull: true,
type: "boolean"
},
ableState: { ableState: {
type: "enum", type: "enum",
enumeration: ["enabled", "disabled"] enumeration: ["enabled", "disabled"]

View File

@ -14,6 +14,7 @@ export type OpSchema = EntityShape & {
logo?: String<512> | null; logo?: String<512> | null;
isConfidential: Boolean; isConfidential: Boolean;
scopes?: StringListJson | null; scopes?: StringListJson | null;
requirePKCE: Boolean;
ableState?: AbleState | null; ableState?: AbleState | null;
} & { } & {
[A in ExpressionKey]?: any; [A in ExpressionKey]?: any;
@ -32,6 +33,7 @@ export type OpFilter = {
logo: Q_StringValue; logo: Q_StringValue;
isConfidential: Q_BooleanValue; isConfidential: Q_BooleanValue;
scopes: JsonFilter<StringListJson>; scopes: JsonFilter<StringListJson>;
requirePKCE: Q_BooleanValue;
ableState: Q_EnumValue<AbleState>; ableState: Q_EnumValue<AbleState>;
} & ExprOp<OpAttr | string>; } & ExprOp<OpAttr | string>;
export type OpProjection = { export type OpProjection = {
@ -49,6 +51,7 @@ export type OpProjection = {
logo?: number; logo?: number;
isConfidential?: number; isConfidential?: number;
scopes?: number | JsonProjection<StringListJson>; scopes?: number | JsonProjection<StringListJson>;
requirePKCE?: number;
ableState?: number; ableState?: number;
} & Partial<ExprOp<OpAttr | string>>; } & Partial<ExprOp<OpAttr | string>>;
export type OpSortAttr = Partial<{ export type OpSortAttr = Partial<{
@ -64,6 +67,7 @@ export type OpSortAttr = Partial<{
logo: number; logo: number;
isConfidential: number; isConfidential: number;
scopes: number; scopes: number;
requirePKCE: number;
ableState: number; ableState: number;
[k: string]: any; [k: string]: any;
} | ExprOp<OpAttr | string>>; } | ExprOp<OpAttr | string>>;

View File

@ -9,7 +9,8 @@
"redirectUris": "重定向 URI", "redirectUris": "重定向 URI",
"logo": "应用 Logo", "logo": "应用 Logo",
"isConfidential": "是否保密", "isConfidential": "是否保密",
"scopes": "应用权限范围" "scopes": "应用权限范围",
"requirePKCE": "强制 PKCE"
}, },
"action": { "action": {
"enable": "启用", "enable": "启用",

View File

@ -18,8 +18,10 @@ exports.desc = {
}, },
type: { type: {
notNull: true, notNull: true,
type: "enum", type: "varchar",
enumeration: ["oak", "gitea", "github", "google", "facebook", "twitter", "linkedin", "custom", "gitlab", "microsoft", "apple", "tencent", "weixin", "weibo", "dingtalk"] params: {
length: 64
}
}, },
logo: { logo: {
type: "varchar", type: "varchar",

View File

@ -8,7 +8,7 @@ import { StringListJson } from "../../types/datatype";
export type OpSchema = EntityShape & { export type OpSchema = EntityShape & {
systemId: ForeignKey<"system">; systemId: ForeignKey<"system">;
name: String<64>; name: String<64>;
type: "oak" | "gitea" | "github" | "google" | "facebook" | "twitter" | "linkedin" | "custom" | "gitlab" | "microsoft" | "apple" | "tencent" | "weixin" | "weibo" | "dingtalk"; type: String<64>;
logo?: String<512> | null; logo?: String<512> | null;
authorizationEndpoint: String<512>; authorizationEndpoint: String<512>;
tokenEndpoint: String<512>; tokenEndpoint: String<512>;
@ -32,7 +32,7 @@ export type OpFilter = {
$$updateAt$$: Q_DateValue; $$updateAt$$: Q_DateValue;
systemId: Q_StringValue; systemId: Q_StringValue;
name: Q_StringValue; name: Q_StringValue;
type: Q_EnumValue<"oak" | "gitea" | "github" | "google" | "facebook" | "twitter" | "linkedin" | "custom" | "gitlab" | "microsoft" | "apple" | "tencent" | "weixin" | "weibo" | "dingtalk">; type: Q_StringValue;
logo: Q_StringValue; logo: Q_StringValue;
authorizationEndpoint: Q_StringValue; authorizationEndpoint: Q_StringValue;
tokenEndpoint: Q_StringValue; tokenEndpoint: Q_StringValue;

View File

@ -1,2 +1,2 @@
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>>)[]; declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>>)[];
export default _default; export default _default;

View File

@ -16,6 +16,8 @@ const triggers = [
const systemId = context.getSystemId(); const systemId = context.getSystemId();
data.systemId = systemId; data.systemId = systemId;
data.clientSecret = (0, crypto_1.randomUUID)(); data.clientSecret = (0, crypto_1.randomUUID)();
// 默认不强制 PKCE
data.requirePKCE = data.requirePKCE ?? false;
return 0; // 没有引起数据库行修改 return 0; // 没有引起数据库行修改
} }
}, },

View File

@ -12,6 +12,7 @@ export default OakComponent({
isConfidential: 1, isConfidential: 1,
scopes: 1, scopes: 1,
ableState: 1, ableState: 1,
requirePKCE: 1,
}, },
filters: [{ filters: [{
filter() { filter() {

View File

@ -11,6 +11,7 @@ export default OakComponent({
isConfidential: 1, isConfidential: 1,
scopes: 1, // string[] scopes: 1, // string[]
ableState: 1, ableState: 1,
requirePKCE: 1,
}, },
formData({ data, features }) { formData({ data, features }) {
if (!data) { if (!data) {

View File

@ -18,5 +18,7 @@
"ableState": "启用状态", "ableState": "启用状态",
"noData": "无数据", "noData": "无数据",
"clientId": "客户端ID", "clientId": "客户端ID",
"clientIdPlaceholder": "自动生成的客户端ID" "clientIdPlaceholder": "自动生成的客户端ID",
"requirePKCE": "强制 PKCE",
"requirePKCETooltip": "启用后授权请求必须使用PKCE扩展以增强安全性。"
} }

View File

@ -149,6 +149,18 @@ const Upsert = (
onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}
/> />
</Form.Item> </Form.Item>
{/* requirePKCE */}
<Form.Item
label={t('requirePKCE')}
valuePropName="checked"
tooltip={t('requirePKCETooltip')}
>
<Switch
checked={!!item.requirePKCE}
onChange={(checked) => update({ requirePKCE: checked })}
/>
</Form.Item>
</Form> </Form>
</div> </div>
); );

View File

@ -26,7 +26,7 @@ const OauthProvider = (
const attrs = [ const attrs = [
"id", "name", "description", "redirectUris", "id", "name", "description", "redirectUris",
"logo", "isConfidential", "scopes", "logo", "isConfidential", "scopes",
"ableState" "ableState", "requirePKCE",
] ]
const [upsertId, setUpsertId] = React.useState<string | null>(null); const [upsertId, setUpsertId] = React.useState<string | null>(null);

View File

@ -13,6 +13,7 @@ export default OakComponent({
revokeEndpoint: 1, revokeEndpoint: 1,
refreshEndpoint: 1, refreshEndpoint: 1,
clientId: 1, clientId: 1,
scopes: 1,
clientSecret: 1, clientSecret: 1,
redirectUri: 1, redirectUri: 1,
autoRegister: 1, autoRegister: 1,

View File

@ -5,6 +5,7 @@ export default OakComponent({
name: 1, name: 1,
type: 1, type: 1,
logo: 1, logo: 1,
scopes: 1,
authorizationEndpoint: 1, authorizationEndpoint: 1,
tokenEndpoint: 1, tokenEndpoint: 1,
userInfoEndpoint: 1, userInfoEndpoint: 1,

View File

@ -1,9 +1,10 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Form, Input, Switch, Button, Space, Upload, Select } from 'antd'; import { Form, Input, Switch, Button, Space, Upload, Select, Typography } from 'antd';
import { UploadOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base'; import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import Styles from './styles.module.less'; import Styles from './styles.module.less';
import { EntityDict } from '../../../../../oak-app-domain'; import { EntityDict } from '../../../../../oak-app-domain';
const { Text } = Typography;
const Upsert = ( const Upsert = (
props: WebComponentProps< props: WebComponentProps<
@ -42,28 +43,36 @@ const Upsert = (
<Form.Item <Form.Item
label={t('type')} label={t('type')}
rules={[{ required: true, message: t('typeRequired') }]} rules={[{ required: true, message: t('typeRequired') }]}
extra={
item.type && item.type !== 'oak' && item.type !== 'gitea' ? (
<Text type="warning">
{item.type} handler
</Text>
) : undefined
}
> >
<Select placeholder={t('typePlaceholder')} value={item.type || ""} <Select
mode="tags"
placeholder={t('typePlaceholder')}
value={item.type ? [item.type] : []} // 保持数组形式
onChange={(v) => { onChange={(v) => {
update({ type: v as "oak" | "gitea" | "github" | "google" | "facebook" | "twitter" | "linkedin" | "custom" | "gitlab" | "microsoft" | "apple" | "tencent" | "weixin" | "weibo" | "dingtalk" | null | undefined }); // 只取最后一个输入或选择的值
const last = v.slice(-1)[0] as
| "oak"
| "gitea"
| string
| undefined;
update({ type: last });
}} }}
> tokenSeparators={[',']}
<Select.Option value="oak">Oak</Select.Option> maxTagCount={1} // 只显示一个标签
<Select.Option value="gitea">Gitea</Select.Option> options={[
<Select.Option value="github">GitHub</Select.Option> { value: 'oak', label: 'Oak' },
<Select.Option value="google">Google</Select.Option> { value: 'gitea', label: 'Gitea' },
<Select.Option value="facebook">Facebook</Select.Option> ]}
<Select.Option value="twitter">Twitter</Select.Option>
<Select.Option value="linkedin">LinkedIn</Select.Option> />
<Select.Option value="custom">Custom</Select.Option>
<Select.Option value="gitlab">GitLab</Select.Option>
<Select.Option value="microsoft">Microsoft</Select.Option>
<Select.Option value="apple">Apple</Select.Option>
<Select.Option value="tencent">Tencent</Select.Option>
<Select.Option value="weixin">Weixin</Select.Option>
<Select.Option value="weibo">Weibo</Select.Option>
<Select.Option value="dingtalk">DingTalk</Select.Option>
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item

View File

@ -22,7 +22,8 @@ export const selectFreeEntities: SelectFreeEntities<EntityDict> = [
'userEntityGrant', 'userEntityGrant',
'wechatMpJump', 'wechatMpJump',
'applicationPassport', 'applicationPassport',
'passport' 'passport',
'oauthProvider'
]; ];
// 可以自由更新的对象 // 可以自由更新的对象

View File

@ -400,7 +400,9 @@ const i18ns: I18n[] = [
"ableState": "启用状态", "ableState": "启用状态",
"noData": "无数据", "noData": "无数据",
"clientId": "客户端ID", "clientId": "客户端ID",
"clientIdPlaceholder": "自动生成的客户端ID" "clientIdPlaceholder": "自动生成的客户端ID",
"requirePKCE": "强制 PKCE",
"requirePKCETooltip": "启用后授权请求必须使用PKCE扩展以增强安全性。"
} }
}, },
{ {

View File

@ -20,6 +20,7 @@ export interface Schema extends EntityShape {
logo?: String<512>; logo?: String<512>;
isConfidential: Boolean; isConfidential: Boolean;
scopes?: StringListJson; scopes?: StringListJson;
requirePKCE: Boolean;
}; };
export type SecretAction = 'resetSecret'; export type SecretAction = 'resetSecret';
@ -43,6 +44,7 @@ export const entityDesc: EntityDesc<Schema, Action, '', {
logo: '应用 Logo', logo: '应用 Logo',
isConfidential: '是否保密', isConfidential: '是否保密',
scopes: '应用权限范围', scopes: '应用权限范围',
requirePKCE: '强制 PKCE',
}, },
action: { action: {
enable: '启用', enable: '启用',

View File

@ -14,7 +14,7 @@ import { StringListJson } from '../types/datatype';
export interface Schema extends EntityShape { export interface Schema extends EntityShape {
system: System; system: System;
name: String<64>; name: String<64>;
type: "oak" | "gitea" | "github" | "google" | "facebook" | "twitter" | "linkedin" | "custom" | "gitlab" | "microsoft" | "apple" | "tencent" | "weixin" | "weibo" | "dingtalk"; type: String<64>; // OAuth 提供方类型 (可以自己实现默认注入oak, gitea)
logo?: String<512>; logo?: String<512>;
// OAuth 端点 (RFC 6749 Section 3) // OAuth 端点 (RFC 6749 Section 3)

View File

@ -20,6 +20,9 @@ const triggers: Trigger<EntityDict, "oauthApplication", BRC<EntityDict>>[] = [
data.systemId = systemId; data.systemId = systemId;
data.clientSecret = randomUUID(); data.clientSecret = randomUUID();
// 默认不强制 PKCE
data.requirePKCE = data.requirePKCE ?? false;
return 0; // 没有引起数据库行修改 return 0; // 没有引起数据库行修改
} }
} as CreateTrigger<EntityDict, "oauthApplication", BRC<EntityDict>>, } as CreateTrigger<EntityDict, "oauthApplication", BRC<EntityDict>>,

10
upgrade/5.11.1/01.sql Normal file
View File

@ -0,0 +1,10 @@
SET SESSION sql_mode = 'TRADITIONAL';
START TRANSACTION;
alter table oauthApplication
add requirePKCE tinyint(1) default false not null after ableState;
alter table oauthProvider
modify type varchar(64) not null;
COMMIT;