feat(client,sdk): improve api client (#425)
* feat(client,sdk): improve api client * feat: add test cases * docs: update doc * fix(sdk): cannot destructure property 'authClass' of 'instance' as it is undefined
This commit is contained in:
parent
735581d20d
commit
4412efc145
@ -34,7 +34,6 @@ Request Body
|
|||||||
Response 200 (application/json)
|
Response 200 (application/json)
|
||||||
{
|
{
|
||||||
"data": {},
|
"data": {},
|
||||||
"meta": {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -51,7 +50,6 @@ Request Body
|
|||||||
Response 200 (application/json)
|
Response 200 (application/json)
|
||||||
{
|
{
|
||||||
"data": {},
|
"data": {},
|
||||||
"meta": {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -75,8 +73,7 @@ Response 200 (application/json)
|
|||||||
"user": {
|
"user": {
|
||||||
"id": 1
|
"id": 1
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"meta": {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -117,8 +114,7 @@ Response 200 (application/json)
|
|||||||
"id": 2
|
"id": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"meta": {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@
|
|||||||
|
|
||||||
## boolean
|
## boolean
|
||||||
|
|
||||||
- $truthy
|
- $isTruthy
|
||||||
- $falsy
|
- $isFalsy
|
||||||
|
|
||||||
## date
|
## date
|
||||||
|
|
||||||
|
@ -280,13 +280,7 @@ Response 200 (application/json)
|
|||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
id: 1
|
id: 1
|
||||||
},
|
}
|
||||||
meta: {
|
|
||||||
count: 1
|
|
||||||
page: 1,
|
|
||||||
pageSize: 1,
|
|
||||||
totalPage: 1
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -52,15 +52,67 @@ mock.onGet('users:get').reply(200, {
|
|||||||
await api.request({ url: 'users:get' });
|
await api.request({ url: 'users:get' });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
APIClient uses localStorage by default, you can also custom storage.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Storage } from '@nocobase/sdk';
|
||||||
|
|
||||||
|
class MemoryStorage extends Storage {
|
||||||
|
items = new Map();
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.items.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(key: string) {
|
||||||
|
return this.items.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(key: string, value: string) {
|
||||||
|
return this.items.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(key: string) {
|
||||||
|
return this.items.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new APIClient({
|
||||||
|
baseURL: 'https://localhost:8000/api',
|
||||||
|
storageClass: CustomStorage,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// Pass token directly
|
// sign in and remember the current token
|
||||||
api.auth.token = '123';
|
api.auth.signIn({ email, password });
|
||||||
// Or sign in via signIn
|
// sign out and delete the token
|
||||||
api.auth.signIn();
|
|
||||||
// Log out and delete the token cache
|
|
||||||
api.auth.signOut();
|
api.auth.signOut();
|
||||||
|
// set the token
|
||||||
|
api.auth.setToken('123');
|
||||||
|
// set the role (multiple roles)
|
||||||
|
api.auth.setRole('admin');
|
||||||
|
// set the locale (multiple languages)
|
||||||
|
api.auth.setLocale('zh-CN');
|
||||||
|
```
|
||||||
|
|
||||||
|
Custom Auth
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Auth } from '@nocobase/sdk';
|
||||||
|
|
||||||
|
class CustomAuth extends Auth {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new APIClient({
|
||||||
|
baseURL: 'https://localhost:8000/api',
|
||||||
|
authClass: CustomAuth,
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Request
|
## Request
|
||||||
|
@ -1,5 +1,31 @@
|
|||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
|
## 2022/05/26 ~ v0.7.0-alpha.82
|
||||||
|
|
||||||
|
- feat(client,sdk): improve api client
|
||||||
|
|
||||||
|
### Breaking Change
|
||||||
|
|
||||||
|
There are major changes to the `@nocobase/sdk` API, see details [JavaScript SDK](./development/http-api/javascript-sdk.md)
|
||||||
|
|
||||||
|
## 2022/05/25 ~ v0.7.0-alpha.81
|
||||||
|
|
||||||
|
- feat: add create-plugin command (#423)
|
||||||
|
- fix: "typescript": "4.5.5"
|
||||||
|
- docs: update documentation
|
||||||
|
- fix(client): filter menu item schema by permissions
|
||||||
|
- fix(database): cannot read properties of null (reading 'substring')
|
||||||
|
- fix(client): add description
|
||||||
|
- fix(client): clone schema before insert
|
||||||
|
- feat(client): add a description to the junction collection field
|
||||||
|
- fix(devtools): unexpected token '.'
|
||||||
|
|
||||||
|
## 2022/05/24 ~ v0.7.0-alpha.78
|
||||||
|
|
||||||
|
- fix(client): add RemoteDocumentTitleProvider
|
||||||
|
- fix(client): incomplete calendar events
|
||||||
|
- fix(plugin-users): add translations (#416)
|
||||||
|
|
||||||
## 2022/05/23 ~ v0.7.0-alpha.59
|
## 2022/05/23 ~ v0.7.0-alpha.59
|
||||||
|
|
||||||
- feat(docs): add alert message
|
- feat(docs): add alert message
|
||||||
|
@ -34,7 +34,6 @@ Request Body
|
|||||||
Response 200 (application/json)
|
Response 200 (application/json)
|
||||||
{
|
{
|
||||||
"data": {},
|
"data": {},
|
||||||
"meta": {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -50,8 +49,7 @@ Request Body
|
|||||||
|
|
||||||
Response 200 (application/json)
|
Response 200 (application/json)
|
||||||
{
|
{
|
||||||
"data": {},
|
"data": {}
|
||||||
"meta": {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -75,8 +73,7 @@ Response 200 (application/json)
|
|||||||
"user": {
|
"user": {
|
||||||
"id": 1
|
"id": 1
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"meta": {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -117,8 +114,7 @@ Response 200 (application/json)
|
|||||||
"id": 2
|
"id": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"meta": {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -141,6 +137,3 @@ Response 200 (application/json)
|
|||||||
### `remove`
|
### `remove`
|
||||||
|
|
||||||
### `toggle`
|
### `toggle`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@
|
|||||||
|
|
||||||
## boolean
|
## boolean
|
||||||
|
|
||||||
- $truthy
|
- $isTruly
|
||||||
- $falsy
|
- $isFalsy
|
||||||
|
|
||||||
## date
|
## date
|
||||||
|
|
||||||
|
@ -281,12 +281,6 @@ Response 200 (application/json)
|
|||||||
data: {
|
data: {
|
||||||
id: 1
|
id: 1
|
||||||
},
|
},
|
||||||
meta: {
|
|
||||||
count: 1
|
|
||||||
page: 1,
|
|
||||||
pageSize: 1,
|
|
||||||
totalPage: 1
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -52,15 +52,67 @@ mock.onGet('users:get').reply(200, {
|
|||||||
await api.request({ url: 'users:get' });
|
await api.request({ url: 'users:get' });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
APIClient 默认使用的 localStorage,你也可以自定义 Storage,如:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Storage } from '@nocobase/sdk';
|
||||||
|
|
||||||
|
class MemoryStorage extends Storage {
|
||||||
|
items = new Map();
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.items.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(key: string) {
|
||||||
|
return this.items.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(key: string, value: string) {
|
||||||
|
return this.items.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(key: string) {
|
||||||
|
return this.items.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new APIClient({
|
||||||
|
baseURL: 'https://localhost:8000/api',
|
||||||
|
storageClass: CustomStorage,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// 直接传 token
|
// 登录并记录 token
|
||||||
api.auth.token = '123';
|
api.auth.signIn({ email, password });
|
||||||
// 或者通过 signIn 登录
|
// 注销并删除 token
|
||||||
api.auth.signIn();
|
|
||||||
// 注销并删除 token 缓存
|
|
||||||
api.auth.signOut();
|
api.auth.signOut();
|
||||||
|
// 设置 token
|
||||||
|
api.auth.setToken('123');
|
||||||
|
// 设置 role(当需要多角色时)
|
||||||
|
api.auth.setRole('admin');
|
||||||
|
// 设置 locale(当需要多语言时)
|
||||||
|
api.auth.setLocale('zh-CN');
|
||||||
|
```
|
||||||
|
|
||||||
|
自定义 Auth
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Auth } from '@nocobase/sdk';
|
||||||
|
|
||||||
|
class CustomAuth extends Auth {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new APIClient({
|
||||||
|
baseURL: 'https://localhost:8000/api',
|
||||||
|
authClass: CustomAuth,
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Request
|
## Request
|
||||||
|
@ -1,5 +1,31 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## 2022/05/26 ~ v0.7.0-alpha.82
|
||||||
|
|
||||||
|
- feat(client,sdk): improve api client
|
||||||
|
|
||||||
|
### Breaking Change
|
||||||
|
|
||||||
|
There are major changes to the `APIClient` API, see details [JavaScript SDK](./development/http-api/javascript-sdk.md)
|
||||||
|
|
||||||
|
## 2022/05/25 ~ v0.7.0-alpha.81
|
||||||
|
|
||||||
|
- feat: add create-plugin command (#423)
|
||||||
|
- fix: "typescript": "4.5.5"
|
||||||
|
- docs: update documentation
|
||||||
|
- fix(client): filter menu item schema by permissions
|
||||||
|
- fix(database): cannot read properties of null (reading 'substring')
|
||||||
|
- fix(client): add description
|
||||||
|
- fix(client): clone schema before insert
|
||||||
|
- feat(client): add a description to the junction collection field
|
||||||
|
- fix(devtools): unexpected token '.'
|
||||||
|
|
||||||
|
## 2022/05/24 ~ v0.7.0-alpha.78
|
||||||
|
|
||||||
|
- fix(client): add RemoteDocumentTitleProvider
|
||||||
|
- fix(client): incomplete calendar events
|
||||||
|
- fix(plugin-users): add translations (#416)
|
||||||
|
|
||||||
## 2022/05/23 ~ v0.7.0-alpha.62
|
## 2022/05/23 ~ v0.7.0-alpha.62
|
||||||
|
|
||||||
- feat(docs): add alert message
|
- feat(docs): add alert message
|
||||||
|
@ -2,6 +2,9 @@ import { APIClient } from '@nocobase/client';
|
|||||||
|
|
||||||
const apiClient = new APIClient({
|
const apiClient = new APIClient({
|
||||||
baseURL: process.env.API_BASE_URL,
|
baseURL: process.env.API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'X-Hostname': window?.location?.hostname,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiClient;
|
export default apiClient;
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"@formily/antd": "2.0.20",
|
"@formily/antd": "2.0.20",
|
||||||
"@formily/core": "2.0.20",
|
"@formily/core": "2.0.20",
|
||||||
"@formily/react": "2.0.20",
|
"@formily/react": "2.0.20",
|
||||||
|
"@nocobase/sdk": "0.7.0-alpha.81",
|
||||||
"@nocobase/utils": "0.7.0-alpha.81",
|
"@nocobase/utils": "0.7.0-alpha.81",
|
||||||
"ahooks": "^3.0.5",
|
"ahooks": "^3.0.5",
|
||||||
"antd": "~4.19.5",
|
"antd": "~4.19.5",
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { useCookieState } from 'ahooks';
|
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { useRequest } from '../api-client';
|
import { useAPIClient, useRequest } from '../api-client';
|
||||||
import { useCollection } from '../collection-manager';
|
import { useCollection } from '../collection-manager';
|
||||||
import { useRecordIsOwn } from '../record-provider';
|
import { useRecordIsOwn } from '../record-provider';
|
||||||
import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
||||||
@ -22,7 +21,7 @@ export const ACLProvider = (props) => {
|
|||||||
|
|
||||||
export const ACLRolesCheckProvider = (props) => {
|
export const ACLRolesCheckProvider = (props) => {
|
||||||
const { setDesignable } = useDesignable();
|
const { setDesignable } = useDesignable();
|
||||||
const [roleName, setRoleName] = useCookieState('currentRoleName');
|
const api = useAPIClient();
|
||||||
const result = useRequest(
|
const result = useRequest(
|
||||||
{
|
{
|
||||||
url: 'roles:check',
|
url: 'roles:check',
|
||||||
@ -32,8 +31,8 @@ export const ACLRolesCheckProvider = (props) => {
|
|||||||
if (!data?.data?.allowConfigure && !data?.data?.allowAll) {
|
if (!data?.data?.allowConfigure && !data?.data?.allowAll) {
|
||||||
setDesignable(false);
|
setDesignable(false);
|
||||||
}
|
}
|
||||||
if (data?.data?.role !== roleName) {
|
if (data?.data?.role !== api.auth.role) {
|
||||||
setRoleName(data?.data?.role);
|
api.auth.setRole(data?.data?.role);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3,10 +3,11 @@ import enUS from 'antd/lib/locale/en_US';
|
|||||||
import zhCN from 'antd/lib/locale/zh_CN';
|
import zhCN from 'antd/lib/locale/zh_CN';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useRequest } from '../api-client';
|
import { useAPIClient, useRequest } from '../api-client';
|
||||||
|
|
||||||
export function AntdConfigProvider(props) {
|
export function AntdConfigProvider(props) {
|
||||||
const { remoteLocale, ...others } = props;
|
const { remoteLocale, ...others } = props;
|
||||||
|
const api = useAPIClient();
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const { loading } = useRequest(
|
const { loading } = useRequest(
|
||||||
{
|
{
|
||||||
@ -14,8 +15,9 @@ export function AntdConfigProvider(props) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
const locale = localStorage.getItem('NOCOBASE_LANG');
|
const locale = api.auth.locale;
|
||||||
if (data?.data?.lang && !locale) {
|
if (data?.data?.lang && !locale) {
|
||||||
|
api.auth.setLocale(data?.data?.lang);
|
||||||
i18n.changeLanguage(data?.data?.lang);
|
i18n.changeLanguage(data?.data?.lang);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,124 +1,6 @@
|
|||||||
import { observable } from '@formily/reactive';
|
import { APIClient as APIClientSDK } from '@nocobase/sdk';
|
||||||
import { Result } from 'ahooks/lib/useRequest/src/types';
|
import { Result } from 'ahooks/lib/useRequest/src/types';
|
||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
import qs from 'qs';
|
|
||||||
|
|
||||||
export interface ActionParams {
|
|
||||||
filterByTk?: any;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceActionOptions<P = any> = {
|
|
||||||
resource?: string;
|
|
||||||
resourceOf?: any;
|
|
||||||
action?: string;
|
|
||||||
params?: P;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IResource {
|
|
||||||
list?: (params?: ActionParams) => Promise<any>;
|
|
||||||
get?: (params?: ActionParams) => Promise<any>;
|
|
||||||
create?: (params?: ActionParams) => Promise<any>;
|
|
||||||
update?: (params?: ActionParams) => Promise<any>;
|
|
||||||
destroy?: (params?: ActionParams) => Promise<any>;
|
|
||||||
[key: string]: (params?: ActionParams) => Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class APIClient {
|
|
||||||
axios: AxiosInstance;
|
|
||||||
|
|
||||||
|
export class APIClient extends APIClientSDK {
|
||||||
services: Record<string, Result<any, any>>;
|
services: Record<string, Result<any, any>>;
|
||||||
|
|
||||||
tokenKey = 'NOCOBASE_TOKEN';
|
|
||||||
|
|
||||||
constructor(instance?: AxiosInstance | AxiosRequestConfig) {
|
|
||||||
this.services = observable({});
|
|
||||||
if (typeof instance === 'function') {
|
|
||||||
this.axios = instance;
|
|
||||||
} else {
|
|
||||||
this.axios = axios.create(instance);
|
|
||||||
}
|
|
||||||
this.qsMiddleware();
|
|
||||||
this.authMiddleware();
|
|
||||||
}
|
|
||||||
|
|
||||||
qsMiddleware() {
|
|
||||||
this.axios.interceptors.request.use((config) => {
|
|
||||||
config.paramsSerializer = (params) => {
|
|
||||||
return qs.stringify(params, {
|
|
||||||
strictNullHandling: true,
|
|
||||||
arrayFormat: 'brackets',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
authMiddleware() {
|
|
||||||
this.axios.interceptors.request.use((config) => {
|
|
||||||
const token = localStorage.getItem(this.tokenKey);
|
|
||||||
config.headers['X-Locale'] = localStorage.getItem('NOCOBASE_LANG');
|
|
||||||
config.headers['X-Hostname'] = window.location.hostname;
|
|
||||||
if (token) {
|
|
||||||
config.headers['Authorization'] = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
const currentRoleName = Cookies.get('currentRoleName');
|
|
||||||
if (currentRoleName) {
|
|
||||||
config.headers['X-Role'] = currentRoleName;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
setBearerToken(token: any) {
|
|
||||||
localStorage.setItem(this.tokenKey, token || '');
|
|
||||||
Cookies.remove('currentRoleName');
|
|
||||||
}
|
|
||||||
|
|
||||||
service(uid: string): Result<any, any> {
|
|
||||||
return this.services[uid];
|
|
||||||
}
|
|
||||||
|
|
||||||
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R> {
|
|
||||||
const { resource, resourceOf, action, params } = config as any;
|
|
||||||
if (resource) {
|
|
||||||
return this.resource(resource, resourceOf)[action](params);
|
|
||||||
}
|
|
||||||
return this.axios.request<T, R, D>(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
resource(name: string, of?: any): IResource {
|
|
||||||
const target = {};
|
|
||||||
const handler = {
|
|
||||||
get: (_: any, actionName: string) => {
|
|
||||||
let url = name.split('.').join(`/${of || '_'}/`);
|
|
||||||
url += `:${actionName}`;
|
|
||||||
const config: AxiosRequestConfig = { url };
|
|
||||||
if (['get', 'list'].includes(actionName)) {
|
|
||||||
config['method'] = 'get';
|
|
||||||
} else {
|
|
||||||
config['method'] = 'post';
|
|
||||||
}
|
|
||||||
return async (params?: ActionParams) => {
|
|
||||||
const { values, filter, ...others } = params || {};
|
|
||||||
config['params'] = others;
|
|
||||||
if (filter) {
|
|
||||||
if (typeof filter === 'string') {
|
|
||||||
config['params']['filter'] = filter;
|
|
||||||
} else {
|
|
||||||
config['params']['filter'] = JSON.stringify(filter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (config.method !== 'get') {
|
|
||||||
config['data'] = values || {};
|
|
||||||
}
|
|
||||||
return await this.request(config);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return new Proxy(target, handler);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ const log = require('debug')('i18next');
|
|||||||
export const i18n = i18next.createInstance();
|
export const i18n = i18next.createInstance();
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
lng: localStorage.getItem('NOCOBASE_LANG') || 'en-US',
|
lng: localStorage.getItem('NOCOBASE_LOCALE') || 'en-US',
|
||||||
// debug: true,
|
// debug: true,
|
||||||
defaultNS: 'client',
|
defaultNS: 'client',
|
||||||
// parseMissingKeyHandler: (key) => {
|
// parseMissingKeyHandler: (key) => {
|
||||||
@ -29,9 +29,9 @@ function setMomentLng(language) {
|
|||||||
moment.locale(lng);
|
moment.locale(lng);
|
||||||
}
|
}
|
||||||
|
|
||||||
setMomentLng(localStorage.getItem('NOCOBASE_LANG'));
|
setMomentLng(localStorage.getItem('NOCOBASE_LOCALE'));
|
||||||
|
|
||||||
i18n.on('languageChanged', (lng) => {
|
i18n.on('languageChanged', (lng) => {
|
||||||
localStorage.setItem('NOCOBASE_LANG', lng);
|
localStorage.setItem('NOCOBASE_LOCALE', lng);
|
||||||
setMomentLng(lng);
|
setMomentLng(lng);
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,7 @@ export const CurrentUser = () => {
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await api.resource('users').signout();
|
await api.resource('users').signout();
|
||||||
api.setBearerToken(null);
|
api.auth.setToken(null);
|
||||||
history.push('/signin');
|
history.push('/signin');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -34,6 +34,7 @@ export const LanguageSettings = () => {
|
|||||||
appLang: lang,
|
appLang: lang,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
api.auth.setLocale(lang);
|
||||||
await i18n.changeLanguage(lang);
|
await i18n.changeLanguage(lang);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
|
@ -68,13 +68,8 @@ const useSignin = () => {
|
|||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
await form.submit();
|
await form.submit();
|
||||||
const response = await api.resource('users').signin({
|
await api.auth.signIn(form.values);
|
||||||
values: form.values,
|
|
||||||
});
|
|
||||||
if (response?.data?.data?.token) {
|
|
||||||
api.setBearerToken(response?.data?.data?.token);
|
|
||||||
history.push(redirect || '/admin');
|
history.push(redirect || '/admin');
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useCookieState } from 'ahooks';
|
|
||||||
import { Menu, Select } from 'antd';
|
import { Menu, Select } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -30,9 +29,6 @@ export const SwitchRole = () => {
|
|||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const roles = useCurrentRoles();
|
const roles = useCurrentRoles();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [roleName, setRoleName] = useCookieState('currentRoleName', {
|
|
||||||
defaultValue: roles?.find((role) => role.default)?.name,
|
|
||||||
});
|
|
||||||
if (roles.length <= 1) {
|
if (roles.length <= 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -47,9 +43,9 @@ export const SwitchRole = () => {
|
|||||||
value: 'name',
|
value: 'name',
|
||||||
}}
|
}}
|
||||||
options={roles}
|
options={roles}
|
||||||
value={roleName}
|
value={api.auth.role}
|
||||||
onChange={async (roleName) => {
|
onChange={async (roleName) => {
|
||||||
setRoleName(roleName);
|
api.auth.setRole(roleName);
|
||||||
await api.resource('users').setDefaultRole({ values: { roleName } });
|
await api.resource('users').setDefaultRole({ values: { roleName } });
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}}
|
}}
|
||||||
|
@ -46,11 +46,14 @@ function getUmiConfig() {
|
|||||||
function resolveNocobasePackagesAlias(config) {
|
function resolveNocobasePackagesAlias(config) {
|
||||||
const clientSrc = resolve(process.cwd(), './packages/core/client/src');
|
const clientSrc = resolve(process.cwd(), './packages/core/client/src');
|
||||||
const utilsSrc = resolve(process.cwd(), './packages/core/utils/src');
|
const utilsSrc = resolve(process.cwd(), './packages/core/utils/src');
|
||||||
|
const sdkSrc = resolve(process.cwd(), './packages/core/sdk/src');
|
||||||
if (existsSync(clientSrc)) {
|
if (existsSync(clientSrc)) {
|
||||||
config.module.rules.get('ts-in-node_modules').include.add(clientSrc);
|
config.module.rules.get('ts-in-node_modules').include.add(clientSrc);
|
||||||
config.resolve.alias.set('@nocobase/client', clientSrc);
|
config.resolve.alias.set('@nocobase/client', clientSrc);
|
||||||
config.module.rules.get('ts-in-node_modules').include.add(utilsSrc);
|
config.module.rules.get('ts-in-node_modules').include.add(utilsSrc);
|
||||||
config.resolve.alias.set('@nocobase/utils', utilsSrc);
|
config.resolve.alias.set('@nocobase/utils', utilsSrc);
|
||||||
|
config.module.rules.get('ts-in-node_modules').include.add(sdkSrc);
|
||||||
|
config.resolve.alias.set('@nocobase/sdk', sdkSrc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"module": "es/index.js",
|
"module": "es/index.js",
|
||||||
"typings": "es/index.d.ts",
|
"typings": "es/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.1"
|
"axios": "^0.26.1",
|
||||||
|
"qs": "^6.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"axios-mock-adapter": "^1.20.0"
|
"axios-mock-adapter": "^1.20.0"
|
||||||
|
@ -22,15 +22,51 @@ export interface IResource {
|
|||||||
[key: string]: (params?: ActionParams) => Promise<any>;
|
[key: string]: (params?: ActionParams) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Auth {
|
export class Auth {
|
||||||
protected api: APIClient;
|
protected api: APIClient;
|
||||||
protected token: string;
|
|
||||||
protected role: string;
|
protected options = {
|
||||||
|
token: null,
|
||||||
|
locale: null,
|
||||||
|
role: null,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(api: APIClient) {
|
constructor(api: APIClient) {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
this.api.axios.interceptors.request.use((config) => {
|
this.locale = this.getLocale();
|
||||||
config.headers['X-Hostname'] = window.location.hostname;
|
this.role = this.getRole();
|
||||||
|
this.token = this.getToken();
|
||||||
|
this.api.axios.interceptors.request.use(this.middleware.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
get locale() {
|
||||||
|
return this.getLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
get role() {
|
||||||
|
return this.getRole();
|
||||||
|
}
|
||||||
|
|
||||||
|
get token() {
|
||||||
|
return this.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
set locale(value) {
|
||||||
|
this.setLocale(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
set role(value) {
|
||||||
|
this.setRole(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
set token(value) {
|
||||||
|
this.setToken(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(config: AxiosRequestConfig) {
|
||||||
|
if (this.locale) {
|
||||||
|
config.headers['X-Locale'] = this.locale;
|
||||||
|
}
|
||||||
if (this.role) {
|
if (this.role) {
|
||||||
config.headers['X-Role'] = this.role;
|
config.headers['X-Role'] = this.role;
|
||||||
}
|
}
|
||||||
@ -38,26 +74,48 @@ class Auth {
|
|||||||
config.headers['Authorization'] = `Bearer ${this.token}`;
|
config.headers['Authorization'] = `Bearer ${this.token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
getLocale() {
|
||||||
|
return this.api.storage.getItem('NOCOBASE_LOCALE');
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocale(locale: string) {
|
||||||
|
this.options.locale = locale;
|
||||||
|
this.api.storage.setItem('NOCOBASE_LOCALE', locale || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken() {
|
||||||
|
return this.api.storage.getItem('NOCOBASE_TOKEN');
|
||||||
}
|
}
|
||||||
|
|
||||||
setToken(token: string) {
|
setToken(token: string) {
|
||||||
this.token = token;
|
this.options.token = token;
|
||||||
|
this.api.storage.setItem('NOCOBASE_TOKEN', token || '');
|
||||||
|
if (!token) {
|
||||||
|
this.setRole(null);
|
||||||
|
this.setLocale(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRole() {
|
||||||
|
return this.api.storage.getItem('NOCOBASE_ROLE');
|
||||||
}
|
}
|
||||||
|
|
||||||
setRole(role: string) {
|
setRole(role: string) {
|
||||||
this.role = role;
|
this.options.role = role;
|
||||||
|
this.api.storage.setItem('NOCOBASE_ROLE', role || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
async signIn(values) {
|
async signIn(values): Promise<AxiosResponse<any>> {
|
||||||
const response = await this.api.request({
|
const response = await this.api.request({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: 'users:signin',
|
url: 'users:signin',
|
||||||
data: values,
|
data: values,
|
||||||
});
|
});
|
||||||
const data = response?.data?.data;
|
const data = response?.data?.data;
|
||||||
this.token = data;
|
this.setToken(data?.token);
|
||||||
return data;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async signOut() {
|
async signOut() {
|
||||||
@ -65,25 +123,78 @@ class Auth {
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
url: 'users:signout',
|
url: 'users:signout',
|
||||||
});
|
});
|
||||||
this.token = null;
|
this.setToken(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export abstract class Storage {
|
||||||
|
abstract clear(): void;
|
||||||
|
abstract getItem(key: string): string | null;
|
||||||
|
abstract removeItem(key: string): void;
|
||||||
|
abstract setItem(key: string, value: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MemoryStorage extends Storage {
|
||||||
|
items = new Map();
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.items.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(key: string) {
|
||||||
|
return this.items.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(key: string, value: string) {
|
||||||
|
return this.items.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(key: string) {
|
||||||
|
return this.items.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExtendedOptions {
|
||||||
|
authClass?: any;
|
||||||
|
storageClass?: any;
|
||||||
|
}
|
||||||
|
|
||||||
export class APIClient {
|
export class APIClient {
|
||||||
axios: AxiosInstance;
|
axios: AxiosInstance;
|
||||||
auth: Auth;
|
auth: Auth;
|
||||||
|
storage: Storage;
|
||||||
|
|
||||||
constructor(instance?: AxiosInstance | AxiosRequestConfig) {
|
constructor(instance?: AxiosInstance | (AxiosRequestConfig & ExtendedOptions)) {
|
||||||
if (typeof instance === 'function') {
|
if (typeof instance === 'function') {
|
||||||
this.axios = instance;
|
this.axios = instance;
|
||||||
} else {
|
} else {
|
||||||
this.axios = axios.create(instance);
|
const { authClass, storageClass, ...others } = instance || {};
|
||||||
|
this.axios = axios.create(others);
|
||||||
|
this.initStorage(storageClass);
|
||||||
|
if (authClass) {
|
||||||
|
this.auth = new authClass(this);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (!this.storage) {
|
||||||
|
this.initStorage();
|
||||||
|
}
|
||||||
|
if (!this.auth) {
|
||||||
this.auth = new Auth(this);
|
this.auth = new Auth(this);
|
||||||
this.qsMiddleware();
|
}
|
||||||
|
this.paramsSerializer();
|
||||||
}
|
}
|
||||||
|
|
||||||
qsMiddleware() {
|
private initStorage(storage?: any) {
|
||||||
|
if (storage) {
|
||||||
|
this.storage = new storage(this);
|
||||||
|
} else if (localStorage) {
|
||||||
|
this.storage = localStorage;
|
||||||
|
} else {
|
||||||
|
this.storage = new MemoryStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsSerializer() {
|
||||||
this.axios.interceptors.request.use((config) => {
|
this.axios.interceptors.request.use((config) => {
|
||||||
config.paramsSerializer = (params) => {
|
config.paramsSerializer = (params) => {
|
||||||
return qs.stringify(params, {
|
return qs.stringify(params, {
|
||||||
|
116
packages/core/sdk/src/__tests__/api-client.test.ts
Normal file
116
packages/core/sdk/src/__tests__/api-client.test.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { APIClient, Storage } from '../';
|
||||||
|
import { Auth } from '../APIClient';
|
||||||
|
|
||||||
|
describe('api-client', () => {
|
||||||
|
test('instance', async () => {
|
||||||
|
const api = new APIClient({
|
||||||
|
baseURL: 'https://localhost:8000/api',
|
||||||
|
});
|
||||||
|
const mock = new MockAdapter(api.axios);
|
||||||
|
mock.onGet('users:get').reply(200, {
|
||||||
|
data: { id: 1, name: 'John Smith' },
|
||||||
|
});
|
||||||
|
const response = await api.request({ url: 'users:get' });
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.data).toMatchObject({
|
||||||
|
data: { id: 1, name: 'John Smith' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('signIn', async () => {
|
||||||
|
const api = new APIClient({
|
||||||
|
baseURL: 'https://localhost:8000/api',
|
||||||
|
});
|
||||||
|
const mock = new MockAdapter(api.axios);
|
||||||
|
mock.onPost('users:signin').reply(200, {
|
||||||
|
data: { id: 1, name: 'John Smith', token: '123' },
|
||||||
|
});
|
||||||
|
const response = await api.auth.signIn({});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(api.auth.token).toBe('123');
|
||||||
|
expect(localStorage.getItem('NOCOBASE_TOKEN')).toBe('123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resource action', async () => {
|
||||||
|
const api = new APIClient({
|
||||||
|
baseURL: 'https://localhost:8000/api',
|
||||||
|
});
|
||||||
|
const mock = new MockAdapter(api.axios);
|
||||||
|
mock.onPost('users:test').reply(200, {
|
||||||
|
data: { id: 1, name: 'John Smith', token: '123' },
|
||||||
|
});
|
||||||
|
const response = await api.resource('users').test();
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.data).toMatchObject({
|
||||||
|
data: { id: 1, name: 'John Smith', token: '123' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom storage', async () => {
|
||||||
|
const items = new Map();
|
||||||
|
|
||||||
|
class TestStorage extends Storage {
|
||||||
|
clear() {
|
||||||
|
items.clear();
|
||||||
|
}
|
||||||
|
getItem(key: string) {
|
||||||
|
return items.get(key);
|
||||||
|
}
|
||||||
|
setItem(key: string, value: string) {
|
||||||
|
return items.set(key, value);
|
||||||
|
}
|
||||||
|
removeItem(key: string) {
|
||||||
|
return items.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new APIClient({
|
||||||
|
baseURL: 'https://localhost:8000/api',
|
||||||
|
storageClass: TestStorage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mock = new MockAdapter(api.axios);
|
||||||
|
mock.onPost('users:signin').reply(200, {
|
||||||
|
data: { id: 1, name: 'John Smith', token: '123' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await api.auth.signIn({});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(api.auth.token).toBe('123');
|
||||||
|
expect(items.get('NOCOBASE_TOKEN')).toBe('123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom auth', async () => {
|
||||||
|
class TestAuth extends Auth {
|
||||||
|
async signIn(values: any): Promise<AxiosResponse<any, any>> {
|
||||||
|
const response = await this.api.request({
|
||||||
|
method: 'post',
|
||||||
|
url: 'users:test',
|
||||||
|
data: values,
|
||||||
|
});
|
||||||
|
const data = response?.data?.data;
|
||||||
|
this.setToken(data?.token);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new APIClient({
|
||||||
|
baseURL: 'https://localhost:8000/api',
|
||||||
|
authClass: TestAuth,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(api.auth).toBeInstanceOf(TestAuth);
|
||||||
|
|
||||||
|
const mock = new MockAdapter(api.axios);
|
||||||
|
mock.onPost('users:test').reply(200, {
|
||||||
|
data: { id: 1, name: 'John Smith', token: '123' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await api.auth.signIn({});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(api.auth.token).toBe('123');
|
||||||
|
expect(localStorage.getItem('NOCOBASE_TOKEN')).toBe('123');
|
||||||
|
});
|
||||||
|
});
|
@ -17596,6 +17596,13 @@ q@^1.1.2, q@^1.5.1:
|
|||||||
resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||||
|
|
||||||
|
qs@^6.10.1:
|
||||||
|
version "6.10.3"
|
||||||
|
resolved "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
|
||||||
|
integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
|
||||||
|
dependencies:
|
||||||
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
qs@^6.4.0, qs@^6.5.2, qs@^6.9.4:
|
qs@^6.4.0, qs@^6.5.2, qs@^6.9.4:
|
||||||
version "6.10.1"
|
version "6.10.1"
|
||||||
resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
|
resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
|
||||||
|
Loading…
Reference in New Issue
Block a user