feat: oidc (#1126)
* feat: oidc * feat: oidc remove comments * feat: oidc add shared type * feat: oidc add id_token sign alg * feat: oidc i18n & batch delete * feat: oidc i18n * feat: oidc import fix * feat: oidc saml list fix * feat: oidc i18n move to plugin * feat: oidc cr fix * feat: oidc cr fix * feat: oidc cr fix * feat: oidc fix nonce value * feat: oidc page extension fix * feat: oidc remove canceltoken Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
		
							parent
							
								
									78fe77e2cf
								
							
						
					
					
						commit
						1ac0032e5c
					
				
							
								
								
									
										1
									
								
								packages/app/client/src/plugins/oidc.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/app/client/src/plugins/oidc.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
export { default } from '@nocobase/plugin-oidc/client';
 | 
			
		||||
@ -18,18 +18,19 @@ import {
 | 
			
		||||
  RemoteRouteSwitchProvider,
 | 
			
		||||
  RouteSchemaComponent,
 | 
			
		||||
  RouteSwitch,
 | 
			
		||||
  useRoutes
 | 
			
		||||
  useRoutes,
 | 
			
		||||
} from '../route-switch';
 | 
			
		||||
import {
 | 
			
		||||
  AntdSchemaComponentProvider,
 | 
			
		||||
  DesignableSwitch,
 | 
			
		||||
  MenuItemInitializers,
 | 
			
		||||
  SchemaComponentProvider
 | 
			
		||||
  SchemaComponentProvider,
 | 
			
		||||
} from '../schema-component';
 | 
			
		||||
import { SchemaInitializerProvider } from '../schema-initializer';
 | 
			
		||||
import { BlockTemplateDetails, BlockTemplatePage, SchemaTemplateShortcut } from '../schema-templates';
 | 
			
		||||
import { SystemSettingsProvider, SystemSettingsShortcut } from '../system-settings';
 | 
			
		||||
import { SigninPage, SignupPage } from '../user';
 | 
			
		||||
import { SigninPageExtensionProvider } from '../user/SigninPageExtension';
 | 
			
		||||
import { compose } from './compose';
 | 
			
		||||
 | 
			
		||||
export interface ApplicationOptions {
 | 
			
		||||
@ -112,6 +113,7 @@ export class Application {
 | 
			
		||||
    });
 | 
			
		||||
    this.use(BlockSchemaComponentProvider);
 | 
			
		||||
    this.use(AntdSchemaComponentProvider);
 | 
			
		||||
    this.use(SigninPageExtensionProvider);
 | 
			
		||||
    this.use(ACLProvider);
 | 
			
		||||
    this.use(RemoteDocumentTitleProvider);
 | 
			
		||||
    this.use(PMProvider);
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,11 @@
 | 
			
		||||
import { ISchema, useForm } from '@formily/react';
 | 
			
		||||
import { Tabs } from 'antd';
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
import { Space, Tabs } from 'antd';
 | 
			
		||||
import React, { useCallback, useContext } from 'react';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Link, useHistory, useLocation } from 'react-router-dom';
 | 
			
		||||
import { SchemaComponent, useAPIClient, useCurrentDocumentTitle, useSystemSettings } from '..';
 | 
			
		||||
import { useSigninPageExtension } from './SigninPageExtension';
 | 
			
		||||
import VerificationCode from './VerificationCode';
 | 
			
		||||
 | 
			
		||||
const passwordForm: ISchema = {
 | 
			
		||||
@ -47,7 +49,7 @@ const passwordForm: ISchema = {
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function useRedirect(next = '/admin') {
 | 
			
		||||
export function useRedirect(next = '/admin') {
 | 
			
		||||
  const location = useLocation<any>();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const redirect = location?.['query']?.redirect;
 | 
			
		||||
@ -130,10 +132,16 @@ export const SigninPage = (props: SigninPageProps) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  useCurrentDocumentTitle('Signin');
 | 
			
		||||
  const ctx = useSystemSettings();
 | 
			
		||||
  const signinExtension = useSigninPageExtension();
 | 
			
		||||
  const { allowSignUp, smsAuthEnabled } = ctx?.data?.data || {};
 | 
			
		||||
  const { schema, components, scope } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
    <Space
 | 
			
		||||
      direction="vertical"
 | 
			
		||||
      className={css`
 | 
			
		||||
        display: flex;
 | 
			
		||||
      `}
 | 
			
		||||
    >
 | 
			
		||||
      {smsAuthEnabled ? (
 | 
			
		||||
        <Tabs defaultActiveKey="password">
 | 
			
		||||
          <Tabs.TabPane tab={t('Sign in via account')} key="password">
 | 
			
		||||
@ -157,11 +165,12 @@ export const SigninPage = (props: SigninPageProps) => {
 | 
			
		||||
          schema={schema || passwordForm}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <div>{signinExtension}</div>
 | 
			
		||||
      {allowSignUp && (
 | 
			
		||||
        <div>
 | 
			
		||||
          <Link to="/signup">{t('Create an account')}</Link>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
    </Space>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								packages/core/client/src/user/SigninPageExtension.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/core/client/src/user/SigninPageExtension.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
import React, { createContext, FunctionComponent, useContext } from 'react';
 | 
			
		||||
 | 
			
		||||
export interface SigninPageExtensionContextValue {
 | 
			
		||||
  components: FunctionComponent[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SigninPageExtensionContext = createContext<SigninPageExtensionContextValue>({ components: [] });
 | 
			
		||||
 | 
			
		||||
export const useSigninPageExtension = () => {
 | 
			
		||||
  const { components } = useContext(SigninPageExtensionContext);
 | 
			
		||||
  return components.map((component, index) => React.createElement(component, { key: index }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const SigninPageExtensionProvider = (props: { component: FunctionComponent; children: JSX.Element }) => {
 | 
			
		||||
  const { components } = useContext(SigninPageExtensionContext);
 | 
			
		||||
 | 
			
		||||
  const list = props.component ? [...components, props.component] : components;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SigninPageExtensionContext.Provider value={{ components: list }}>
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </SigninPageExtensionContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -2,4 +2,4 @@ export * from './CurrentUser';
 | 
			
		||||
export * from './CurrentUserProvider';
 | 
			
		||||
export * from './SigninPage';
 | 
			
		||||
export * from './SignupPage';
 | 
			
		||||
 | 
			
		||||
export * from './SigninPageExtension';
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,7 @@ export class PluginManager {
 | 
			
		||||
      if (options?.method !== 'install' || options.reload) {
 | 
			
		||||
        await this.repository.load();
 | 
			
		||||
      }
 | 
			
		||||
      this.app.acl.allow('applicationPlugins', 'list');
 | 
			
		||||
    });
 | 
			
		||||
    this.addStaticMultiple(options.plugins);
 | 
			
		||||
  }
 | 
			
		||||
@ -329,7 +330,9 @@ export class PluginManager {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getPluginPkgPrefix() {
 | 
			
		||||
    return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split(',');
 | 
			
		||||
    return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split(
 | 
			
		||||
      ',',
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async findPackage(name: string) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								packages/plugins/oidc/client.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								packages/plugins/oidc/client.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
// @ts-nocheck
 | 
			
		||||
export * from './lib/client';
 | 
			
		||||
export { default } from './lib/client';
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								packages/plugins/oidc/client.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								packages/plugins/oidc/client.js
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
 | 
			
		||||
 | 
			
		||||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
 | 
			
		||||
 | 
			
		||||
var _index = _interopRequireWildcard(require("./lib/client"));
 | 
			
		||||
 | 
			
		||||
Object.defineProperty(exports, "__esModule", {
 | 
			
		||||
  value: true
 | 
			
		||||
});
 | 
			
		||||
var _exportNames = {};
 | 
			
		||||
Object.defineProperty(exports, "default", {
 | 
			
		||||
  enumerable: true,
 | 
			
		||||
  get: function get() {
 | 
			
		||||
    return _index.default;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
Object.keys(_index).forEach(function (key) {
 | 
			
		||||
  if (key === "default" || key === "__esModule") return;
 | 
			
		||||
  if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
 | 
			
		||||
  if (key in exports && exports[key] === _index[key]) return;
 | 
			
		||||
  Object.defineProperty(exports, key, {
 | 
			
		||||
    enumerable: true,
 | 
			
		||||
    get: function get() {
 | 
			
		||||
      return _index[key];
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										12
									
								
								packages/plugins/oidc/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/plugins/oidc/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@nocobase/plugin-oidc",
 | 
			
		||||
  "version": "0.8.0-alpha.13",
 | 
			
		||||
  "main": "lib/server/index.js",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@nocobase/server": "0.8.0-alpha.13",
 | 
			
		||||
    "@nocobase/test": "0.8.0-alpha.13"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "openid-client": "^5.3.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								packages/plugins/oidc/server.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								packages/plugins/oidc/server.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
// @ts-nocheck
 | 
			
		||||
export * from './lib/server';
 | 
			
		||||
export { default } from './lib/server';
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								packages/plugins/oidc/server.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								packages/plugins/oidc/server.js
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
 | 
			
		||||
 | 
			
		||||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
 | 
			
		||||
 | 
			
		||||
var _index = _interopRequireWildcard(require("./lib/server"));
 | 
			
		||||
 | 
			
		||||
Object.defineProperty(exports, "__esModule", {
 | 
			
		||||
  value: true
 | 
			
		||||
});
 | 
			
		||||
var _exportNames = {};
 | 
			
		||||
Object.defineProperty(exports, "default", {
 | 
			
		||||
  enumerable: true,
 | 
			
		||||
  get: function get() {
 | 
			
		||||
    return _index.default;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
Object.keys(_index).forEach(function (key) {
 | 
			
		||||
  if (key === "default" || key === "__esModule") return;
 | 
			
		||||
  if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
 | 
			
		||||
  if (key in exports && exports[key] === _index[key]) return;
 | 
			
		||||
  Object.defineProperty(exports, key, {
 | 
			
		||||
    enumerable: true,
 | 
			
		||||
    get: function get() {
 | 
			
		||||
      return _index[key];
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										101
									
								
								packages/plugins/oidc/src/client/OIDCList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								packages/plugins/oidc/src/client/OIDCList.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,101 @@
 | 
			
		||||
import React, { useEffect, useState, useRef } from 'react';
 | 
			
		||||
import { Button, Space } from 'antd';
 | 
			
		||||
import { LoginOutlined } from '@ant-design/icons';
 | 
			
		||||
import { useMemoizedFn } from 'ahooks';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { useAPIClient, useRedirect } from '@nocobase/client';
 | 
			
		||||
 | 
			
		||||
export interface OIDCProvider {
 | 
			
		||||
  clientId: string;
 | 
			
		||||
  title: string;
 | 
			
		||||
  authorizeUrl: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const OIDCList = () => {
 | 
			
		||||
  const [list, setList] = useState<OIDCProvider[]>([]);
 | 
			
		||||
  const [windowHandler, setWindowHandler] = useState<Window | undefined>();
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
  const redirect = useRedirect();
 | 
			
		||||
 | 
			
		||||
  const getOidcList = async () => {
 | 
			
		||||
    const { data: pluginsRes } = await api.request({
 | 
			
		||||
      url: 'app:getPlugins',
 | 
			
		||||
    });
 | 
			
		||||
    if (!(pluginsRes.data as string[]).includes('oidc')) return;
 | 
			
		||||
    const { data: providersRes } = await api.request({
 | 
			
		||||
      url: 'oidcProviders:list',
 | 
			
		||||
      params: {
 | 
			
		||||
        filter: {
 | 
			
		||||
          enabled: true,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    setList(providersRes.data);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 打开登录弹出框
 | 
			
		||||
   */
 | 
			
		||||
  const handleOpen = useMemoizedFn(async (item: OIDCProvider) => {
 | 
			
		||||
    const response = await api.request({
 | 
			
		||||
      method: 'post',
 | 
			
		||||
      url: 'oidc:getAuthUrl',
 | 
			
		||||
      data: {
 | 
			
		||||
        clientId: item.clientId,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const authUrl = response?.data?.data;
 | 
			
		||||
    const { width, height } = screen;
 | 
			
		||||
 | 
			
		||||
    const win = window.open(
 | 
			
		||||
      authUrl,
 | 
			
		||||
      '_blank',
 | 
			
		||||
      `width=800,height=600,left=${(width - 800) / 2},top=${
 | 
			
		||||
        (height - 600) / 2
 | 
			
		||||
      },toolbar=no,menubar=no,location=no,status=no`,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    setWindowHandler(win);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 从弹出窗口,发消息回来进行登录
 | 
			
		||||
   */
 | 
			
		||||
  const handleOIDCLogin = useMemoizedFn(async (event: MessageEvent) => {
 | 
			
		||||
    await api.auth.signIn(event.data, 'oidc');
 | 
			
		||||
    windowHandler.close();
 | 
			
		||||
    setWindowHandler(undefined);
 | 
			
		||||
    redirect();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 监听弹出窗口的消息
 | 
			
		||||
   */
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!windowHandler) return;
 | 
			
		||||
    window.addEventListener('message', handleOIDCLogin);
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener('message', handleOIDCLogin);
 | 
			
		||||
    };
 | 
			
		||||
  }, [windowHandler]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getOidcList();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Space
 | 
			
		||||
      direction="vertical"
 | 
			
		||||
      className={css`
 | 
			
		||||
        display: flex;
 | 
			
		||||
      `}
 | 
			
		||||
    >
 | 
			
		||||
      {list.map((item) => (
 | 
			
		||||
        <Button shape="round" block key={item.clientId} icon={<LoginOutlined />} onClick={() => handleOpen(item)}>
 | 
			
		||||
          OIDC: {item.title}
 | 
			
		||||
        </Button>
 | 
			
		||||
      ))}
 | 
			
		||||
    </Space>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										23
									
								
								packages/plugins/oidc/src/client/OIDCPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/plugins/oidc/src/client/OIDCPanel.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { uid } from '@formily/shared';
 | 
			
		||||
import { SchemaComponent, useRecord } from '@nocobase/client';
 | 
			
		||||
import { Card } from 'antd';
 | 
			
		||||
import { oidcSchema } from './schemas/oidc';
 | 
			
		||||
import { RedirectURLInput } from './RedirectURLInput';
 | 
			
		||||
import { useOidcTranslation } from './locale';
 | 
			
		||||
 | 
			
		||||
const schema = {
 | 
			
		||||
  type: 'object',
 | 
			
		||||
  properties: {
 | 
			
		||||
    [uid()]: oidcSchema,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const OIDCPanel = () => {
 | 
			
		||||
  const { t } = useOidcTranslation();
 | 
			
		||||
  return (
 | 
			
		||||
    <Card bordered={false}>
 | 
			
		||||
      <SchemaComponent components={{ RedirectURLInput }} schema={schema} scope={{ t }} />
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										27
									
								
								packages/plugins/oidc/src/client/RedirectURLInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								packages/plugins/oidc/src/client/RedirectURLInput.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { FormLayout } from '@formily/antd';
 | 
			
		||||
import { Field } from '@formily/core';
 | 
			
		||||
import { observer, useField, useForm } from '@formily/react';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { Input, useRecord } from '@nocobase/client';
 | 
			
		||||
 | 
			
		||||
export const RedirectURLInput = observer(() => {
 | 
			
		||||
  const form = useForm();
 | 
			
		||||
  const field = useField<Field>();
 | 
			
		||||
  const record = useRecord();
 | 
			
		||||
 | 
			
		||||
  const clientId = form.values.clientId ?? record.clientId;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const { protocol, host } = window.location;
 | 
			
		||||
    field.setValue(`${protocol}//${host}/api/oidc:redirect?clientId=${clientId}`);
 | 
			
		||||
  }, [clientId]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <FormLayout layout={'vertical'}>
 | 
			
		||||
        <Input disabled value={field.value} />
 | 
			
		||||
      </FormLayout>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										38
									
								
								packages/plugins/oidc/src/client/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								packages/plugins/oidc/src/client/index.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
import { PluginManagerContext, SettingsCenterProvider, SigninPageExtensionProvider } from '@nocobase/client';
 | 
			
		||||
import React, { useContext } from 'react';
 | 
			
		||||
import { useOidcTranslation } from './locale';
 | 
			
		||||
import { OIDCList } from './OIDCList';
 | 
			
		||||
import { OIDCPanel } from './OIDCPanel';
 | 
			
		||||
 | 
			
		||||
export default function (props) {
 | 
			
		||||
  const { t } = useOidcTranslation();
 | 
			
		||||
  const ctx = useContext(PluginManagerContext);
 | 
			
		||||
  return (
 | 
			
		||||
    <SigninPageExtensionProvider component={OIDCList}>
 | 
			
		||||
      <SettingsCenterProvider
 | 
			
		||||
        settings={{
 | 
			
		||||
          'oidc-manager': {
 | 
			
		||||
            title: t('OIDC manager'),
 | 
			
		||||
            icon: 'FileOutlined',
 | 
			
		||||
            tabs: {
 | 
			
		||||
              storages: {
 | 
			
		||||
                title: t('OIDC Providers'),
 | 
			
		||||
                component: OIDCPanel,
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <PluginManagerContext.Provider
 | 
			
		||||
          value={{
 | 
			
		||||
            components: {
 | 
			
		||||
              ...ctx?.components,
 | 
			
		||||
            },
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {props.children}
 | 
			
		||||
        </PluginManagerContext.Provider>
 | 
			
		||||
      </SettingsCenterProvider>
 | 
			
		||||
    </SigninPageExtensionProvider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								packages/plugins/oidc/src/client/locale/en-US.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								packages/plugins/oidc/src/client/locale/en-US.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
export default {
 | 
			
		||||
  Enable: 'Enable',
 | 
			
		||||
  Issuer: 'Issuer',
 | 
			
		||||
  'OIDC manager': 'OIDC manager',
 | 
			
		||||
  'OIDC Providers': 'OIDC Providers',
 | 
			
		||||
  'Provider name': 'Name',
 | 
			
		||||
  'Client id': 'Client id',
 | 
			
		||||
  'Client secret': 'Client secret',
 | 
			
		||||
  'Openid configuration': 'Openid configuration',
 | 
			
		||||
  'Authorization endpoint': 'Authorization endpoint',
 | 
			
		||||
  'Access token endpoint': 'Access token endpoint',
 | 
			
		||||
  'JWKS endpoint': 'JWKS endpoint',
 | 
			
		||||
  'Userinfo endpoint': 'Userinfo endpoint',
 | 
			
		||||
  'Redirect url': 'Redirect url',
 | 
			
		||||
  'Logout endpoint': 'Logout endpoint',
 | 
			
		||||
  'Id token sign alg': 'Id token sign alg',
 | 
			
		||||
  'Add provider': 'Add provider',
 | 
			
		||||
  'Edit provider': 'Edit provider',
 | 
			
		||||
  'Delete provider': 'Delete provider',
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										24
									
								
								packages/plugins/oidc/src/client/locale/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/plugins/oidc/src/client/locale/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { i18n } from '@nocobase/client';
 | 
			
		||||
 | 
			
		||||
import zhCN from './zh-CN';
 | 
			
		||||
import enUS from './en-US';
 | 
			
		||||
import jaJP from './ja-JP';
 | 
			
		||||
import ruRU from './ru-RU';
 | 
			
		||||
import trTR from './tr-TR';
 | 
			
		||||
 | 
			
		||||
export const NAMESPACE = 'workflow';
 | 
			
		||||
 | 
			
		||||
i18n.addResources('zh-CN', NAMESPACE, zhCN);
 | 
			
		||||
i18n.addResources('en-US', NAMESPACE, enUS);
 | 
			
		||||
i18n.addResources('ja-JP', NAMESPACE, jaJP);
 | 
			
		||||
i18n.addResources('ru-RU', NAMESPACE, ruRU);
 | 
			
		||||
i18n.addResources('tr-TR', NAMESPACE, trTR);
 | 
			
		||||
 | 
			
		||||
export function lang(key: string) {
 | 
			
		||||
  return i18n.t(key, { ns: NAMESPACE });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useOidcTranslation() {
 | 
			
		||||
  return useTranslation(NAMESPACE);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/plugins/oidc/src/client/locale/ja-JP.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/oidc/src/client/locale/ja-JP.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
export default {};
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/plugins/oidc/src/client/locale/ru-RU.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/oidc/src/client/locale/ru-RU.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
export default {};
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/plugins/oidc/src/client/locale/tr-TR.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/oidc/src/client/locale/tr-TR.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
export default {};
 | 
			
		||||
							
								
								
									
										23
									
								
								packages/plugins/oidc/src/client/locale/zh-CN.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/plugins/oidc/src/client/locale/zh-CN.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
export default {
 | 
			
		||||
  Issuer: 'Issuer',
 | 
			
		||||
  Enable: '启用',
 | 
			
		||||
  Actions: '操作',
 | 
			
		||||
  Delete: '删除',
 | 
			
		||||
  Edit: '编辑',
 | 
			
		||||
  'OIDC manager': 'OIDC 管理',
 | 
			
		||||
  'OIDC Providers': 'OIDC 身份提供者',
 | 
			
		||||
  'Provider name': '名称',
 | 
			
		||||
  'Client id': '客户端 id',
 | 
			
		||||
  'Client secret': '客户端 secret',
 | 
			
		||||
  'Openid configuration': '服务发现地址',
 | 
			
		||||
  'Authorization endpoint': '授权端点',
 | 
			
		||||
  'Access token endpoint': '令牌端点',
 | 
			
		||||
  'JWKS endpoint': 'JWKS 公钥端点',
 | 
			
		||||
  'Userinfo endpoint': '用户信息端点',
 | 
			
		||||
  'Redirect url': '重定向地址',
 | 
			
		||||
  'Logout endpoint': '登出端点',
 | 
			
		||||
  'Id token sign alg': 'Id token 签名算法',
 | 
			
		||||
  'Add provider': '添加身份提供者',
 | 
			
		||||
  'Edit provider': '编辑身份提供者',
 | 
			
		||||
  'Delete provider': '删除身份提供者',
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										451
									
								
								packages/plugins/oidc/src/client/schemas/oidc.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										451
									
								
								packages/plugins/oidc/src/client/schemas/oidc.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,451 @@
 | 
			
		||||
import { ISchema } from '@formily/react';
 | 
			
		||||
import { uid } from '@formily/shared';
 | 
			
		||||
import { useActionContext, useRequest } from '@nocobase/client';
 | 
			
		||||
import { IDTOKEN_SIGN_ALG } from '../../server/shared/types';
 | 
			
		||||
 | 
			
		||||
const collection = {
 | 
			
		||||
  name: 'oidcProviders',
 | 
			
		||||
  fields: [
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'title',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Provider name")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'clientId',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Client id")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'clientSecret',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Client secret")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'issuer',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Issuer")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'openidConfiguration',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Openid configuration")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'authorizeUrl',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Authorization endpoint")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'tokenUrl',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Access token endpoint")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'jwksUrl',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("JWKS endpoint")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'userinfoUrl',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Userinfo endpoint")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'logoutUrl',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Logout endpoint")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'idTokenSignAlg',
 | 
			
		||||
      interface: 'select',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{ t("Id token sign alg") }}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Select',
 | 
			
		||||
        'x-component-props': {
 | 
			
		||||
          showSearch: false,
 | 
			
		||||
          filterSort: false,
 | 
			
		||||
          options: [
 | 
			
		||||
            {
 | 
			
		||||
              label: IDTOKEN_SIGN_ALG.RS256,
 | 
			
		||||
              value: IDTOKEN_SIGN_ALG.RS256,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              label: IDTOKEN_SIGN_ALG.HS256,
 | 
			
		||||
              value: IDTOKEN_SIGN_ALG.HS256,
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'redirectUrl',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Redirect url")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'RedirectURLInput',
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'boolean',
 | 
			
		||||
      name: 'enabled',
 | 
			
		||||
      interface: 'boolean',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Enable")}}',
 | 
			
		||||
        type: 'boolean',
 | 
			
		||||
        'x-component': 'Checkbox',
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const formProperties = {
 | 
			
		||||
  title: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  clientId: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  clientSecret: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  issuer: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  openidConfiguration: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  authorizeUrl: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  tokenUrl: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  jwksUrl: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  userinfoUrl: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  logoutUrl: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  idTokenSignAlg: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  redirectUrl: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  enabled: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
    title: '',
 | 
			
		||||
    'x-content': '{{t("Enable")}}',
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const oidcSchema: ISchema = {
 | 
			
		||||
  type: 'object',
 | 
			
		||||
  properties: {
 | 
			
		||||
    block1: {
 | 
			
		||||
      type: 'void',
 | 
			
		||||
      'x-decorator': 'ResourceActionProvider',
 | 
			
		||||
      'x-decorator-props': {
 | 
			
		||||
        collection,
 | 
			
		||||
        resourceName: 'oidcProviders',
 | 
			
		||||
        request: {
 | 
			
		||||
          resource: 'oidcProviders',
 | 
			
		||||
          action: 'list',
 | 
			
		||||
          params: {
 | 
			
		||||
            pageSize: 50,
 | 
			
		||||
            sort: ['id'],
 | 
			
		||||
            appends: [],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      'x-component': 'CollectionProvider',
 | 
			
		||||
      'x-component-props': {
 | 
			
		||||
        collection,
 | 
			
		||||
      },
 | 
			
		||||
      properties: {
 | 
			
		||||
        actions: {
 | 
			
		||||
          type: 'void',
 | 
			
		||||
          'x-component': 'ActionBar',
 | 
			
		||||
          'x-component-props': {
 | 
			
		||||
            style: {
 | 
			
		||||
              marginBottom: 16,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          properties: {
 | 
			
		||||
            delete: {
 | 
			
		||||
              type: 'void',
 | 
			
		||||
              title: '{{ t("Delete") }}',
 | 
			
		||||
              'x-component': 'Action',
 | 
			
		||||
              'x-component-props': {
 | 
			
		||||
                useAction: '{{ cm.useBulkDestroyAction }}',
 | 
			
		||||
                confirm: {
 | 
			
		||||
                  title: "{{t('Delete provider')}}",
 | 
			
		||||
                  content: "{{t('Are you sure you want to delete it?')}}",
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            create: {
 | 
			
		||||
              type: 'void',
 | 
			
		||||
              title: '{{t("Add provider")}}',
 | 
			
		||||
              'x-component': 'Action',
 | 
			
		||||
              'x-component-props': {
 | 
			
		||||
                type: 'primary',
 | 
			
		||||
              },
 | 
			
		||||
              properties: {
 | 
			
		||||
                drawer: {
 | 
			
		||||
                  type: 'void',
 | 
			
		||||
                  'x-component': 'Action.Drawer',
 | 
			
		||||
                  'x-decorator': 'Form',
 | 
			
		||||
                  'x-decorator-props': {
 | 
			
		||||
                    useValues(options) {
 | 
			
		||||
                      const ctx = useActionContext();
 | 
			
		||||
                      // 初始化数据
 | 
			
		||||
                      return useRequest(
 | 
			
		||||
                        () =>
 | 
			
		||||
                          Promise.resolve({
 | 
			
		||||
                            data: {
 | 
			
		||||
                              enable: true,
 | 
			
		||||
                              idTokenSignAlg: IDTOKEN_SIGN_ALG.RS256,
 | 
			
		||||
                            },
 | 
			
		||||
                          }),
 | 
			
		||||
                        { ...options, refreshDeps: [ctx.visible] },
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                  title: '{{t("Add provider")}}',
 | 
			
		||||
                  properties: {
 | 
			
		||||
                    ...formProperties,
 | 
			
		||||
                    footer: {
 | 
			
		||||
                      type: 'void',
 | 
			
		||||
                      'x-component': 'Action.Drawer.Footer',
 | 
			
		||||
                      properties: {
 | 
			
		||||
                        cancel: {
 | 
			
		||||
                          title: '{{t("Cancel")}}',
 | 
			
		||||
                          'x-component': 'Action',
 | 
			
		||||
                          'x-component-props': {
 | 
			
		||||
                            useAction: '{{ cm.useCancelAction }}',
 | 
			
		||||
                          },
 | 
			
		||||
                        },
 | 
			
		||||
                        submit: {
 | 
			
		||||
                          title: '{{t("Submit")}}',
 | 
			
		||||
                          'x-component': 'Action',
 | 
			
		||||
                          'x-component-props': {
 | 
			
		||||
                            type: 'primary',
 | 
			
		||||
                            useAction: '{{ cm.useCreateAction }}',
 | 
			
		||||
                          },
 | 
			
		||||
                        },
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        table: {
 | 
			
		||||
          type: 'void',
 | 
			
		||||
          'x-uid': 'input',
 | 
			
		||||
          'x-component': 'Table.Void',
 | 
			
		||||
          'x-component-props': {
 | 
			
		||||
            rowKey: 'id',
 | 
			
		||||
            rowSelection: {
 | 
			
		||||
              type: 'checkbox',
 | 
			
		||||
            },
 | 
			
		||||
            useDataSource: '{{ cm.useDataSourceFromRAC }}',
 | 
			
		||||
          },
 | 
			
		||||
          properties: {
 | 
			
		||||
            column1: {
 | 
			
		||||
              type: 'void',
 | 
			
		||||
              'x-decorator': 'Table.Column.Decorator',
 | 
			
		||||
              'x-component': 'Table.Column',
 | 
			
		||||
              properties: {
 | 
			
		||||
                title: {
 | 
			
		||||
                  type: 'string',
 | 
			
		||||
                  'x-component': 'CollectionField',
 | 
			
		||||
                  'x-read-pretty': true,
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            column2: {
 | 
			
		||||
              type: 'void',
 | 
			
		||||
              'x-decorator': 'Table.Column.Decorator',
 | 
			
		||||
              'x-component': 'Table.Column',
 | 
			
		||||
              properties: {
 | 
			
		||||
                redirectUrl: {
 | 
			
		||||
                  type: 'string',
 | 
			
		||||
                  'x-component': 'CollectionField',
 | 
			
		||||
                  'x-read-pretty': true,
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            column3: {
 | 
			
		||||
              type: 'void',
 | 
			
		||||
              'x-decorator': 'Table.Column.Decorator',
 | 
			
		||||
              'x-component': 'Table.Column',
 | 
			
		||||
              properties: {
 | 
			
		||||
                enabled: {
 | 
			
		||||
                  type: 'boolean',
 | 
			
		||||
                  'x-component': 'CollectionField',
 | 
			
		||||
                  'x-read-pretty': true,
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            column4: {
 | 
			
		||||
              type: 'void',
 | 
			
		||||
              title: '{{t("Actions")}}',
 | 
			
		||||
              'x-component': 'Table.Column',
 | 
			
		||||
              properties: {
 | 
			
		||||
                actions: {
 | 
			
		||||
                  type: 'void',
 | 
			
		||||
                  'x-component': 'Space',
 | 
			
		||||
                  'x-component-props': {
 | 
			
		||||
                    split: '|',
 | 
			
		||||
                  },
 | 
			
		||||
                  properties: {
 | 
			
		||||
                    update: {
 | 
			
		||||
                      type: 'void',
 | 
			
		||||
                      title: '{{t("Edit")}}',
 | 
			
		||||
                      'x-component': 'Action.Link',
 | 
			
		||||
                      'x-component-props': {
 | 
			
		||||
                        type: 'primary',
 | 
			
		||||
                      },
 | 
			
		||||
                      properties: {
 | 
			
		||||
                        drawer: {
 | 
			
		||||
                          type: 'void',
 | 
			
		||||
                          'x-component': 'Action.Drawer',
 | 
			
		||||
                          'x-decorator': 'Form',
 | 
			
		||||
                          'x-decorator-props': {
 | 
			
		||||
                            useValues: '{{ cm.useValuesFromRecord }}',
 | 
			
		||||
                          },
 | 
			
		||||
                          title: '{{t("Edit provider")}}',
 | 
			
		||||
                          properties: {
 | 
			
		||||
                            ...formProperties,
 | 
			
		||||
                            footer: {
 | 
			
		||||
                              type: 'void',
 | 
			
		||||
                              'x-component': 'Action.Drawer.Footer',
 | 
			
		||||
                              properties: {
 | 
			
		||||
                                cancel: {
 | 
			
		||||
                                  title: '{{t("Cancel")}}',
 | 
			
		||||
                                  'x-component': 'Action',
 | 
			
		||||
                                  'x-component-props': {
 | 
			
		||||
                                    useAction: '{{ cm.useCancelAction }}',
 | 
			
		||||
                                  },
 | 
			
		||||
                                },
 | 
			
		||||
                                submit: {
 | 
			
		||||
                                  title: '{{t("Submit")}}',
 | 
			
		||||
                                  'x-component': 'Action',
 | 
			
		||||
                                  'x-component-props': {
 | 
			
		||||
                                    type: 'primary',
 | 
			
		||||
                                    useAction: '{{ cm.useUpdateAction }}',
 | 
			
		||||
                                  },
 | 
			
		||||
                                },
 | 
			
		||||
                              },
 | 
			
		||||
                            },
 | 
			
		||||
                          },
 | 
			
		||||
                        },
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                    delete: {
 | 
			
		||||
                      type: 'void',
 | 
			
		||||
                      title: '{{ t("Delete") }}',
 | 
			
		||||
                      'x-component': 'Action.Link',
 | 
			
		||||
                      'x-component-props': {
 | 
			
		||||
                        confirm: {
 | 
			
		||||
                          title: "{{t('Delete role')}}",
 | 
			
		||||
                          content: "{{t('Are you sure you want to delete it?')}}",
 | 
			
		||||
                        },
 | 
			
		||||
                        useAction: '{{cm.useDestroyAction}}',
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										3
									
								
								packages/plugins/oidc/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/plugins/oidc/src/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
export { default } from './server';
 | 
			
		||||
 | 
			
		||||
export const namespace = require('../package.json').name;
 | 
			
		||||
							
								
								
									
										24
									
								
								packages/plugins/oidc/src/server/actions/getAuthUrl.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/plugins/oidc/src/server/actions/getAuthUrl.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
import { Context } from '@nocobase/actions';
 | 
			
		||||
import { createOIDCClient } from '../shared/createOIDCClient';
 | 
			
		||||
import { OIDCProvider } from '../shared/types';
 | 
			
		||||
 | 
			
		||||
export const getAuthUrl = async (ctx: Context, next) => {
 | 
			
		||||
  const {
 | 
			
		||||
    params: { values },
 | 
			
		||||
  } = ctx.action;
 | 
			
		||||
  const providerRepo = ctx.db.getRepository('oidcProviders');
 | 
			
		||||
  const record = await providerRepo.findOne({
 | 
			
		||||
    filter: {
 | 
			
		||||
      clientId: values.clientId,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const provider: OIDCProvider = record.toJSON();
 | 
			
		||||
  const client = await createOIDCClient(provider);
 | 
			
		||||
 | 
			
		||||
  ctx.body = client.authorizationUrl({
 | 
			
		||||
    nonce: ctx.OIDC_NONCE,
 | 
			
		||||
    scope: 'openid profile',
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return next();
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										27
									
								
								packages/plugins/oidc/src/server/actions/redirect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								packages/plugins/oidc/src/server/actions/redirect.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import { Context } from '@nocobase/actions';
 | 
			
		||||
 | 
			
		||||
export const redirect = async (ctx: Context, next) => {
 | 
			
		||||
  const { params } = ctx.action;
 | 
			
		||||
 | 
			
		||||
  const template = `
 | 
			
		||||
    <!DOCTYPE html>
 | 
			
		||||
    <html lang="en">
 | 
			
		||||
    <head>
 | 
			
		||||
      <meta charset="UTF-8">
 | 
			
		||||
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
			
		||||
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
      <title></title>
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
      <script>
 | 
			
		||||
        window.opener.postMessage(${JSON.stringify(params)}, '*');
 | 
			
		||||
      </script>
 | 
			
		||||
    </body>
 | 
			
		||||
    </html>
 | 
			
		||||
  `;
 | 
			
		||||
 | 
			
		||||
  ctx.body = template;
 | 
			
		||||
  ctx.withoutDataWrapping = true;
 | 
			
		||||
 | 
			
		||||
  await next();
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										52
									
								
								packages/plugins/oidc/src/server/authenticators/oidc.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								packages/plugins/oidc/src/server/authenticators/oidc.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
import { Context } from '@nocobase/actions';
 | 
			
		||||
import { createOIDCClient } from '../shared/createOIDCClient';
 | 
			
		||||
import { OIDCProvider } from '../shared/types';
 | 
			
		||||
 | 
			
		||||
export const oidc = async (ctx: Context, next) => {
 | 
			
		||||
  const {
 | 
			
		||||
    params: { values },
 | 
			
		||||
  } = ctx.action;
 | 
			
		||||
 | 
			
		||||
  const providerRepo = ctx.db.getRepository('oidcProviders');
 | 
			
		||||
  const record = await providerRepo.findOne({
 | 
			
		||||
    filter: {
 | 
			
		||||
      clientId: values.clientId,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const provider: OIDCProvider = record.toJSON();
 | 
			
		||||
  const client = await createOIDCClient(provider);
 | 
			
		||||
 | 
			
		||||
  const tokens = await client.callback(
 | 
			
		||||
    provider.redirectUrl,
 | 
			
		||||
    {
 | 
			
		||||
      code: values.code,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      nonce: ctx.OIDC_NONCE,
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const userinfo = await client.userinfo(tokens.access_token);
 | 
			
		||||
  const usersRepo = ctx.db.getRepository('users');
 | 
			
		||||
 | 
			
		||||
  const name = userinfo.preferred_username || userinfo.nickname || userinfo.name;
 | 
			
		||||
 | 
			
		||||
  let user = await usersRepo.findOne({
 | 
			
		||||
    filter: {
 | 
			
		||||
      nickname: name,
 | 
			
		||||
      email: userinfo.email ?? null,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (!user) {
 | 
			
		||||
    user = await usersRepo.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        nickname: name,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ctx.state.currentUser = user;
 | 
			
		||||
 | 
			
		||||
  return next();
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,86 @@
 | 
			
		||||
import { CollectionOptions } from '@nocobase/database';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'oidcProviders',
 | 
			
		||||
  title: '{{t("OIDC Providers")}}',
 | 
			
		||||
  fields: [
 | 
			
		||||
    {
 | 
			
		||||
      title: 'Provider 名称',
 | 
			
		||||
      comment: 'Provider 名称',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'title',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '客户端id',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'clientId',
 | 
			
		||||
      unique: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '客户端密钥',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'clientSecret',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: 'issuer',
 | 
			
		||||
      comment: '用于标识 token 发放来源的字段',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'issuer',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '用于获取当前 Provider 支持的各端点信息和支持的模式、参数信息,可公开访问',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'openidConfiguration',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '单点登录的地址 ',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'authorizeUrl',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '获取取 token 的接口地址',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'tokenUrl',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '令牌吊销 token 的接口',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'revokeUrl',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '验证 id_token 的接口',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'jwksUrl',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '获取用户信息的接口',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'userinfoUrl',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '是否启用',
 | 
			
		||||
      type: 'boolean',
 | 
			
		||||
      name: 'enabled',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '重定向回调地址',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'redirectUrl',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '登出端点',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'logoutUrl',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: 'id token 加密方式',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'idTokenSignAlg',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      comment: '获取用户信息的接口',
 | 
			
		||||
      type: 'json',
 | 
			
		||||
      name: 'attrsMap',
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
} as CollectionOptions;
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/plugins/oidc/src/server/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/oidc/src/server/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
export { default } from './plugin';
 | 
			
		||||
							
								
								
									
										62
									
								
								packages/plugins/oidc/src/server/plugin.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								packages/plugins/oidc/src/server/plugin.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
			
		||||
import UsersPlugin from '@nocobase/plugin-users';
 | 
			
		||||
import { InstallOptions, Plugin } from '@nocobase/server';
 | 
			
		||||
import { resolve } from 'path';
 | 
			
		||||
import { generators } from 'openid-client';
 | 
			
		||||
import { oidc } from './authenticators/oidc';
 | 
			
		||||
import { getAuthUrl } from './actions/getAuthUrl';
 | 
			
		||||
import { redirect } from './actions/redirect';
 | 
			
		||||
 | 
			
		||||
export class OidcPlugin extends Plugin {
 | 
			
		||||
  #OIDC_NONCE = null;
 | 
			
		||||
 | 
			
		||||
  afterAdd() {}
 | 
			
		||||
 | 
			
		||||
  beforeLoad() {}
 | 
			
		||||
 | 
			
		||||
  async load() {
 | 
			
		||||
    // 导入 collection
 | 
			
		||||
    await this.db.import({
 | 
			
		||||
      directory: resolve(__dirname, 'collections'),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 获取 User 插件
 | 
			
		||||
    const userPlugin = this.app.getPlugin('users') as UsersPlugin;
 | 
			
		||||
 | 
			
		||||
    // 注册 OIDC 验证器
 | 
			
		||||
    userPlugin.authenticators.register('oidc', oidc);
 | 
			
		||||
 | 
			
		||||
    // 注册接口
 | 
			
		||||
    this.app.resource({
 | 
			
		||||
      name: 'oidc',
 | 
			
		||||
      actions: {
 | 
			
		||||
        getAuthUrl,
 | 
			
		||||
        redirect,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 注册中间件,处理 nonce 值
 | 
			
		||||
    this.app.use(async (ctx, next) => {
 | 
			
		||||
      if (ctx.url.startsWith('/api/users:signin?authenticator=oidc')) {
 | 
			
		||||
        ctx.OIDC_NONCE = this.#OIDC_NONCE;
 | 
			
		||||
      }
 | 
			
		||||
      if (ctx.url.startsWith('/api/oidc:getAuthUrl')) {
 | 
			
		||||
        ctx.OIDC_NONCE = this.#OIDC_NONCE = generators.nonce();
 | 
			
		||||
      }
 | 
			
		||||
      await next();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 开放访问权限
 | 
			
		||||
    this.app.acl.allow('oidcProviders', '*');
 | 
			
		||||
    this.app.acl.allow('oidc', '*');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async install(options?: InstallOptions) {}
 | 
			
		||||
 | 
			
		||||
  async afterEnable() {}
 | 
			
		||||
 | 
			
		||||
  async afterDisable() {}
 | 
			
		||||
 | 
			
		||||
  async remove() {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default OidcPlugin;
 | 
			
		||||
							
								
								
									
										12
									
								
								packages/plugins/oidc/src/server/shared/createOIDCClient.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/plugins/oidc/src/server/shared/createOIDCClient.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
import { Issuer } from 'openid-client';
 | 
			
		||||
import { IDTOKEN_SIGN_ALG, OIDCProvider } from './types';
 | 
			
		||||
 | 
			
		||||
export const createOIDCClient = async (provider: OIDCProvider) => {
 | 
			
		||||
  const issuer = await Issuer.discover(provider.issuer);
 | 
			
		||||
  return new issuer.Client({
 | 
			
		||||
    client_id: provider.clientId,
 | 
			
		||||
    client_secret: provider.clientSecret,
 | 
			
		||||
    response_types: ['code'],
 | 
			
		||||
    id_token_signed_response_alg: provider.idTokenSignAlg,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										22
									
								
								packages/plugins/oidc/src/server/shared/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/plugins/oidc/src/server/shared/types.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
export enum IDTOKEN_SIGN_ALG {
 | 
			
		||||
  HS256 = 'HS256',
 | 
			
		||||
  RS256 = 'RS256',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface OIDCProvider {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  clientId?: string;
 | 
			
		||||
  clientSecret?: string;
 | 
			
		||||
  issuer?: string;
 | 
			
		||||
  openidConfiguration?: string;
 | 
			
		||||
  authorizeUrl?: string;
 | 
			
		||||
  tokenUrl?: string;
 | 
			
		||||
  revodeUrl?: string;
 | 
			
		||||
  jwksUrl?: string;
 | 
			
		||||
  userinfoUrl?: string;
 | 
			
		||||
  enable?: string;
 | 
			
		||||
  redirectUrl?: string;
 | 
			
		||||
  logoutUrl?: string;
 | 
			
		||||
  idTokenSignAlg?: string;
 | 
			
		||||
  attrsMap?: Record<string, string>;
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user