feat: 增加判断oauth登录时是否开启相应applicationPassport

This commit is contained in:
lxy 2025-11-14 10:28:01 +08:00
parent d7d302f329
commit 3f690e6725
22 changed files with 396 additions and 43 deletions

View File

@ -16,6 +16,7 @@ export async function loginByOauth(params, context) {
// 验证 state 并获取 OAuth 配置 // 验证 state 并获取 OAuth 配置
const [state] = await context.select("oauthState", { const [state] = await context.select("oauthState", {
data: { data: {
providerId: 1,
provider: { provider: {
type: 1, type: 1,
clientId: 1, clientId: 1,
@ -32,6 +33,31 @@ export async function loginByOauth(params, context) {
state: stateCode, state: stateCode,
}, },
}, { dontCollect: true }); }, { dontCollect: true });
const systemId = context.getSystemId();
const [applicationPassport] = await context.select('applicationPassport', {
data: {
id: 1,
applicationId: 1,
passportId: 1,
passport: {
id: 1,
type: 1,
systemId: 1,
config: 1,
},
},
filter: {
passport: {
systemId,
type: 'oauth',
},
applicationId,
}
}, { dontCollect: true });
const allowOauth = !!(state.providerId && applicationPassport?.passport?.config?.oauthIds && applicationPassport?.passport?.config)?.oauthIds.includes(state.providerId);
if (!allowOauth) {
throw new OakUserException('error::user.loginWayDisabled');
}
assert(state, '无效的 state 参数'); assert(state, '无效的 state 参数');
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用'); assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
// 如果已经使用 // 如果已经使用
@ -329,7 +355,7 @@ export async function authorize(params, context) {
applicationId: context.getApplicationId(), applicationId: context.getApplicationId(),
userId: context.getCurrentUserId(), userId: context.getCurrentUserId(),
scope: scope === undefined ? [] : [scope], scope: scope === undefined ? [] : [scope],
expiresAt: Date.now() + 10 * 60 * 1000, expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期
// PKCE 支持 // PKCE 支持
codeChallenge: code_challenge, codeChallenge: code_challenge,
codeChallengeMethod: code_challenge_method || 'plain', codeChallengeMethod: code_challenge_method || 'plain',

View File

@ -21,13 +21,43 @@ export default OakComponent({
state: '', state: '',
}, },
lifetimes: { lifetimes: {
ready() { async ready() {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const clientId = searchParams.get('client_id') || ''; const clientId = searchParams.get('client_id') || '';
const responseType = searchParams.get('response_type') || ''; const responseType = searchParams.get('response_type') || '';
const redirectUri = searchParams.get('redirect_uri') || ''; const redirectUri = searchParams.get('redirect_uri') || '';
const scope = searchParams.get('scope') || ''; const scope = searchParams.get('scope') || '';
const state = searchParams.get('state') || ''; const state = searchParams.get('state') || '';
//判断是否允许oauth登录
const application = this.features.application.getApplication();
const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id });
const oauthPassport = applicationPassports?.find((ele) => ele.passport?.type === 'oauth');
const oauthIds = oauthPassport?.config?.oauthIds;
let allowOauth = false;
if (clientId) {
const { data: [oauthProvider] } = await this.features.cache.refresh('oauthProvider', {
data: {
id: 1,
clientId: 1,
systemId: 1,
},
filter: {
clientId,
systemId: application.systemId,
}
});
if (oauthProvider?.id && oauthIds?.length > 0 && oauthIds.includes(oauthProvider?.id)) {
allowOauth = true;
}
}
if (!allowOauth) {
this.setState({
hasError: true,
errorMsg: 'oauth.login',
});
this.setState({ loading: false });
return;
}
this.setState({ this.setState({
client_id: clientId, client_id: clientId,
response_type: responseType, response_type: responseType,

View File

@ -16,6 +16,7 @@
"missing_client_id": "缺少 client_id 参数", "missing_client_id": "缺少 client_id 参数",
"unknown": "未知错误,请稍后重试" "unknown": "未知错误,请稍后重试"
} }
} },
"login": "当前暂未支持该第三方应用授权登录"
} }
} }

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base'; import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../../../oak-app-domain'; import { EntityDict } from '../../../../../oak-app-domain';
declare const Upsert: (props: WebComponentProps<EntityDict, 'oauthProvider', false, { declare const Upsert: (props: WebComponentProps<EntityDict, "oauthProvider", false, {
item: RowWithActions<EntityDict, 'oauthProvider'>; item: RowWithActions<EntityDict, "oauthProvider">;
}>) => React.JSX.Element; }>) => React.JSX.Element;
export default Upsert; export default Upsert;

View File

@ -10,13 +10,13 @@ const Upsert = (props) => {
} }
return (<div className={Styles.id}> return (<div className={Styles.id}>
<Form layout="vertical" autoComplete="off"> <Form layout="vertical" autoComplete="off">
<Form.Item label={t('name')} rules={[{ required: true, message: t('nameRequired') }]}> <Form.Item label={t('name')} required={true} rules={[{ required: true, message: t('nameRequired') }]}>
<Input placeholder={t('namePlaceholder')} value={item.name || ""} onChange={(v) => { <Input placeholder={t('namePlaceholder')} value={item.name || ""} onChange={(v) => {
update({ name: v.target.value }); update({ name: v.target.value });
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('type')} rules={[{ required: true, message: t('typeRequired') }]} extra={item.type && item.type !== 'oak' && item.type !== 'gitea' ? (<Text type="warning"> <Form.Item label={t('type')} required={true} rules={[{ required: true, message: t('typeRequired') }]} extra={item.type && item.type !== 'oak' && item.type !== 'gitea' ? (<Text type="warning">
{item.type}不是预设类型请自行注入 handler {item.type}不是预设类型请自行注入 handler
</Text>) : undefined}> </Text>) : undefined}>
<Select mode="tags" placeholder={t('typePlaceholder')} value={item.type ? [item.type] : []} // 保持数组形式 <Select mode="tags" placeholder={t('typePlaceholder')} value={item.type ? [item.type] : []} // 保持数组形式
@ -37,13 +37,13 @@ const Upsert = (props) => {
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('authorizationEndpoint')} rules={[{ required: true, message: t('authorizationEndpointRequired') }]}> <Form.Item label={t('authorizationEndpoint')} required={true} rules={[{ required: true, message: t('authorizationEndpointRequired') }]}>
<Input placeholder={t('authorizationEndpointPlaceholder')} value={item.authorizationEndpoint || ""} onChange={(v) => { <Input placeholder={t('authorizationEndpointPlaceholder')} value={item.authorizationEndpoint || ""} onChange={(v) => {
update({ authorizationEndpoint: v.target.value }); update({ authorizationEndpoint: v.target.value });
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('tokenEndpoint')} rules={[{ required: true, message: t('tokenEndpointRequired') }]}> <Form.Item label={t('tokenEndpoint')} required={true} rules={[{ required: true, message: t('tokenEndpointRequired') }]}>
<Input placeholder={t('tokenEndpointPlaceholder')} value={item.tokenEndpoint || ""} onChange={(v) => { <Input placeholder={t('tokenEndpointPlaceholder')} value={item.tokenEndpoint || ""} onChange={(v) => {
update({ tokenEndpoint: v.target.value }); update({ tokenEndpoint: v.target.value });
}}/> }}/>
@ -67,13 +67,13 @@ const Upsert = (props) => {
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('clientId')} rules={[{ required: true, message: t('clientIdRequired') }]}> <Form.Item label={t('clientId')} required={true} rules={[{ required: true, message: t('clientIdRequired') }]}>
<Input placeholder={t('clientIdPlaceholder')} value={item.clientId || ""} onChange={(v) => { <Input placeholder={t('clientIdPlaceholder')} value={item.clientId || ""} onChange={(v) => {
update({ clientId: v.target.value }); update({ clientId: v.target.value });
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('clientSecret')} rules={[{ required: true, message: t('clientSecretRequired') }]}> <Form.Item label={t('clientSecret')} required={true} rules={[{ required: true, message: t('clientSecretRequired') }]}>
<Input.Password placeholder={t('clientSecretPlaceholder')} value={item.clientSecret || ""} onChange={(v) => { <Input.Password placeholder={t('clientSecretPlaceholder')} value={item.clientSecret || ""} onChange={(v) => {
update({ clientSecret: v.target.value }); update({ clientSecret: v.target.value });
}}/> }}/>
@ -85,7 +85,7 @@ const Upsert = (props) => {
}} tokenSeparators={[',']} open={false}/> }} tokenSeparators={[',']} open={false}/>
</Form.Item> </Form.Item>
<Form.Item label={t('redirectUri')} rules={[{ required: true, message: t('redirectUriRequired') }]}> <Form.Item label={t('redirectUri')} required={true} rules={[{ required: true, message: t('redirectUriRequired') }]}>
<Input placeholder={t('redirectUriPlaceholder')} value={item.redirectUri || ""} onChange={(v) => { <Input placeholder={t('redirectUriPlaceholder')} value={item.redirectUri || ""} onChange={(v) => {
update({ redirectUri: v.target.value }); update({ redirectUri: v.target.value });
}}/> }}/>
@ -96,7 +96,7 @@ const Upsert = (props) => {
</Form.Item> </Form.Item>
<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 === 'enabled'} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}/>
</Form.Item> </Form.Item>
{/* <Form.Item> {/* <Form.Item>

View File

@ -64,9 +64,11 @@ export default OakComponent({
id: 1, id: 1,
name: 1, name: 1,
systemId: 1, systemId: 1,
ableState: 1,
}, },
filter: { filter: {
systemId, systemId,
ableState: 'enabled'
} }
}); });
if (oauthProviders && oauthProviders?.length > 0) { if (oauthProviders && oauthProviders?.length > 0) {

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Switch, Form, Select, Tag, } from 'antd'; import { Switch, Form, Select, Tag, Tooltip, } from 'antd';
import Styles from './web.module.less'; import Styles from './web.module.less';
export default function Oauth(props) { export default function Oauth(props) {
const { passport, t, changeEnabled, updateConfig, oauthOptions } = props; const { passport, t, changeEnabled, updateConfig, oauthOptions } = props;
@ -12,15 +12,17 @@ export default function Oauth(props) {
return (<div className={Styles.item}> return (<div className={Styles.item}>
<div className={Styles.title}> <div className={Styles.title}>
<Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag> <Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag>
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => { <Tooltip title={(oauthOptions && oauthOptions?.length > 0) ? '' : '请先启用oauth供应商'}>
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => {
changeEnabled(checked); changeEnabled(checked);
}}/> }} disabled={!(oauthOptions && oauthOptions?.length > 0)}/>
</Tooltip>
</div> </div>
{enabled && {enabled &&
<div> <div>
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 900, marginTop: 16 }}> <Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 900, marginTop: 16 }}>
<Form.Item label='oauth供商'> <Form.Item label='oauth商'>
<Select mode="multiple" style={{ width: '100%' }} placeholder="请选择oauth供商" value={oauthIds} onChange={(value) => { <Select mode="multiple" style={{ width: '100%' }} placeholder="请选择oauth商" value={oauthIds} onChange={(value) => {
updateConfig(id, config, 'oauthIds', value, 'oauth'); updateConfig(id, config, 'oauthIds', value, 'oauth');
}} options={oauthOptions}/> }} options={oauthOptions}/>
</Form.Item> </Form.Item>

View File

@ -95,6 +95,7 @@ export default function render(props) {
<div>* 如需启用邮箱登录请先前往配置管理邮箱设置创建系统邮箱,并完成相关配置</div> <div>* 如需启用邮箱登录请先前往配置管理邮箱设置创建系统邮箱,并完成相关配置</div>
<div>* 如需启用小程序授权登录请先前往应用管理创建小程序application,并完成基础配置</div> <div>* 如需启用小程序授权登录请先前往应用管理创建小程序application,并完成基础配置</div>
<div>* 如需启用公众号授权登录请先前往应用管理创建是服务号的公众号application,并完成基础配置</div> <div>* 如需启用公众号授权登录请先前往应用管理创建是服务号的公众号application,并完成基础配置</div>
<div>* 如需启用OAuth授权登录请先前往OAuth管理创建OAuth供应商,并启用</div>
</div> </div>
</Row> </Row>
{passports && passports.map((passport) => { {passports && passports.map((passport) => {

View File

@ -266,7 +266,8 @@ const i18ns = [
"missing_client_id": "缺少 client_id 参数", "missing_client_id": "缺少 client_id 参数",
"unknown": "未知错误,请稍后重试" "unknown": "未知错误,请稍后重试"
} }
} },
"login": "当前暂未支持该第三方应用授权登录"
} }
} }
}, },

View File

@ -140,5 +140,68 @@ const triggers = [
return count; return count;
} }
}, },
{
name: '当provider禁用时更新passport',
entity: 'oauthProvider',
action: 'update',
when: 'after',
check: (operation) => {
const { data } = operation;
return data.hasOwnProperty('ableState') && data.ableState === 'disabled';
},
fn: async ({ operation }, context, option) => {
const { filter } = operation;
const [oauthProvider] = await context.select('oauthProvider', {
data: {
id: 1,
systemId: 1,
ableState: 1,
},
filter: filter,
}, { forUpdate: true });
assert(oauthProvider, '禁用oauthProvider的filter请勿包含abledState');
let count = 0;
const { id, systemId } = oauthProvider;
const [passport] = await context.select('passport', {
data: {
id: 1,
type: 1,
config: 1,
systemId: 1,
enabled: 1,
},
filter: {
type: 'oauth',
systemId,
config: {
oauthIds: {
$contains: [id],
}
}
}
}, { forUpdate: true });
if (passport && passport.enabled) {
const { id: passportId, config } = passport;
let newConfig = cloneDeep(config);
pull(newConfig?.oauthIds, id);
if (newConfig?.oauthIds?.length <= 0) {
//无可支持的oauthProvider将启用了的passport关闭
await context.operate('passport', {
id: await generateNewIdAsync(),
action: 'update',
data: {
enabled: false,
config: newConfig,
},
filter: {
id: passport.id,
}
}, option);
count++;
}
}
return count;
}
}
]; ];
export default triggers; export default triggers;

View File

@ -1,6 +1,9 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.authorize = exports.createOAuthState = exports.getOAuthClientInfo = exports.loginByOauth = void 0; exports.loginByOauth = loginByOauth;
exports.getOAuthClientInfo = getOAuthClientInfo;
exports.createOAuthState = createOAuthState;
exports.authorize = authorize;
const tslib_1 = require("tslib"); const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert")); const assert_1 = tslib_1.__importDefault(require("assert"));
const types_1 = require("oak-domain/lib/types"); const types_1 = require("oak-domain/lib/types");
@ -20,6 +23,7 @@ async function loginByOauth(params, context) {
// 验证 state 并获取 OAuth 配置 // 验证 state 并获取 OAuth 配置
const [state] = await context.select("oauthState", { const [state] = await context.select("oauthState", {
data: { data: {
providerId: 1,
provider: { provider: {
type: 1, type: 1,
clientId: 1, clientId: 1,
@ -36,6 +40,31 @@ async function loginByOauth(params, context) {
state: stateCode, state: stateCode,
}, },
}, { dontCollect: true }); }, { dontCollect: true });
const systemId = context.getSystemId();
const [applicationPassport] = await context.select('applicationPassport', {
data: {
id: 1,
applicationId: 1,
passportId: 1,
passport: {
id: 1,
type: 1,
systemId: 1,
config: 1,
},
},
filter: {
passport: {
systemId,
type: 'oauth',
},
applicationId,
}
}, { dontCollect: true });
const allowOauth = !!(state.providerId && applicationPassport?.passport?.config?.oauthIds && applicationPassport?.passport?.config)?.oauthIds.includes(state.providerId);
if (!allowOauth) {
throw new types_1.OakUserException('error::user.loginWayDisabled');
}
(0, assert_1.default)(state, '无效的 state 参数'); (0, assert_1.default)(state, '无效的 state 参数');
(0, assert_1.default)(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用'); (0, assert_1.default)(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
// 如果已经使用 // 如果已经使用
@ -184,7 +213,6 @@ async function loginByOauth(params, context) {
return tokenValue; return tokenValue;
} }
} }
exports.loginByOauth = loginByOauth;
async function getOAuthClientInfo(params, context) { async function getOAuthClientInfo(params, context) {
const { client_id, currentUserId } = params; const { client_id, currentUserId } = params;
const closeRootMode = context.openRootMode(); const closeRootMode = context.openRootMode();
@ -247,7 +275,6 @@ async function getOAuthClientInfo(params, context) {
alreadyAuth: !!hasAuth, alreadyAuth: !!hasAuth,
}; };
} }
exports.getOAuthClientInfo = getOAuthClientInfo;
async function createOAuthState(params, context) { async function createOAuthState(params, context) {
const { providerId, userId, type } = params; const { providerId, userId, type } = params;
const closeRootMode = context.openRootMode(); const closeRootMode = context.openRootMode();
@ -269,7 +296,6 @@ async function createOAuthState(params, context) {
closeRootMode(); closeRootMode();
return state; return state;
} }
exports.createOAuthState = createOAuthState;
async function authorize(params, context) { async function authorize(params, context) {
const { response_type, client_id, redirect_uri, scope, state, action, code_challenge, code_challenge_method } = params; const { response_type, client_id, redirect_uri, scope, state, action, code_challenge, code_challenge_method } = params;
if (response_type !== 'code') { if (response_type !== 'code') {
@ -336,7 +362,7 @@ async function authorize(params, context) {
applicationId: context.getApplicationId(), applicationId: context.getApplicationId(),
userId: context.getCurrentUserId(), userId: context.getCurrentUserId(),
scope: scope === undefined ? [] : [scope], scope: scope === undefined ? [] : [scope],
expiresAt: Date.now() + 10 * 60 * 1000, expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期
// PKCE 支持 // PKCE 支持
codeChallenge: code_challenge, codeChallenge: code_challenge,
codeChallengeMethod: code_challenge_method || 'plain', codeChallengeMethod: code_challenge_method || 'plain',
@ -366,7 +392,6 @@ async function authorize(params, context) {
closeRootMode(); closeRootMode();
throw new Error('unknown action'); throw new Error('unknown action');
} }
exports.authorize = authorize;
const fetchOAuthUserInfo = async (code, providerConfig) => { const fetchOAuthUserInfo = async (code, providerConfig) => {
// 1. 使用 code 换取 access_token // 1. 使用 code 换取 access_token
const tokenResponse = await fetch(providerConfig.tokenEndpoint, { const tokenResponse = await fetch(providerConfig.tokenEndpoint, {

View File

@ -268,7 +268,8 @@ const i18ns = [
"missing_client_id": "缺少 client_id 参数", "missing_client_id": "缺少 client_id 参数",
"unknown": "未知错误,请稍后重试" "unknown": "未知错误,请稍后重试"
} }
} },
"login": "当前暂未支持该第三方应用授权登录"
} }
} }
}, },

View File

@ -143,5 +143,68 @@ const triggers = [
return count; return count;
} }
}, },
{
name: '当provider禁用时更新passport',
entity: 'oauthProvider',
action: 'update',
when: 'after',
check: (operation) => {
const { data } = operation;
return data.hasOwnProperty('ableState') && data.ableState === 'disabled';
},
fn: async ({ operation }, context, option) => {
const { filter } = operation;
const [oauthProvider] = await context.select('oauthProvider', {
data: {
id: 1,
systemId: 1,
ableState: 1,
},
filter: filter,
}, { forUpdate: true });
(0, assert_1.default)(oauthProvider, '禁用oauthProvider的filter请勿包含abledState');
let count = 0;
const { id, systemId } = oauthProvider;
const [passport] = await context.select('passport', {
data: {
id: 1,
type: 1,
config: 1,
systemId: 1,
enabled: 1,
},
filter: {
type: 'oauth',
systemId,
config: {
oauthIds: {
$contains: [id],
}
}
}
}, { forUpdate: true });
if (passport && passport.enabled) {
const { id: passportId, config } = passport;
let newConfig = (0, lodash_1.cloneDeep)(config);
(0, lodash_1.pull)(newConfig?.oauthIds, id);
if (newConfig?.oauthIds?.length <= 0) {
//无可支持的oauthProvider将启用了的passport关闭
await context.operate('passport', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'update',
data: {
enabled: false,
config: newConfig,
},
filter: {
id: passport.id,
}
}, option);
count++;
}
}
return count;
}
}
]; ];
exports.default = triggers; exports.default = triggers;

View File

@ -6,6 +6,7 @@ import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
import { loadTokenInfo, setUpTokenAndUser } from "./token"; import { loadTokenInfo, setUpTokenAndUser } from "./token";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { processUserInfo } from "../utils/oauth"; import { processUserInfo } from "../utils/oauth";
import { OAuthConfig } from "../entities/Passport";
export async function loginByOauth<ED extends EntityDict>(params: { export async function loginByOauth<ED extends EntityDict>(params: {
code: string; code: string;
@ -26,6 +27,7 @@ export async function loginByOauth<ED extends EntityDict>(params: {
// 验证 state 并获取 OAuth 配置 // 验证 state 并获取 OAuth 配置
const [state] = await context.select("oauthState", { const [state] = await context.select("oauthState", {
data: { data: {
providerId: 1,
provider: { provider: {
type: 1, type: 1,
clientId: 1, clientId: 1,
@ -41,7 +43,33 @@ export async function loginByOauth<ED extends EntityDict>(params: {
filter: { filter: {
state: stateCode, state: stateCode,
}, },
}, { dontCollect: true }) }, { dontCollect: true });
const systemId = context.getSystemId();
const [applicationPassport] = await context.select('applicationPassport', {
data: {
id: 1,
applicationId: 1,
passportId: 1,
passport: {
id: 1,
type: 1,
systemId: 1,
config: 1,
},
},
filter: {
passport: {
systemId,
type: 'oauth',
},
applicationId,
}
}, { dontCollect: true });
const allowOauth = !!(state.providerId && (applicationPassport?.passport?.config as OAuthConfig)?.oauthIds && applicationPassport?.passport?.config as OAuthConfig)?.oauthIds.includes(state.providerId);
if (!allowOauth) {
throw new OakUserException('error::user.loginWayDisabled');
}
assert(state, '无效的 state 参数'); assert(state, '无效的 state 参数');
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用'); assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
@ -414,7 +442,7 @@ export async function authorize<ED extends EntityDict>(params: {
// PKCE 支持 // PKCE 支持
codeChallenge: code_challenge, codeChallenge: code_challenge,
codeChallengeMethod: code_challenge_method || 'plain', codeChallengeMethod: code_challenge_method || 'plain',
} }
}, { dontCollect: true }) }, { dontCollect: true })

View File

@ -24,7 +24,7 @@ export default OakComponent({
state: '', state: '',
}, },
lifetimes: { lifetimes: {
ready() { async ready() {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const clientId = searchParams.get('client_id') || ''; const clientId = searchParams.get('client_id') || '';
const responseType = searchParams.get('response_type') || ''; const responseType = searchParams.get('response_type') || '';
@ -32,6 +32,38 @@ export default OakComponent({
const scope = searchParams.get('scope') || ''; const scope = searchParams.get('scope') || '';
const state = searchParams.get('state') || ''; const state = searchParams.get('state') || '';
//判断是否允许oauth登录
const application = this.features.application.getApplication();
const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id });
const oauthPassport = applicationPassports?.find((ele: EntityDict['applicationPassport']['Schema']) => ele.passport?.type === 'oauth');
const oauthIds = oauthPassport?.config?.oauthIds;
let allowOauth = false;
if (clientId) {
const { data: [oauthProvider] } = await this.features.cache.refresh('oauthProvider', {
data: {
id: 1,
clientId: 1,
systemId: 1,
},
filter: {
clientId,
systemId: application.systemId,
}
});
if (oauthProvider?.id && oauthIds?.length > 0 && oauthIds.includes(oauthProvider?.id)) {
allowOauth = true;
}
}
if (!allowOauth) {
this.setState({
hasError: true,
errorMsg: 'oauth.login',
});
this.setState({ loading: false });
return;
}
this.setState({ this.setState({
client_id: clientId, client_id: clientId,
response_type: responseType, response_type: responseType,

View File

@ -16,6 +16,7 @@
"missing_client_id": "缺少 client_id 参数", "missing_client_id": "缺少 client_id 参数",
"unknown": "未知错误,请稍后重试" "unknown": "未知错误,请稍后重试"
} }
} },
"login": "当前暂未支持该第三方应用授权登录"
} }
} }

View File

@ -31,6 +31,7 @@ const Upsert = (
> >
<Form.Item <Form.Item
label={t('name')} label={t('name')}
required={true}
rules={[{ required: true, message: t('nameRequired') }]} rules={[{ required: true, message: t('nameRequired') }]}
> >
<Input placeholder={t('namePlaceholder')} value={item.name || ""} <Input placeholder={t('namePlaceholder')} value={item.name || ""}
@ -42,6 +43,7 @@ const Upsert = (
<Form.Item <Form.Item
label={t('type')} label={t('type')}
required={true}
rules={[{ required: true, message: t('typeRequired') }]} rules={[{ required: true, message: t('typeRequired') }]}
extra={ extra={
item.type && item.type !== 'oak' && item.type !== 'gitea' ? ( item.type && item.type !== 'oak' && item.type !== 'gitea' ? (
@ -87,6 +89,7 @@ const Upsert = (
<Form.Item <Form.Item
label={t('authorizationEndpoint')} label={t('authorizationEndpoint')}
required={true}
rules={[{ required: true, message: t('authorizationEndpointRequired') }]} rules={[{ required: true, message: t('authorizationEndpointRequired') }]}
> >
<Input placeholder={t('authorizationEndpointPlaceholder')} value={item.authorizationEndpoint || ""} <Input placeholder={t('authorizationEndpointPlaceholder')} value={item.authorizationEndpoint || ""}
@ -98,6 +101,7 @@ const Upsert = (
<Form.Item <Form.Item
label={t('tokenEndpoint')} label={t('tokenEndpoint')}
required={true}
rules={[{ required: true, message: t('tokenEndpointRequired') }]} rules={[{ required: true, message: t('tokenEndpointRequired') }]}
> >
<Input placeholder={t('tokenEndpointPlaceholder')} value={item.tokenEndpoint || ""} <Input placeholder={t('tokenEndpointPlaceholder')} value={item.tokenEndpoint || ""}
@ -139,6 +143,7 @@ const Upsert = (
<Form.Item <Form.Item
label={t('clientId')} label={t('clientId')}
required={true}
rules={[{ required: true, message: t('clientIdRequired') }]} rules={[{ required: true, message: t('clientIdRequired') }]}
> >
<Input placeholder={t('clientIdPlaceholder')} value={item.clientId || ""} <Input placeholder={t('clientIdPlaceholder')} value={item.clientId || ""}
@ -150,6 +155,7 @@ const Upsert = (
<Form.Item <Form.Item
label={t('clientSecret')} label={t('clientSecret')}
required={true}
rules={[{ required: true, message: t('clientSecretRequired') }]} rules={[{ required: true, message: t('clientSecretRequired') }]}
> >
<Input.Password placeholder={t('clientSecretPlaceholder')} value={item.clientSecret || ""} <Input.Password placeholder={t('clientSecretPlaceholder')} value={item.clientSecret || ""}
@ -176,6 +182,7 @@ const Upsert = (
<Form.Item <Form.Item
label={t('redirectUri')} label={t('redirectUri')}
required={true}
rules={[{ required: true, message: t('redirectUriRequired') }]} rules={[{ required: true, message: t('redirectUriRequired') }]}
> >
<Input placeholder={t('redirectUriPlaceholder')} value={item.redirectUri || ""} <Input placeholder={t('redirectUriPlaceholder')} value={item.redirectUri || ""}
@ -196,7 +203,7 @@ const Upsert = (
label={t('ableState')} label={t('ableState')}
valuePropName="checked" valuePropName="checked"
> >
<Switch checked={!!item.ableState} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })} /> <Switch checked={item.ableState === 'enabled'} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })} />
</Form.Item> </Form.Item>
{/* <Form.Item> {/* <Form.Item>

View File

@ -69,9 +69,11 @@ export default OakComponent({
id: 1, id: 1,
name: 1, name: 1,
systemId: 1, systemId: 1,
ableState: 1,
}, },
filter: { filter: {
systemId, systemId,
ableState: 'enabled'
} }
}); });
if (oauthProviders && oauthProviders?.length > 0) { if (oauthProviders && oauthProviders?.length > 0) {

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport"; import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport";
import { EntityDict } from "../../../oak-app-domain"; import { EntityDict } from "../../../oak-app-domain";
import { Switch, Form, Input, Select, Space, Tag, InputNumber, Radio, } from 'antd'; import { Switch, Form, Select, Tag, Tooltip, } from 'antd';
import Styles from './web.module.less'; import Styles from './web.module.less';
export default function Oauth(props: { export default function Oauth(props: {
@ -24,14 +24,17 @@ export default function Oauth(props: {
<div className={Styles.item}> <div className={Styles.item}>
<div className={Styles.title}> <div className={Styles.title}>
<Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag> <Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag>
<Switch <Tooltip title={(oauthOptions && oauthOptions?.length > 0) ? '' : '请先启用oauth供应商'}>
checkedChildren="开启" <Switch
unCheckedChildren="关闭" checkedChildren="开启"
checked={enabled} unCheckedChildren="关闭"
onChange={(checked) => { checked={enabled}
changeEnabled(checked) onChange={(checked) => {
}} changeEnabled(checked)
/> }}
disabled={!(oauthOptions && oauthOptions?.length > 0)}
/>
</Tooltip>
</div> </div>
{enabled && {enabled &&
<div> <div>
@ -41,12 +44,12 @@ export default function Oauth(props: {
style={{ maxWidth: 900, marginTop: 16 }} style={{ maxWidth: 900, marginTop: 16 }}
> >
<Form.Item <Form.Item
label='oauth供商' label='oauth商'
> >
<Select <Select
mode="multiple" mode="multiple"
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder="请选择oauth供商" placeholder="请选择oauth商"
value={oauthIds} value={oauthIds}
onChange={(value: string[]) => { onChange={(value: string[]) => {
updateConfig(id, config!, 'oauthIds', value, 'oauth'); updateConfig(id, config!, 'oauthIds', value, 'oauth');

View File

@ -159,6 +159,7 @@ export default function render(props: WebComponentProps<
<div>* ,</div> <div>* ,</div>
<div>* application,</div> <div>* application,</div>
<div>* application,</div> <div>* application,</div>
<div>* OAuth授权登录OAuth管理OAuth供应商,</div>
</div> </div>
</Row> </Row>
{passports && passports.map((passport) => { {passports && passports.map((passport) => {

View File

@ -268,7 +268,8 @@ const i18ns: I18n[] = [
"missing_client_id": "缺少 client_id 参数", "missing_client_id": "缺少 client_id 参数",
"unknown": "未知错误,请稍后重试" "unknown": "未知错误,请稍后重试"
} }
} },
"login": "当前暂未支持该第三方应用授权登录"
} }
} }
}, },

View File

@ -147,6 +147,69 @@ const triggers: Trigger<EntityDict, "oauthProvider", BRC<EntityDict>>[] = [
return count; return count;
} }
} as RemoveTrigger<EntityDict, "oauthProvider", BRC<EntityDict>>, } as RemoveTrigger<EntityDict, "oauthProvider", BRC<EntityDict>>,
{
name: '当provider禁用时更新passport',
entity: 'oauthProvider',
action: 'update',
when: 'after',
check: (operation) => {
const { data } = operation as EntityDict['oauthProvider']['Update'];
return data.hasOwnProperty('ableState') && data.ableState === 'disabled';
},
fn: async ({ operation }, context, option) => {
const { filter } = operation;
const [oauthProvider] = await context.select('oauthProvider', {
data: {
id: 1,
systemId: 1,
ableState: 1,
},
filter: filter,
}, { forUpdate: true });
assert(oauthProvider, '禁用oauthProvider的filter请勿包含abledState');
let count = 0;
const { id, systemId } = oauthProvider;
const [passport] = await context.select('passport', {
data: {
id: 1,
type: 1,
config: 1,
systemId: 1,
enabled: 1,
},
filter: {
type: 'oauth',
systemId,
config: {
oauthIds: {
$contains: [id!],
}
}
}
}, { forUpdate: true })
if (passport && passport.enabled) {
const { id: passportId, config } = passport;
let newConfig = cloneDeep(config) as OAuthConfig;
pull(newConfig?.oauthIds, id);
if (newConfig?.oauthIds?.length <= 0) {
//无可支持的oauthProvider将启用了的passport关闭
await context.operate('passport', {
id: await generateNewIdAsync(),
action: 'update',
data: {
enabled: false,
config: newConfig,
},
filter: {
id: passport.id,
}
}, option);
count++;
}
}
return count;
}
}
]; ];
export default triggers; export default triggers;