feat(plugin-api-keys): support fetch api via api-keys (#2136)
* feat(plugin-api-keys): support fetch api via api-keys * feat: full support * fix: missing parseToken in auth * feat: add created at column * feat: configure snippet * fix: remove unused code * fix: revert * chore: update deps * feat: improve role * fix: avoid create api key without not exist role * feat: improve select roles * refactor: when no X-Role is found, roles should not be randomly assigned * feat: improve code * feat: improve current role * fix: revert * fix: revert apilicent * fix: revert auth * feat: improve currentRole logic * feat: use resourcer.use instead it * refactor: remove api-keys-auth * fix: type * refactor: move jwt to authManager * refactor: remove unused code * refactor: remove protected * Revert "refactor: remove unused code" This reverts commit 8f81535ab7e9c412bdc4d4bc05abad64ff60ba3f. * feat: remove unused code * feat: improve code * fix: test error * test: update test * test: add test cases * docs: update * chore: update X-Role * fix: token's roleName not work * docs: update usage * fix: i18n Add APi key * docs: update capital * docs: update * feat: clean * Update package.json * Update roles.ts * fix: api key --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
4bf722326c
commit
6cfd586175
1
packages/app/client/src/plugins/api-keys.ts
Normal file
1
packages/app/client/src/plugins/api-keys.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-api-keys/client';
|
@ -1,7 +1,7 @@
|
||||
import Koa from 'koa';
|
||||
import { Cache } from '@nocobase/cache';
|
||||
import { Database } from '@nocobase/database';
|
||||
import { Action } from '@nocobase/resourcer';
|
||||
import { Cache } from '@nocobase/cache';
|
||||
import Koa from 'koa';
|
||||
import lodash from 'lodash';
|
||||
import * as actions from './actions';
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { Context, Next } from '@nocobase/actions';
|
||||
import { Model } from '@nocobase/database';
|
||||
import { Registry } from '@nocobase/utils';
|
||||
import { Auth, AuthExtend } from './auth';
|
||||
import { JwtOptions, JwtService } from './base/jwt-service';
|
||||
|
||||
type Storer = {
|
||||
get: (name: string) => Promise<Model>;
|
||||
@ -10,6 +11,7 @@ type Storer = {
|
||||
type AuthManagerOptions = {
|
||||
authKey: string;
|
||||
default?: string;
|
||||
jwt?: JwtOptions;
|
||||
};
|
||||
|
||||
type AuthConfig = {
|
||||
@ -21,9 +23,11 @@ export class AuthManager {
|
||||
protected authTypes: Registry<AuthConfig> = new Registry();
|
||||
// authenticators collection manager.
|
||||
protected storer: Storer;
|
||||
jwt: JwtService;
|
||||
|
||||
constructor(options: AuthManagerOptions) {
|
||||
this.options = options;
|
||||
this.jwt = new JwtService(options.jwt);
|
||||
}
|
||||
|
||||
setStorer(storer: Storer) {
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { Auth, AuthConfig } from '../auth';
|
||||
import { JwtOptions, JwtService } from './jwt-service';
|
||||
import { Collection, Model } from '@nocobase/database';
|
||||
import { Auth, AuthConfig } from '../auth';
|
||||
import { JwtService } from './jwt-service';
|
||||
|
||||
/**
|
||||
* BaseAuth
|
||||
* @description A base class with jwt provide some common methods.
|
||||
*/
|
||||
export class BaseAuth extends Auth {
|
||||
protected jwt: JwtService;
|
||||
protected userCollection: Collection;
|
||||
|
||||
constructor(
|
||||
@ -15,10 +14,17 @@ export class BaseAuth extends Auth {
|
||||
userCollection: Collection;
|
||||
},
|
||||
) {
|
||||
const { options, userCollection } = config;
|
||||
const { userCollection } = config;
|
||||
super(config);
|
||||
this.userCollection = userCollection;
|
||||
this.jwt = new JwtService(options.jwt as JwtOptions);
|
||||
}
|
||||
|
||||
get userRepository() {
|
||||
return this.userCollection.repository;
|
||||
}
|
||||
|
||||
get jwt(): JwtService {
|
||||
return this.ctx.app.authManager.jwt;
|
||||
}
|
||||
|
||||
set user(user: Model) {
|
||||
@ -35,13 +41,17 @@ export class BaseAuth extends Auth {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const { userId } = await this.jwt.decode(token);
|
||||
const user = await this.userCollection.repository.findOne({
|
||||
const { userId, roleName } = await this.jwt.decode(token);
|
||||
|
||||
if (roleName) {
|
||||
this.ctx.headers['x-role'] = roleName;
|
||||
}
|
||||
|
||||
return await this.userRepository.findOne({
|
||||
filter: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
} catch (err) {
|
||||
this.ctx.logger.error(err);
|
||||
return null;
|
||||
|
@ -1,15 +1,21 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import jwt, { SignOptions } from 'jsonwebtoken';
|
||||
|
||||
export interface JwtOptions {
|
||||
secret: string;
|
||||
expiresIn?: string;
|
||||
}
|
||||
|
||||
export type SignPayload = Parameters<typeof jwt.sign>[0];
|
||||
|
||||
export class JwtService {
|
||||
constructor(protected options: JwtOptions) {
|
||||
const { secret, expiresIn } = options || {};
|
||||
constructor(
|
||||
protected options: JwtOptions = {
|
||||
secret: process.env.APP_KEY,
|
||||
},
|
||||
) {
|
||||
const { secret, expiresIn } = options;
|
||||
this.options = {
|
||||
secret: secret || process.env.APP_KEY,
|
||||
secret: secret,
|
||||
expiresIn: expiresIn || process.env.JWT_EXPIRES_IN || '7d',
|
||||
};
|
||||
}
|
||||
@ -22,8 +28,8 @@ export class JwtService {
|
||||
return this.options.secret;
|
||||
}
|
||||
|
||||
sign(payload: any) {
|
||||
return jwt.sign(payload, this.secret(), { expiresIn: this.expiresIn() });
|
||||
sign(payload: SignPayload, options?: SignOptions) {
|
||||
return jwt.sign(payload, this.secret(), { expiresIn: this.expiresIn(), ...options });
|
||||
}
|
||||
|
||||
decode(token: string): Promise<any> {
|
||||
|
@ -83,7 +83,7 @@ export const Action: ComposedAction = observer(
|
||||
const [formValueChanged, setFormValueChanged] = useState(false);
|
||||
const Designer = useDesigner();
|
||||
const field = useField<any>();
|
||||
const { run } = useAction();
|
||||
const { run, element } = useAction();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const compile = useCompile();
|
||||
const form = useForm();
|
||||
@ -163,6 +163,7 @@ export const Action: ComposedAction = observer(
|
||||
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
|
||||
{!popover && renderButton()}
|
||||
{!popover && <div onClick={(e) => e.stopPropagation()}>{props.children}</div>}
|
||||
{element}
|
||||
</ActionContextProvider>
|
||||
);
|
||||
},
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { toArr } from '@formily/shared';
|
||||
import React, { Fragment, useRef, useState } from 'react';
|
||||
import { useDesignable } from '../../';
|
||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
||||
import { CollectionProvider } from '../../../collection-manager';
|
||||
import { RecordProvider, useRecord } from '../../../record-provider';
|
||||
import { FormProvider } from '../../core';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { useDesignable } from '../../';
|
||||
import { ActionContextProvider, useActionContext } from '../action';
|
||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||
|
@ -55,7 +55,7 @@ describe('role', () => {
|
||||
expect(ctx.state.currentRole).toBe('root');
|
||||
});
|
||||
|
||||
it('should set role with default when x-role does not exist', async () => {
|
||||
it('should throw 401', async () => {
|
||||
ctx.state.currentUser = await db.getRepository('users').findOne({
|
||||
appends: ['roles'],
|
||||
});
|
||||
@ -64,8 +64,11 @@ describe('role', () => {
|
||||
return 'abc';
|
||||
}
|
||||
};
|
||||
const throwFn = jest.fn();
|
||||
ctx.throw = throwFn;
|
||||
await setCurrentRole(ctx, () => {});
|
||||
expect(ctx.state.currentRole).toBe('root');
|
||||
expect(throwFn).lastCalledWith(401, 'User role not found');
|
||||
expect(ctx.state.currentRole).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should set role with anonymous', async () => {
|
||||
|
@ -93,7 +93,7 @@ describe('actions', () => {
|
||||
|
||||
const rolesCheckResponse2 = (await loggedAgent.set('Accept', 'application/json').get('/roles:check')) as any;
|
||||
|
||||
expect(rolesCheckResponse2.status).toEqual(500);
|
||||
expect(rolesCheckResponse2.status).toEqual(401);
|
||||
expect(rolesCheckResponse2.body.errors[0].message).toEqual('User role not found');
|
||||
});
|
||||
|
||||
|
@ -8,9 +8,6 @@ const map2obj = (map: Map<string, string>) => {
|
||||
|
||||
export async function checkAction(ctx, next) {
|
||||
const currentRole = ctx.state.currentRole;
|
||||
if (!currentRole) {
|
||||
throw new Error('User role not found');
|
||||
}
|
||||
|
||||
const roleInstance = await ctx.db.getRepository('roles').findOne({
|
||||
filter: {
|
||||
|
@ -84,6 +84,6 @@ export default {
|
||||
type: 'set',
|
||||
name: 'snippets',
|
||||
defaultValue: ['!ui.*', '!pm', '!pm.*'],
|
||||
},
|
||||
}
|
||||
],
|
||||
} as CollectionOptions;
|
||||
|
@ -1,5 +1,8 @@
|
||||
export async function setCurrentRole(ctx, next) {
|
||||
let currentRole = ctx.get('X-Role');
|
||||
import { Context } from '@nocobase/actions';
|
||||
import { Repository } from '@nocobase/database';
|
||||
|
||||
export async function setCurrentRole(ctx: Context, next) {
|
||||
const currentRole = ctx.get('X-Role');
|
||||
|
||||
if (currentRole === 'anonymous') {
|
||||
ctx.state.currentRole = currentRole;
|
||||
@ -10,22 +13,22 @@ export async function setCurrentRole(ctx, next) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const repository = ctx.db.getRepository('users.roles', ctx.state.currentUser.id);
|
||||
const repository = ctx.db.getRepository('users.roles', ctx.state.currentUser.id) as unknown as Repository;
|
||||
const roles = await repository.find();
|
||||
ctx.state.currentUser.setDataValue('roles', roles);
|
||||
|
||||
if (roles.length == 1) {
|
||||
currentRole = roles[0].name;
|
||||
} else if (roles.length > 1) {
|
||||
const role = roles.find((item) => item.name === currentRole);
|
||||
if (!role) {
|
||||
const defaultRole = roles.find((item) => item?.rolesUsers?.default);
|
||||
currentRole = (defaultRole || roles[0])?.name;
|
||||
}
|
||||
// 1. If the X-Role is set, use the specified role
|
||||
if (currentRole) {
|
||||
ctx.state.currentRole = roles.find((role) => role.name === currentRole)?.name;
|
||||
}
|
||||
// 2. If the X-Role is not set, use the default role
|
||||
else {
|
||||
const defaultRole = roles.find((item) => item?.rolesUsers?.default);
|
||||
ctx.state.currentRole = (defaultRole || roles[0])?.name;
|
||||
}
|
||||
|
||||
if (currentRole) {
|
||||
ctx.state.currentRole = currentRole;
|
||||
if (!ctx.state.currentRole) {
|
||||
return ctx.throw(401, 'User role not found');
|
||||
}
|
||||
|
||||
await next();
|
||||
|
9
packages/plugins/api-keys/README.md
Normal file
9
packages/plugins/api-keys/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# api-keys
|
||||
|
||||
English | [中文](./README.zh-CN.md)
|
||||
|
||||
## 安装激活
|
||||
|
||||
内置插件无需手动安装激活。
|
||||
|
||||
## 使用方法
|
9
packages/plugins/api-keys/README.zh-CN.md
Normal file
9
packages/plugins/api-keys/README.zh-CN.md
Normal file
@ -0,0 +1,9 @@
|
||||
# api-keys
|
||||
|
||||
[English](./README.md) | 中文
|
||||
|
||||
## 安装激活
|
||||
|
||||
内置插件无需手动安装激活。
|
||||
|
||||
## 使用方法
|
4
packages/plugins/api-keys/client.d.ts
vendored
Executable file
4
packages/plugins/api-keys/client.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/client';
|
||||
export { default } from './lib/client';
|
||||
|
30
packages/plugins/api-keys/client.js
Executable file
30
packages/plugins/api-keys/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];
|
||||
}
|
||||
});
|
||||
});
|
1
packages/plugins/api-keys/docs/en-US/changelog.md
Normal file
1
packages/plugins/api-keys/docs/en-US/changelog.md
Normal file
@ -0,0 +1 @@
|
||||
# API keys Changelog
|
9
packages/plugins/api-keys/docs/en-US/index.md
Normal file
9
packages/plugins/api-keys/docs/en-US/index.md
Normal file
@ -0,0 +1,9 @@
|
||||
# API keys
|
||||
|
||||
## Introduction
|
||||
|
||||
This plugin allows you to create and manage APIs. The generated API keys can be used to access all `NocoBase` APIs.
|
||||
|
||||
## Access Control
|
||||
|
||||
When creating an API key, you can bind it to a role, and the role's permissions will be the same as the Key's permissions.
|
14
packages/plugins/api-keys/docs/en-US/tabs.json
Normal file
14
packages/plugins/api-keys/docs/en-US/tabs.json
Normal file
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"title": "Introduction",
|
||||
"path": "index"
|
||||
},
|
||||
{
|
||||
"title": "Usage",
|
||||
"path": "usage"
|
||||
},
|
||||
{
|
||||
"title": "Changelog",
|
||||
"path": "changelog"
|
||||
}
|
||||
]
|
19
packages/plugins/api-keys/docs/en-US/usage.md
Normal file
19
packages/plugins/api-keys/docs/en-US/usage.md
Normal file
@ -0,0 +1,19 @@
|
||||
# API keys Usage
|
||||
|
||||
## Creating an API key
|
||||
|
||||
After enabling the plugin, go to the API keys plugin management page, click `Add API key`, fill in the relevant information, and click Save to create an API key.
|
||||
|
||||
## Using an API key
|
||||
|
||||
Add the `Authorization` field to the request header, with the value of `Bearer ${API_KEY}`, to access all `NocoBase` APIs using the API key.
|
||||
|
||||
Here's an example using cURL:
|
||||
|
||||
```bash
|
||||
curl '{domain}/api/roles:check' -H 'Authorization: Bearer {API key}'
|
||||
```
|
||||
|
||||
## Deleting an API key
|
||||
|
||||
Currently, deleting an API key does not make it invalid. Please keep your API key safe.
|
1
packages/plugins/api-keys/docs/zh-CN/changelog.md
Normal file
1
packages/plugins/api-keys/docs/zh-CN/changelog.md
Normal file
@ -0,0 +1 @@
|
||||
# API keys 更新日志
|
10
packages/plugins/api-keys/docs/zh-CN/index.md
Normal file
10
packages/plugins/api-keys/docs/zh-CN/index.md
Normal file
@ -0,0 +1,10 @@
|
||||
# API keys
|
||||
|
||||
## 简介
|
||||
|
||||
该插件允许你创建和管理 API,生成的 API keys 可以用于访问 `NocoBase` 所有 API。
|
||||
|
||||
|
||||
## 权限控制
|
||||
|
||||
创建 API key 时,可以为该 Key 绑定角色,角色的权限就是 Key 的权限。
|
14
packages/plugins/api-keys/docs/zh-CN/tabs.json
Normal file
14
packages/plugins/api-keys/docs/zh-CN/tabs.json
Normal file
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"title": "介绍",
|
||||
"path": "index"
|
||||
},
|
||||
{
|
||||
"title": "用法",
|
||||
"path": "usage"
|
||||
},
|
||||
{
|
||||
"title": "日志",
|
||||
"path": "changelog"
|
||||
}
|
||||
]
|
19
packages/plugins/api-keys/docs/zh-CN/usage.md
Normal file
19
packages/plugins/api-keys/docs/zh-CN/usage.md
Normal file
@ -0,0 +1,19 @@
|
||||
# API keys 使用方法
|
||||
|
||||
## 创建 API key
|
||||
|
||||
当你启用插件后,前往 API keys 的插件管理页面,点击 `添加 API key` 并填写相关信息,点击 `保存` 即可创建 API key。
|
||||
|
||||
## 使用 API key
|
||||
|
||||
在请求头中添加 `Authorization` 字段,值为 `Bearer ${API_KEY}`,即可使用 API key 访问 `NocoBase` 所有 API。
|
||||
|
||||
cURL 的例子如下
|
||||
|
||||
```bash
|
||||
curl '{domain}/api/roles:check' -H 'Authorization: Bearer {API key}'
|
||||
```
|
||||
|
||||
## 删除 API key
|
||||
|
||||
目前删除 API key 并不能使 Key 失效,请注意保管好你的 API key。
|
24
packages/plugins/api-keys/package.json
Normal file
24
packages/plugins/api-keys/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-api-keys",
|
||||
"displayName": "API keys",
|
||||
"displayName.zh-CN": "API keys",
|
||||
"description": "Allow user use API key to fetch nocobase api",
|
||||
"description.zh-CN": "允许用户使用 api 密钥来访问 nocobase api",
|
||||
"version": "0.10.0-alpha.5",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/actions": "0.10.0-alpha.5",
|
||||
"@nocobase/database": "0.10.0-alpha.5",
|
||||
"@nocobase/resourcer": "0.10.0-alpha.5",
|
||||
"@nocobase/server": "0.10.0-alpha.5",
|
||||
"@nocobase/utils": "0.10.0-alpha.5",
|
||||
"jsonwebtoken": "^8.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/test": "0.10.0-alpha.5",
|
||||
"@types/jsonwebtoken": "^8.5.8"
|
||||
},
|
||||
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
|
||||
}
|
4
packages/plugins/api-keys/server.d.ts
vendored
Executable file
4
packages/plugins/api-keys/server.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/server';
|
||||
export { default } from './lib/server';
|
||||
|
30
packages/plugins/api-keys/server.js
Executable file
30
packages/plugins/api-keys/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];
|
||||
}
|
||||
});
|
||||
});
|
13
packages/plugins/api-keys/src/client/Configuration/index.tsx
Normal file
13
packages/plugins/api-keys/src/client/Configuration/index.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { RecursionField } from '@formily/react';
|
||||
import { CollectionManagerProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { apiKeysCollection } from '../../collections';
|
||||
import { configurationSchema } from './schema';
|
||||
|
||||
export const Configuration = () => {
|
||||
return (
|
||||
<CollectionManagerProvider collections={[apiKeysCollection]}>
|
||||
<RecursionField schema={configurationSchema} />
|
||||
</CollectionManagerProvider>
|
||||
);
|
||||
};
|
262
packages/plugins/api-keys/src/client/Configuration/schema.tsx
Normal file
262
packages/plugins/api-keys/src/client/Configuration/schema.tsx
Normal file
@ -0,0 +1,262 @@
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useActionContext, useBlockRequestContext, useRecord } from '@nocobase/client';
|
||||
import { Alert, Modal, Space, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { generateNTemplate, useTranslation } from '../locale';
|
||||
const { useModal } = Modal;
|
||||
|
||||
const useCreateAction = () => {
|
||||
const form = useForm();
|
||||
const { setVisible } = useActionContext();
|
||||
const { resource, service } = useBlockRequestContext();
|
||||
const { t } = useTranslation();
|
||||
const [modalIns, element] = useModal();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
const response = await resource.create({
|
||||
values: form.values,
|
||||
});
|
||||
|
||||
modalIns.success({
|
||||
title: t('API key created successfully'),
|
||||
onOk: () => {
|
||||
form.reset();
|
||||
setVisible(false);
|
||||
},
|
||||
content: (
|
||||
<Space direction="vertical">
|
||||
<Alert
|
||||
message={t('Make sure to copy your personal access key now as you will not be able to see this again.')}
|
||||
type="warning"
|
||||
></Alert>
|
||||
<Typography.Text copyable>{response.data?.data?.token}</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
service?.refresh();
|
||||
},
|
||||
element,
|
||||
};
|
||||
};
|
||||
|
||||
const useDestroyAction = () => {
|
||||
const record = useRecord();
|
||||
const { resource, service } = useBlockRequestContext();
|
||||
return {
|
||||
async run() {
|
||||
await resource.destroy({
|
||||
filterByTk: record.id,
|
||||
});
|
||||
service.refresh();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const configurationSchema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
configuration: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'apiKeys',
|
||||
resource: 'apiKeys',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
appends: ['role'],
|
||||
sort: ['-createdAt'],
|
||||
},
|
||||
rowKey: 'name',
|
||||
showIndex: true,
|
||||
},
|
||||
'x-component': 'CardItem',
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
create: {
|
||||
type: 'void',
|
||||
'x-action': 'create',
|
||||
title: generateNTemplate('Add API key'),
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
type: 'primary',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
title: generateNTemplate('Add API key'),
|
||||
'x-decorator': 'Form',
|
||||
'x-component': 'Action.Modal',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
maxWidth: '520px',
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
title: generateNTemplate('Key name'),
|
||||
required: true,
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
title: generateNTemplate('Role'),
|
||||
required: true,
|
||||
'x-collection-field': 'apiKeys.role',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
expiresIn: {
|
||||
type: 'string',
|
||||
title: generateNTemplate('Expiration'),
|
||||
required: true,
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
default: '30d',
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Modal.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: useCreateAction,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'x-align': 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
[uid()]: {
|
||||
type: 'array',
|
||||
'x-component': 'TableV2',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
useProps: '{{ useTableBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
column1: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-component': 'TableV2.Column',
|
||||
title: generateNTemplate('Key name'),
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column2: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-component': 'TableV2.Column',
|
||||
title: generateNTemplate('Role'),
|
||||
properties: {
|
||||
role: {
|
||||
type: 'object',
|
||||
'x-collection-field': 'apiKeys.role',
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': {
|
||||
enableLink: false,
|
||||
},
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column3: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-component': 'TableV2.Column',
|
||||
title: generateNTemplate('Expiration'),
|
||||
properties: {
|
||||
expiresIn: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column4: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-component': 'TableV2.Column',
|
||||
title: generateNTemplate('Created at'),
|
||||
properties: {
|
||||
createdAt: {
|
||||
type: 'date',
|
||||
// 'x-component': 'CollectionField',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
actionColumn: {
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
properties: {
|
||||
columnActions: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
properties: {
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
confirm: {
|
||||
title: generateNTemplate('Delete API key'),
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
useAction: useDestroyAction,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
29
packages/plugins/api-keys/src/client/index.tsx
Normal file
29
packages/plugins/api-keys/src/client/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { SchemaComponentOptions, SettingsCenterProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Configuration } from './Configuration';
|
||||
import { useTranslation } from './locale';
|
||||
|
||||
const ApiKeysProvider = React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<SettingsCenterProvider
|
||||
settings={{
|
||||
['api-keys']: {
|
||||
title: t('API keys'),
|
||||
icon: 'EnvironmentOutlined',
|
||||
tabs: {
|
||||
configuration: {
|
||||
title: t('Keys manager'),
|
||||
component: Configuration,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SchemaComponentOptions components={{}}>{props.children}</SchemaComponentOptions>
|
||||
</SettingsCenterProvider>
|
||||
);
|
||||
});
|
||||
ApiKeysProvider.displayName = 'ApiKeysProvider';
|
||||
|
||||
export default ApiKeysProvider;
|
3
packages/plugins/api-keys/src/client/locale/en-US.ts
Normal file
3
packages/plugins/api-keys/src/client/locale/en-US.ts
Normal file
@ -0,0 +1,3 @@
|
||||
const locale = {};
|
||||
|
||||
export default locale;
|
17
packages/plugins/api-keys/src/client/locale/index.ts
Normal file
17
packages/plugins/api-keys/src/client/locale/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { i18n } from '@nocobase/client';
|
||||
import { useTranslation as useT } from 'react-i18next';
|
||||
import { NAMESPACE } from '../../constants';
|
||||
|
||||
export function lang(key: string) {
|
||||
return i18n.t(key, { ns: NAMESPACE });
|
||||
}
|
||||
|
||||
export function generateNTemplate(key: string) {
|
||||
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
|
||||
}
|
||||
|
||||
export function useTranslation() {
|
||||
return useT([NAMESPACE, 'client'], {
|
||||
nsMode: 'fallback',
|
||||
});
|
||||
}
|
14
packages/plugins/api-keys/src/client/locale/zh-CN.ts
Normal file
14
packages/plugins/api-keys/src/client/locale/zh-CN.ts
Normal file
@ -0,0 +1,14 @@
|
||||
const locale = {
|
||||
'API key created successfully': 'API key 创建成功',
|
||||
'Make sure to copy your personal access key now as you will not be able to see this again.':
|
||||
'请确保现在复制你的个人访问密钥,因为你将无法再次看到这个密钥。',
|
||||
'Key name': '密钥名称',
|
||||
Expiration: '过期时间',
|
||||
'Delete API key': '删除 API key',
|
||||
Role: '角色',
|
||||
'Keys manager': '密钥管理',
|
||||
'Created at': '创建时间',
|
||||
'Add API key': '添加 API key',
|
||||
};
|
||||
|
||||
export default locale;
|
91
packages/plugins/api-keys/src/collections/api-keys.ts
Normal file
91
packages/plugins/api-keys/src/collections/api-keys.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import type { CollectionOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
namespace: 'api-keys',
|
||||
duplicator: 'optional',
|
||||
name: 'apiKeys',
|
||||
title: '{{t("API keys")}}',
|
||||
sortable: 'sort',
|
||||
model: 'ApiKeyModel',
|
||||
createdBy: true,
|
||||
updatedAt: false,
|
||||
updatedBy: false,
|
||||
logging: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'bigInt',
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
interface: 'id',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: '{{t("name")}}',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'obo',
|
||||
type: 'belongsTo',
|
||||
name: 'role',
|
||||
target: 'roles',
|
||||
foreignKey: 'roleName',
|
||||
uiSchema: {
|
||||
type: 'object',
|
||||
title: '{{t("Roles")}}',
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
},
|
||||
service: {
|
||||
params: {
|
||||
filter: {
|
||||
$and: [
|
||||
{
|
||||
users: { id: { $eq: '{{$user.id}}' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'expiresIn',
|
||||
type: 'string',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: '{{t("Expires")}}',
|
||||
'x-component': 'Select',
|
||||
enum: [
|
||||
{
|
||||
label: '{{t("1 day")}}',
|
||||
value: '1d',
|
||||
},
|
||||
{
|
||||
label: '{{t("7 days")}}',
|
||||
value: '7d',
|
||||
},
|
||||
{
|
||||
label: '{{t("30 days")}}',
|
||||
value: '30d',
|
||||
},
|
||||
{
|
||||
label: '{{t("90 days")}}',
|
||||
value: '90d',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as CollectionOptions;
|
1
packages/plugins/api-keys/src/collections/index.ts
Normal file
1
packages/plugins/api-keys/src/collections/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as apiKeysCollection } from './api-keys';
|
1
packages/plugins/api-keys/src/constants.ts
Normal file
1
packages/plugins/api-keys/src/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const NAMESPACE = 'api-keys';
|
1
packages/plugins/api-keys/src/index.ts
Normal file
1
packages/plugins/api-keys/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './server';
|
171
packages/plugins/api-keys/src/server/__tests__/actions.test.ts
Normal file
171
packages/plugins/api-keys/src/server/__tests__/actions.test.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import Database, { Repository } from '@nocobase/database';
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
|
||||
describe('actions', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
let repo: Repository;
|
||||
let agent;
|
||||
let resource;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer({
|
||||
registerActions: true,
|
||||
acl: true,
|
||||
plugins: ['users', 'auth', 'api-keys', 'acl'],
|
||||
});
|
||||
|
||||
// app.plugin(ApiKeysPlugin);
|
||||
await app.loadAndInstall({ clean: true });
|
||||
db = app.db;
|
||||
repo = db.getRepository('apiKeys');
|
||||
agent = app.agent();
|
||||
resource = agent.set('X-Role', 'admin').resource('apiKeys');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await repo.destroy({
|
||||
truncate: true,
|
||||
});
|
||||
await db.close();
|
||||
});
|
||||
|
||||
let user;
|
||||
let testUser;
|
||||
let role;
|
||||
let testRole;
|
||||
let createData;
|
||||
const expiresIn = 60 * 60 * 24;
|
||||
|
||||
beforeEach(async () => {
|
||||
const userRepo = await db.getRepository('users');
|
||||
user = await userRepo.findOne({
|
||||
appends: ['roles'],
|
||||
});
|
||||
testUser = await userRepo.create({
|
||||
values: {
|
||||
nickname: 'test',
|
||||
roles: user.roles,
|
||||
},
|
||||
});
|
||||
const roleRepo = await db.getRepository('roles');
|
||||
testRole = await roleRepo.create({
|
||||
values: {
|
||||
name: 'TEST_ROLE',
|
||||
},
|
||||
});
|
||||
|
||||
role = await (db.getRepository('users.roles', user.id) as unknown as Repository).findOne({
|
||||
where: {
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
createData = {
|
||||
values: {
|
||||
name: 'TEST',
|
||||
role,
|
||||
expiresIn,
|
||||
},
|
||||
};
|
||||
await agent.login(user);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
let result;
|
||||
let tokenData;
|
||||
|
||||
beforeEach(async () => {
|
||||
result = (await resource.create(createData)).body.data;
|
||||
tokenData = await app.authManager.jwt.decode(result.token);
|
||||
});
|
||||
|
||||
it('basic', async () => {
|
||||
expect(result).toHaveProperty('token');
|
||||
});
|
||||
|
||||
it('the role that does not belong to you should throw error', async () => {
|
||||
const res = await resource.create({
|
||||
values: {
|
||||
...createData,
|
||||
role: testRole,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.text).toBe('Role not found');
|
||||
});
|
||||
|
||||
it('token should work', async () => {
|
||||
const checkRes = await agent.set('Authorization', `Bearer ${result.token}`).resource('auth').check();
|
||||
expect(checkRes.body.data.nickname).toBe(user.nickname);
|
||||
});
|
||||
|
||||
it('token expiresIn correctly', async () => {
|
||||
expect(tokenData.exp - tokenData.iat).toBe(expiresIn);
|
||||
});
|
||||
|
||||
it('token roleName correctly', async () => {
|
||||
expect(tokenData.roleName).toBe(role.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
beforeEach(async () => {
|
||||
await resource.create(createData);
|
||||
});
|
||||
|
||||
it('basic', async () => {
|
||||
const res = await resource.list();
|
||||
expect(res.body.data.length).toBe(1);
|
||||
const data = res.body.data[0];
|
||||
expect(data.name).toContain(createData.values.name);
|
||||
expect(data.roleName).toContain(createData.values.role.name);
|
||||
});
|
||||
|
||||
it("Only show current user's API keys", async () => {
|
||||
expect((await resource.list()).body.data.length).toBe(1);
|
||||
await agent.login(testUser);
|
||||
expect((await resource.list()).body.data.length).toBe(0);
|
||||
const values = {
|
||||
name: 'TEST_USER_KEY',
|
||||
expiresIn: 180 * 24 * 60 * 60,
|
||||
role,
|
||||
};
|
||||
await resource.create({
|
||||
values,
|
||||
});
|
||||
const listData = (await resource.list()).body.data;
|
||||
expect(listData.length).toBe(1);
|
||||
expect(listData[0].name).toBe(values.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
let result;
|
||||
|
||||
beforeEach(async () => {
|
||||
result = (await resource.create(createData)).body.data;
|
||||
});
|
||||
|
||||
it('basic', async () => {
|
||||
const res = await resource.list();
|
||||
expect(res.body.data.length).toBe(1);
|
||||
const data = res.body.data[0];
|
||||
await resource.destroy({
|
||||
id: data.id,
|
||||
});
|
||||
expect((await resource.list()).body.data.length).toBe(0);
|
||||
});
|
||||
|
||||
it("Cannot delete other user's API keys", async () => {
|
||||
const res = await resource.list();
|
||||
expect(res.body.data.length).toBe(1);
|
||||
const data = res.body.data[0];
|
||||
await agent.login(testUser);
|
||||
await resource.destroy({
|
||||
id: data.id,
|
||||
});
|
||||
await agent.login(user);
|
||||
expect((await resource.list()).body.data.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
32
packages/plugins/api-keys/src/server/actions/api-keys.ts
Normal file
32
packages/plugins/api-keys/src/server/actions/api-keys.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import actions, { Context, Next } from '@nocobase/actions';
|
||||
import { Repository } from '@nocobase/database';
|
||||
|
||||
export async function create(ctx: Context, next: Next) {
|
||||
const { values } = ctx.action.params;
|
||||
|
||||
if (!values.role) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repository = ctx.db.getRepository('users.roles', ctx.auth.user.id) as unknown as Repository;
|
||||
const role = await repository.findOne({
|
||||
filter: {
|
||||
name: values.role.name,
|
||||
},
|
||||
});
|
||||
if (!role) {
|
||||
throw ctx.throw(400, ctx.t('Role not found'));
|
||||
}
|
||||
|
||||
return actions.create(ctx, async () => {
|
||||
const token = ctx.app.authManager.jwt.sign(
|
||||
{ userId: ctx.auth.user.id, roleName: role.name },
|
||||
{ expiresIn: values.expiresIn },
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
token,
|
||||
};
|
||||
await next();
|
||||
});
|
||||
}
|
1
packages/plugins/api-keys/src/server/index.ts
Normal file
1
packages/plugins/api-keys/src/server/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './plugin';
|
1
packages/plugins/api-keys/src/server/locale/en-US.ts
Normal file
1
packages/plugins/api-keys/src/server/locale/en-US.ts
Normal file
@ -0,0 +1 @@
|
||||
export default {};
|
2
packages/plugins/api-keys/src/server/locale/index.ts
Normal file
2
packages/plugins/api-keys/src/server/locale/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as enUS } from './en-US';
|
||||
export { default as zhCN } from './zh-CN';
|
3
packages/plugins/api-keys/src/server/locale/zh-CN.ts
Normal file
3
packages/plugins/api-keys/src/server/locale/zh-CN.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default {
|
||||
'Role not found': '角色不存在',
|
||||
};
|
52
packages/plugins/api-keys/src/server/plugin.ts
Normal file
52
packages/plugins/api-keys/src/server/plugin.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { resolve } from 'path';
|
||||
import { NAMESPACE } from '../constants';
|
||||
import { create } from './actions/api-keys';
|
||||
import { enUS, zhCN } from './locale';
|
||||
|
||||
export interface ApiKeysPluginConfig {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export default class ApiKeysPlugin extends Plugin<ApiKeysPluginConfig> {
|
||||
resourceName = 'apiKeys';
|
||||
constructor(app, options) {
|
||||
super(app, options);
|
||||
}
|
||||
|
||||
async beforeLoad() {
|
||||
this.app.i18n.addResources('zh-CN', NAMESPACE, zhCN);
|
||||
this.app.i18n.addResources('en-US', NAMESPACE, enUS);
|
||||
|
||||
await this.app.resourcer.define({
|
||||
name: this.resourceName,
|
||||
actions: {
|
||||
create,
|
||||
},
|
||||
only: ['list', 'create', 'destroy'],
|
||||
});
|
||||
|
||||
this.app.acl.registerSnippet({
|
||||
name: ['pm', this.name, 'configuration'].join('.'),
|
||||
actions: ['apiKeys:list', 'apiKeys:create', 'apiKeys:destroy'],
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
await this.db.import({
|
||||
directory: resolve(__dirname, '../collections'),
|
||||
});
|
||||
|
||||
this.app.resourcer.use(async (ctx, next) => {
|
||||
const { resourceName, actionName } = ctx.action.params;
|
||||
if (resourceName == this.resourceName && ['list', 'destroy'].includes(actionName)) {
|
||||
ctx.action.mergeParams({
|
||||
filter: {
|
||||
createdById: ctx.auth.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
await next();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { AuthConfig, BaseAuth } from '@nocobase/auth';
|
||||
import { namespace } from '../preset';
|
||||
import { PasswordField } from '@nocobase/database';
|
||||
import crypto from 'crypto';
|
||||
import { namespace } from '../preset';
|
||||
|
||||
export class BasicAuth extends BaseAuth {
|
||||
constructor(config: AuthConfig) {
|
||||
@ -16,7 +16,7 @@ export class BasicAuth extends BaseAuth {
|
||||
if (!values[uniqueField]) {
|
||||
ctx.throw(400, ctx.t('Please fill in your email address', { ns: namespace }));
|
||||
}
|
||||
const user = await this.userCollection.repository.findOne({
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
[uniqueField]: values[uniqueField],
|
||||
},
|
||||
@ -54,7 +54,7 @@ export class BasicAuth extends BaseAuth {
|
||||
if (!email) {
|
||||
ctx.throw(400, ctx.t('Please fill in your email address', { ns: namespace }));
|
||||
}
|
||||
const user = await this.userCollection.repository.findOne({
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
@ -72,7 +72,7 @@ export class BasicAuth extends BaseAuth {
|
||||
const {
|
||||
values: { email, password, resetToken },
|
||||
} = ctx.action.params;
|
||||
const user = await this.userCollection.repository.findOne({
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
email,
|
||||
resetToken,
|
||||
@ -91,7 +91,7 @@ export class BasicAuth extends BaseAuth {
|
||||
async getUserByResetToken() {
|
||||
const ctx = this.ctx;
|
||||
const { token } = ctx.action.params;
|
||||
const user = await this.userCollection.repository.findOne({
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
resetToken: token,
|
||||
},
|
||||
@ -111,7 +111,7 @@ export class BasicAuth extends BaseAuth {
|
||||
if (!currentUser) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
const user = await this.userCollection.repository.findOne({
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
email: currentUser.email,
|
||||
},
|
||||
|
@ -71,8 +71,7 @@ export class OIDCAuth extends BaseAuth {
|
||||
// When email is provided, use email to find user
|
||||
// If found, associate the user with the current authenticator
|
||||
if (email) {
|
||||
const userRepo = this.userCollection.repository;
|
||||
const user = await userRepo.findOne({
|
||||
const user = await this.userRepository.findOne({
|
||||
filter: { email },
|
||||
});
|
||||
if (user) {
|
||||
|
@ -52,8 +52,7 @@ export class SAMLAuth extends BaseAuth {
|
||||
// When email is provided or nameID is email, use email to find user
|
||||
// If found, associate the user with the current authenticator
|
||||
if (email || nameID.match(/^.+@.+\..+$/)) {
|
||||
const userRepo = this.userCollection.repository;
|
||||
const user = await userRepo.findOne({
|
||||
const user = await this.userRepository.findOne({
|
||||
filter: { email: email || nameID },
|
||||
});
|
||||
if (user) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AuthConfig, BaseAuth } from '@nocobase/auth';
|
||||
import { Model } from '@nocobase/database';
|
||||
import VerificationPlugin from '@nocobase/plugin-verification';
|
||||
import { AuthModel } from '@nocobase/plugin-auth';
|
||||
import VerificationPlugin from '@nocobase/plugin-verification';
|
||||
import { namespace } from '../constants';
|
||||
|
||||
export class SMSAuth extends BaseAuth {
|
||||
@ -26,8 +26,7 @@ export class SMSAuth extends BaseAuth {
|
||||
} = ctx.action.params;
|
||||
try {
|
||||
// History data compatible processing
|
||||
const userRepo = this.userCollection.repository;
|
||||
user = await userRepo.findOne({
|
||||
user = await this.userRepository.findOne({
|
||||
filter: { phone },
|
||||
});
|
||||
if (user) {
|
||||
|
@ -6,6 +6,7 @@
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/plugin-acl": "0.10.0-alpha.5",
|
||||
"@nocobase/plugin-api-keys": "0.10.0-alpha.5",
|
||||
"@nocobase/plugin-audit-logs": "0.10.0-alpha.5",
|
||||
"@nocobase/plugin-auth": "0.10.0-alpha.5",
|
||||
"@nocobase/plugin-charts": "0.10.0-alpha.5",
|
||||
|
@ -38,6 +38,7 @@ export class PresetNocoBase extends Plugin {
|
||||
'snapshot-field',
|
||||
'graph-collection-manager',
|
||||
'mobile-client',
|
||||
'api-keys',
|
||||
];
|
||||
|
||||
splitNames(name: string) {
|
||||
|
Loading…
Reference in New Issue
Block a user