feat: dingtalk (#1340)
Co-authored-by: sealday <sealday@gmail.com> Reviewed-on: daoyoucloud/tachybase#1340
This commit is contained in:
		
							parent
							
								
									456df870a3
								
							
						
					
					
						commit
						aedcf15863
					
				@ -58,7 +58,7 @@
 | 
			
		||||
    "react-js-cron": "^3.1.0",
 | 
			
		||||
    "react-quill": "^2.0.0",
 | 
			
		||||
    "react-router": "^6.11.2",
 | 
			
		||||
    "react-router-dom": "^6.11.2",
 | 
			
		||||
    "react-router-dom": "^6.25.1",
 | 
			
		||||
    "react-to-print": "^2.14.7",
 | 
			
		||||
    "sanitize-html": "2.10.0",
 | 
			
		||||
    "use-deep-compare-effect": "^1.8.1"
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
    "antd-mobile-icons": "^0.3.0",
 | 
			
		||||
    "lodash": "4.17.21",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "6.x"
 | 
			
		||||
    "react-router-dom": "~6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/client": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,7 @@
 | 
			
		||||
    "react-error-boundary": "^4.0.10",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-pdf": "^7.5.1",
 | 
			
		||||
    "react-router-dom": "^6.11.2",
 | 
			
		||||
    "react-router-dom": "^6.25.1",
 | 
			
		||||
    "react-transition-group": "^4.4.5",
 | 
			
		||||
    "react-zoom-pan-pinch": "^3.4.4",
 | 
			
		||||
    "redis": "^4.6.11",
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@
 | 
			
		||||
    "qrcode": "^1.5.1",
 | 
			
		||||
    "qrcode.react": "^3.1.0",
 | 
			
		||||
    "react": "~18.3.1",
 | 
			
		||||
    "react-router-dom": "^6.11.2",
 | 
			
		||||
    "react-router-dom": "^6.25.1",
 | 
			
		||||
    "sequelize": "^6.26.0"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@ant-design/icons": "^5.3.7",
 | 
			
		||||
    "antd-mobile": "^5.36.1",
 | 
			
		||||
    "react-router-dom": "^6.23.1"
 | 
			
		||||
    "react-router-dom": "^6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/client": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
    "antd": "5.19.1",
 | 
			
		||||
    "lodash": "4.17.21",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "^6.11.2"
 | 
			
		||||
    "react-router-dom": "^6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/client": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
    "@tachybase/schema": "workspace:*",
 | 
			
		||||
    "antd": "5.19.1",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "^6.11.2"
 | 
			
		||||
    "react-router-dom": "^6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/client": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
    "cron": "^3.1.7",
 | 
			
		||||
    "react": "^18.3.1",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "^6.11.2"
 | 
			
		||||
    "react-router-dom": "^6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/actions": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
    "@ant-design/icons": "~5.3.7",
 | 
			
		||||
    "antd": "5.19.1",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "6.x"
 | 
			
		||||
    "react-router-dom": "~6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/actions": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
    "antd": "5.19.1",
 | 
			
		||||
    "lodash": "~4.17.21",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "6.x"
 | 
			
		||||
    "react-router-dom": "~6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/actions": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
    "antd": "5.19.1",
 | 
			
		||||
    "lodash": "4.17.21",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "^6.11.2",
 | 
			
		||||
    "react-router-dom": "^6.25.1",
 | 
			
		||||
    "sequelize": "^6.26.0"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								packages/plugins/@tachybase/plugin-dingtalk/.npmignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/plugins/@tachybase/plugin-dingtalk/.npmignore
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
/node_modules
 | 
			
		||||
/src
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/plugins/@tachybase/plugin-dingtalk/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/@tachybase/plugin-dingtalk/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
# @tachybase/plugin-dingding
 | 
			
		||||
							
								
								
									
										2
									
								
								packages/plugins/@tachybase/plugin-dingtalk/client.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/plugins/@tachybase/plugin-dingtalk/client.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
export * from './dist/client';
 | 
			
		||||
export { default } from './dist/client';
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/plugins/@tachybase/plugin-dingtalk/client.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/@tachybase/plugin-dingtalk/client.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
module.exports = require('./dist/client/index.js');
 | 
			
		||||
							
								
								
									
										21
									
								
								packages/plugins/@tachybase/plugin-dingtalk/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								packages/plugins/@tachybase/plugin-dingtalk/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@tachybase/plugin-dingtalk",
 | 
			
		||||
  "version": "0.21.74",
 | 
			
		||||
  "main": "dist/server/index.js",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@alicloud/dingtalk": "^2.1.30",
 | 
			
		||||
    "@alicloud/openapi-client": "^0.4.10",
 | 
			
		||||
    "@alicloud/tea-util": "^1.4.9",
 | 
			
		||||
    "@ant-design/icons": "^5.3.7",
 | 
			
		||||
    "antd": "5.19.1",
 | 
			
		||||
    "react-router-dom": "^6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/actions": "workspace:*",
 | 
			
		||||
    "@tachybase/auth": "workspace:*",
 | 
			
		||||
    "@tachybase/client": "workspace:*",
 | 
			
		||||
    "@tachybase/plugin-auth": "workspace:*",
 | 
			
		||||
    "@tachybase/server": "workspace:*",
 | 
			
		||||
    "@tachybase/test": "workspace:*"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								packages/plugins/@tachybase/plugin-dingtalk/server.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/plugins/@tachybase/plugin-dingtalk/server.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
export * from './dist/server';
 | 
			
		||||
export { default } from './dist/server';
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/plugins/@tachybase/plugin-dingtalk/server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/@tachybase/plugin-dingtalk/server.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
module.exports = require('./dist/server/index.js');
 | 
			
		||||
@ -0,0 +1,104 @@
 | 
			
		||||
import React, { useEffect } from 'react';
 | 
			
		||||
import { SchemaComponent, useAPIClient, useApp } from '@tachybase/client';
 | 
			
		||||
 | 
			
		||||
import { CopyOutlined, LoginOutlined } from '@ant-design/icons';
 | 
			
		||||
import { Button, Input, message, Typography } from 'antd';
 | 
			
		||||
import { useLocation } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import { useTranslation } from './locale';
 | 
			
		||||
 | 
			
		||||
export const AdminSettingsForm = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const app = useApp();
 | 
			
		||||
  const redirectUrl = React.useMemo(() => app.getApiUrl('dingtalk:redirect'), [app]);
 | 
			
		||||
  const onCopy = (text) => {
 | 
			
		||||
    navigator.clipboard.writeText(text), message.success(t('Copied'));
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <SchemaComponent
 | 
			
		||||
        scope={{ t }}
 | 
			
		||||
        schema={{
 | 
			
		||||
          type: 'object',
 | 
			
		||||
          properties: {
 | 
			
		||||
            public: {
 | 
			
		||||
              type: 'object',
 | 
			
		||||
              properties: {
 | 
			
		||||
                autoSignup: {
 | 
			
		||||
                  'x-decorator': 'FormItem',
 | 
			
		||||
                  type: 'boolean',
 | 
			
		||||
                  title: '{{t("Sign up automatically when the user does not exist")}}',
 | 
			
		||||
                  'x-component': 'Checkbox',
 | 
			
		||||
                  default: true,
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            dingtalk: {
 | 
			
		||||
              type: 'object',
 | 
			
		||||
              properties: {
 | 
			
		||||
                clientId: {
 | 
			
		||||
                  'x-decorator': 'FormItem',
 | 
			
		||||
                  'x-component': 'Input',
 | 
			
		||||
                  type: 'string',
 | 
			
		||||
                  required: true,
 | 
			
		||||
                  title: '{{t("Client ID")}}',
 | 
			
		||||
                },
 | 
			
		||||
                clientSecret: {
 | 
			
		||||
                  'x-decorator': 'FormItem',
 | 
			
		||||
                  'x-component': 'Input',
 | 
			
		||||
                  type: 'string',
 | 
			
		||||
                  required: true,
 | 
			
		||||
                  title: '{{t("Client Secret")}}',
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      <div>
 | 
			
		||||
        <Typography.Title level={5} style={{ fontSize: '14px' }}>
 | 
			
		||||
          {t('Redirect URL')}
 | 
			
		||||
          <span style={{ marginLeft: '2px' }}>:</span>
 | 
			
		||||
        </Typography.Title>
 | 
			
		||||
        <Input value={redirectUrl} disabled={true} addonBefore={<CopyOutlined onClick={() => onCopy(redirectUrl)} />} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const SignInButton = ({ authenticator }) => {
 | 
			
		||||
  const [href, setHref] = React.useState();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
  const urlSearchParams = new URLSearchParams(location.search);
 | 
			
		||||
  const redirect = urlSearchParams.get('redirect');
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    href && (window.location.href = href);
 | 
			
		||||
  }, [href]);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const authenticatorName = urlSearchParams.get('authenticator');
 | 
			
		||||
    const error = urlSearchParams.get('error');
 | 
			
		||||
    if (authenticatorName === authenticator.name && error) {
 | 
			
		||||
      message.error(t(error));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  const onClick = async () => {
 | 
			
		||||
    const result = await api.request({
 | 
			
		||||
      method: 'post',
 | 
			
		||||
      url: 'dingtalk:getAuthUrl',
 | 
			
		||||
      headers: { 'X-Authenticator': authenticator.name },
 | 
			
		||||
      data: { redirect: redirect },
 | 
			
		||||
    });
 | 
			
		||||
    const url = result?.data?.data?.url;
 | 
			
		||||
    if (url) {
 | 
			
		||||
      setHref(url);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Button block icon={<LoginOutlined />} shape="round" onClick={onClick}>
 | 
			
		||||
      {authenticator.title ? t(authenticator.title) : t('Sign in via DingTalk')}
 | 
			
		||||
    </Button>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,13 @@
 | 
			
		||||
import { Plugin } from '@tachybase/client';
 | 
			
		||||
import PluginAuthClient from '@tachybase/plugin-auth/client';
 | 
			
		||||
 | 
			
		||||
import { authType } from '../constants';
 | 
			
		||||
import { AdminSettingsForm, SignInButton } from './components';
 | 
			
		||||
 | 
			
		||||
export class PluginDingding extends Plugin {
 | 
			
		||||
  async load() {
 | 
			
		||||
    this.app.pm.get(PluginAuthClient).registerType(authType, { components: { SignInButton, AdminSettingsForm } });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PluginDingding;
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
import { useTranslation as useT } from '@tachybase/client';
 | 
			
		||||
 | 
			
		||||
import { namespace } from '../constants';
 | 
			
		||||
 | 
			
		||||
export function useTranslation() {
 | 
			
		||||
  return useT([namespace, 'client'], { nsMode: 'fallback' });
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,2 @@
 | 
			
		||||
export const authType = 'dingtalk';
 | 
			
		||||
export const namespace = 'dingtalk';
 | 
			
		||||
							
								
								
									
										2
									
								
								packages/plugins/@tachybase/plugin-dingtalk/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/plugins/@tachybase/plugin-dingtalk/src/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
export * from './server';
 | 
			
		||||
export { default } from './server';
 | 
			
		||||
@ -0,0 +1,15 @@
 | 
			
		||||
export default {
 | 
			
		||||
  "Sign in via DingTalk": "Sign in via DingTalk",
 | 
			
		||||
  "Client ID": "Client ID",
 | 
			
		||||
  "Client Secret": "Client Secret",
 | 
			
		||||
  "DingTalk": "DingTalk",
 | 
			
		||||
  "dingtalk": "dingtalk",
 | 
			
		||||
  "Redirect URL": "Redirect URL",
 | 
			
		||||
  "Please enter the authCode": "Please enter the authCode",
 | 
			
		||||
  "Authorizing Login": "Authorizing Login",
 | 
			
		||||
  "Sign up automatically when the user does not exist": "Sign up automatically when the user does not exist",
 | 
			
		||||
  "User not found": "User not found",
 | 
			
		||||
  "Failed to get mobile": "Failed to get mobile",
 | 
			
		||||
  "Failed to get accessToken": "Failed to get accessToken",
 | 
			
		||||
  "Copied": "Copied"
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,15 @@
 | 
			
		||||
export default {
 | 
			
		||||
  "Sign in via DingTalk": "钉钉登录",
 | 
			
		||||
  "Client ID": "Client ID",
 | 
			
		||||
  "Client Secret": "Client Secret",
 | 
			
		||||
  "DingTalk": "钉钉",
 | 
			
		||||
  "dingtalk": "钉钉",
 | 
			
		||||
  "Redirect URL": "回调地址",
 | 
			
		||||
  "Please enter the authCode": "请输入authCode",
 | 
			
		||||
  "Authorizing Login": "授权登录中...",
 | 
			
		||||
  "Sign up automatically when the user does not exist": "用户不存在时自动注册",
 | 
			
		||||
  "User not found": "用户不存在",
 | 
			
		||||
  "Failed to get mobile": "未获取到手机号",
 | 
			
		||||
  "Failed to get accessToken": "未获取到accessToken",
 | 
			
		||||
  "Copied": "已复制"
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,36 @@
 | 
			
		||||
import { Context, Next } from '@tachybase/actions';
 | 
			
		||||
import { AppSupervisor } from '@tachybase/server';
 | 
			
		||||
 | 
			
		||||
export const getAuthUrl = async (ctx: Context, next: Next) => {
 | 
			
		||||
  const { redirect } = ctx.action.params.values;
 | 
			
		||||
  const url = await ctx.auth.getAuthUrl(redirect);
 | 
			
		||||
  ctx.body = { url };
 | 
			
		||||
  await next();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const redirect = async (ctx: Context, next: Next) => {
 | 
			
		||||
  const { state } = ctx.action.params;
 | 
			
		||||
  const search = new URLSearchParams(decodeURIComponent(state));
 | 
			
		||||
  const authenticator = search.get('name');
 | 
			
		||||
  const appName = search.get('app');
 | 
			
		||||
  const redirect = search.get('redirect') || '/admin';
 | 
			
		||||
  let prefix = process.env.APP_PUBLIC_PATH || '';
 | 
			
		||||
  if (appName && appName !== 'main') {
 | 
			
		||||
    const appSupervisor = AppSupervisor.getInstance();
 | 
			
		||||
    if (appSupervisor?.runningMode !== 'single') {
 | 
			
		||||
      prefix += `apps/${appName}`;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const auth = await ctx.app.authManager.get(authenticator, ctx);
 | 
			
		||||
  if (prefix.endsWith('/')) {
 | 
			
		||||
    prefix = prefix.slice(0, -1);
 | 
			
		||||
  }
 | 
			
		||||
  try {
 | 
			
		||||
    const { token } = await auth.signIn();
 | 
			
		||||
    ctx.redirect(`${prefix}${redirect}?authenticator=${authenticator}&token=${token}`);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    ctx.logger.error('Dingtalk auth error', { error });
 | 
			
		||||
    ctx.redirect(`${prefix}/signin?redirect=${redirect}&authenticator=${authenticator}&error=${error.message}`);
 | 
			
		||||
  }
 | 
			
		||||
  await next();
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,68 @@
 | 
			
		||||
import { BaseAuth } from '@tachybase/auth';
 | 
			
		||||
 | 
			
		||||
import { namespace } from '../constants';
 | 
			
		||||
import { DingtalkClient } from './client';
 | 
			
		||||
 | 
			
		||||
export class Auth extends BaseAuth {
 | 
			
		||||
  constructor(config) {
 | 
			
		||||
    const { ctx } = config;
 | 
			
		||||
    super({
 | 
			
		||||
      ...config,
 | 
			
		||||
      userCollection: ctx.db.getCollection('users'),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  async validate() {
 | 
			
		||||
    const ctx = this.ctx;
 | 
			
		||||
    const { authCode } = ctx.action.params || {};
 | 
			
		||||
    if (!authCode) {
 | 
			
		||||
      ctx.throw(401, ctx.t('Please enter the authCode', { ns: namespace }));
 | 
			
		||||
    }
 | 
			
		||||
    const dingtalkClient = new DingtalkClient({
 | 
			
		||||
      clientId: this.options?.dingtalk?.clientId,
 | 
			
		||||
      clientSecret: this.options?.clientSecret,
 | 
			
		||||
      ctx: this.ctx,
 | 
			
		||||
    });
 | 
			
		||||
    const accessToken = await dingtalkClient.getAccessToken(authCode);
 | 
			
		||||
    if (!accessToken) {
 | 
			
		||||
      ctx.throw(401, ctx.t('Failed to get accessToken', { ns: namespace }));
 | 
			
		||||
    }
 | 
			
		||||
    const userInfo = await dingtalkClient.getUserInfo(accessToken);
 | 
			
		||||
    const { mobile, nick, email } = userInfo;
 | 
			
		||||
    if (!mobile) {
 | 
			
		||||
      ctx.throw(401, ctx.t('Failed to get mobile', { ns: namespace }));
 | 
			
		||||
    }
 | 
			
		||||
    const authenticator = this.authenticator;
 | 
			
		||||
    let user = await authenticator.findUser(mobile);
 | 
			
		||||
    if (user) {
 | 
			
		||||
      return user;
 | 
			
		||||
    }
 | 
			
		||||
    user = await this.userRepository.findOne({
 | 
			
		||||
      filter: { phone: mobile },
 | 
			
		||||
    });
 | 
			
		||||
    if (user) {
 | 
			
		||||
      await authenticator.addUser(user.id, {
 | 
			
		||||
        through: {
 | 
			
		||||
          uuid: mobile,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
      return user;
 | 
			
		||||
    }
 | 
			
		||||
    const { autoSignup } = this.options?.public || {};
 | 
			
		||||
    if (!autoSignup) {
 | 
			
		||||
      ctx.throw(401, ctx.t('User not found', { ns: namespace }));
 | 
			
		||||
    }
 | 
			
		||||
    return await authenticator.newUser(mobile, {
 | 
			
		||||
      nickname: nick ?? null,
 | 
			
		||||
      phone: mobile ?? null,
 | 
			
		||||
      email: email ?? null,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  async getAuthUrl(redirect) {
 | 
			
		||||
    const clientId = this.options?.dingtalk?.clientId;
 | 
			
		||||
    const app = this.ctx.app.name;
 | 
			
		||||
    const redirectUrl = encodeURIComponent(`${this.ctx.origin}${process.env.API_BASE_PATH}dingtalk:redirect`);
 | 
			
		||||
    const state = encodeURIComponent(`redirect=${redirect}&app=${app}&name=${this.ctx.headers['x-authenticator']}`);
 | 
			
		||||
    const url = `https://login.dingtalk.com/oauth2/auth?client_id=${clientId}&response_type=code&scope=openid&state=${state}&redirect_uri=${redirectUrl}&prompt=consent`;
 | 
			
		||||
    return url;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,57 @@
 | 
			
		||||
import ContactClient, { GetUserHeaders } from '@alicloud/dingtalk/contact_1_0';
 | 
			
		||||
import OAuthClient, { GetUserTokenRequest } from '@alicloud/dingtalk/oauth2_1_0';
 | 
			
		||||
import { Config } from '@alicloud/openapi-client';
 | 
			
		||||
import Util, { RuntimeOptions } from '@alicloud/tea-util';
 | 
			
		||||
 | 
			
		||||
export class DingtalkClient {
 | 
			
		||||
  config;
 | 
			
		||||
  ctx;
 | 
			
		||||
  constructor(config) {
 | 
			
		||||
    this.config = config;
 | 
			
		||||
    this.ctx = config?.ctx;
 | 
			
		||||
  }
 | 
			
		||||
  createAuthClient() {
 | 
			
		||||
    const config = new Config({});
 | 
			
		||||
    config.protocol = 'https';
 | 
			
		||||
    config.regionId = 'central';
 | 
			
		||||
    return new OAuthClient(config);
 | 
			
		||||
  }
 | 
			
		||||
  createContactClient() {
 | 
			
		||||
    const config = new Config({});
 | 
			
		||||
    config.protocol = 'https';
 | 
			
		||||
    config.regionId = 'central';
 | 
			
		||||
    return new ContactClient(config);
 | 
			
		||||
  }
 | 
			
		||||
  async getAccessToken(code) {
 | 
			
		||||
    const client = this.createAuthClient();
 | 
			
		||||
    const getUserTokenRequest = new GetUserTokenRequest({
 | 
			
		||||
      clientId: this.config.clientId,
 | 
			
		||||
      clientSecret: this.config.clientSecret,
 | 
			
		||||
      code,
 | 
			
		||||
      grantType: 'authorization_code',
 | 
			
		||||
    });
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await client.getUserToken(getUserTokenRequest);
 | 
			
		||||
      return response?.body?.accessToken || '';
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!Util.empty(err.code) && !Util.empty(err.message)) {
 | 
			
		||||
        this.ctx?.log.debug(`DingTalk get accessToken error code:${err.code}, message:${err.message}`);
 | 
			
		||||
      }
 | 
			
		||||
      return '';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  async getUserInfo(accessToken) {
 | 
			
		||||
    const client = this.createContactClient();
 | 
			
		||||
    const getUserHeaders = new GetUserHeaders({});
 | 
			
		||||
    getUserHeaders.xAcsDingtalkAccessToken = accessToken;
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await client.getUserWithOptions('me', getUserHeaders, new RuntimeOptions({}));
 | 
			
		||||
      return response?.body || {};
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!Util.empty(err.code) && !Util.empty(err.message)) {
 | 
			
		||||
        this.ctx?.log.debug(`DingTalk get userInfo error code:${err.code}, message:${err.message}`);
 | 
			
		||||
      }
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
export { default } from './plugin';
 | 
			
		||||
@ -0,0 +1,53 @@
 | 
			
		||||
import { tval } from '@tachybase/client';
 | 
			
		||||
import { Gateway, Plugin } from '@tachybase/server';
 | 
			
		||||
 | 
			
		||||
import { authType, namespace } from '../constants';
 | 
			
		||||
import { getAuthUrl, redirect } from './actions/dingding';
 | 
			
		||||
import { Auth } from './auth';
 | 
			
		||||
 | 
			
		||||
export class PluginDingdingServer extends Plugin {
 | 
			
		||||
  async afterAdd() {}
 | 
			
		||||
 | 
			
		||||
  async beforeLoad() {}
 | 
			
		||||
 | 
			
		||||
  async load() {
 | 
			
		||||
    console.log('hello world  dingding');
 | 
			
		||||
    this.app.authManager.registerTypes(authType, {
 | 
			
		||||
      auth: Auth,
 | 
			
		||||
      title: tval('DingTalk', { ns: namespace }),
 | 
			
		||||
    });
 | 
			
		||||
    this.app.resourcer.define({
 | 
			
		||||
      name: 'dingtalk',
 | 
			
		||||
      actions: {
 | 
			
		||||
        getAuthUrl: getAuthUrl,
 | 
			
		||||
        redirect: redirect,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    this.app.acl.allow('dingtalk', '*', 'public');
 | 
			
		||||
    Gateway.getInstance().addAppSelectorMiddleware(async (ctx, next) => {
 | 
			
		||||
      const { req } = ctx;
 | 
			
		||||
      const url = new URL(req.url, `http://${req.headers.host}`);
 | 
			
		||||
      const params = url.searchParams;
 | 
			
		||||
      const state = params.get('state');
 | 
			
		||||
      if (!state) {
 | 
			
		||||
        return next();
 | 
			
		||||
      }
 | 
			
		||||
      const search = new URLSearchParams(state);
 | 
			
		||||
      const appName = search.get('app');
 | 
			
		||||
      if (appName) {
 | 
			
		||||
        ctx.resolvedAppName = appName;
 | 
			
		||||
      }
 | 
			
		||||
      await next();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async install() {}
 | 
			
		||||
 | 
			
		||||
  async afterEnable() {}
 | 
			
		||||
 | 
			
		||||
  async afterDisable() {}
 | 
			
		||||
 | 
			
		||||
  async remove() {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PluginDingdingServer;
 | 
			
		||||
@ -25,7 +25,7 @@
 | 
			
		||||
    "lodash": "4.17.21",
 | 
			
		||||
    "react": "^18.3.1",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "^6.11.2"
 | 
			
		||||
    "react-router-dom": "^6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/client": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@
 | 
			
		||||
    "react": "~18.3.1",
 | 
			
		||||
    "react-dom": "~18.3.1",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "^6.11.2"
 | 
			
		||||
    "react-router-dom": "^6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/actions": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@
 | 
			
		||||
    "react": "~18.3.1",
 | 
			
		||||
    "react-dom": "~18.3.1",
 | 
			
		||||
    "react-i18next": "~14.1.2",
 | 
			
		||||
    "react-router-dom": "6.x"
 | 
			
		||||
    "react-router-dom": "~6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/client": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@
 | 
			
		||||
    "pg": "^8.7.3",
 | 
			
		||||
    "react": "~18.3.1",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "^6.11.2"
 | 
			
		||||
    "react-router-dom": "^6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/client": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@
 | 
			
		||||
    "openid-client": "^5.4.2",
 | 
			
		||||
    "react": "~18.3.1",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "^6.11.2"
 | 
			
		||||
    "react-router-dom": "^6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/actions": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ export class OidcPlugin extends Plugin {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 注册接口
 | 
			
		||||
    this.app.resource({
 | 
			
		||||
    this.app.resourcer.define({
 | 
			
		||||
      name: 'oidc',
 | 
			
		||||
      actions: {
 | 
			
		||||
        getAuthUrl,
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
    "antd": "5.19.1",
 | 
			
		||||
    "react": "~18.3.1",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-router-dom": "^6.11.2"
 | 
			
		||||
    "react-router-dom": "^6.25.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@tachybase/actions": "workspace:*",
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@
 | 
			
		||||
    "react": "~18.3.1",
 | 
			
		||||
    "react-i18next": "^14.1.2",
 | 
			
		||||
    "react-js-cron": "^3.1.0",
 | 
			
		||||
    "react-router-dom": "^6.11.2",
 | 
			
		||||
    "react-router-dom": "^6.25.1",
 | 
			
		||||
    "sequelize": "^6.26.0"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										603
									
								
								pnpm-lock.yaml
									
									
									
									
									
								
							
							
						
						
									
										603
									
								
								pnpm-lock.yaml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user