@nocobase/plugin-api-keys 0.10.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +9 -0
  2. package/README.zh-CN.md +9 -0
  3. package/client.d.ts +4 -0
  4. package/client.js +30 -0
  5. package/docs/en-US/changelog.md +1 -0
  6. package/docs/en-US/index.md +9 -0
  7. package/docs/en-US/tabs.json +14 -0
  8. package/docs/en-US/usage.md +19 -0
  9. package/docs/zh-CN/changelog.md +1 -0
  10. package/docs/zh-CN/index.md +10 -0
  11. package/docs/zh-CN/tabs.json +14 -0
  12. package/docs/zh-CN/usage.md +19 -0
  13. package/lib/client/Configuration/ExpiresSelect.d.ts +3 -0
  14. package/lib/client/Configuration/ExpiresSelect.js +131 -0
  15. package/lib/client/Configuration/index.d.ts +2 -0
  16. package/lib/client/Configuration/index.js +47 -0
  17. package/lib/client/Configuration/roles.d.ts +3 -0
  18. package/lib/client/Configuration/roles.js +20 -0
  19. package/lib/client/Configuration/schema.d.ts +2 -0
  20. package/lib/client/Configuration/schema.js +319 -0
  21. package/lib/client/index.d.ts +3 -0
  22. package/lib/client/index.js +46 -0
  23. package/lib/client/locale/en-US.d.ts +2 -0
  24. package/lib/client/locale/en-US.js +9 -0
  25. package/lib/client/locale/index.d.ts +2 -0
  26. package/lib/client/locale/index.js +32 -0
  27. package/lib/client/locale/zh-CN.d.ts +19 -0
  28. package/lib/client/locale/zh-CN.js +26 -0
  29. package/lib/collections/api-keys.d.ts +3 -0
  30. package/lib/collections/api-keys.js +87 -0
  31. package/lib/collections/index.d.ts +1 -0
  32. package/lib/collections/index.js +13 -0
  33. package/lib/constants.d.ts +1 -0
  34. package/lib/constants.js +8 -0
  35. package/lib/index.d.ts +1 -0
  36. package/lib/index.js +13 -0
  37. package/lib/locale.d.ts +1 -0
  38. package/lib/locale.js +10 -0
  39. package/lib/server/actions/api-keys.d.ts +3 -0
  40. package/lib/server/actions/api-keys.js +71 -0
  41. package/lib/server/index.d.ts +1 -0
  42. package/lib/server/index.js +13 -0
  43. package/lib/server/locale/en-US.d.ts +2 -0
  44. package/lib/server/locale/en-US.js +8 -0
  45. package/lib/server/locale/index.d.ts +2 -0
  46. package/lib/server/locale/index.js +20 -0
  47. package/lib/server/locale/zh-CN.d.ts +4 -0
  48. package/lib/server/locale/zh-CN.js +10 -0
  49. package/lib/server/plugin.d.ts +10 -0
  50. package/lib/server/plugin.js +77 -0
  51. package/package.json +24 -0
  52. package/server.d.ts +4 -0
  53. package/server.js +30 -0
  54. package/src/client/Configuration/ExpiresSelect.tsx +80 -0
  55. package/src/client/Configuration/index.tsx +18 -0
  56. package/src/client/Configuration/roles.ts +9 -0
  57. package/src/client/Configuration/schema.tsx +264 -0
  58. package/src/client/index.tsx +29 -0
  59. package/src/client/locale/en-US.ts +3 -0
  60. package/src/client/locale/index.ts +13 -0
  61. package/src/client/locale/zh-CN.ts +21 -0
  62. package/src/collections/api-keys.ts +95 -0
  63. package/src/collections/index.ts +1 -0
  64. package/src/constants.ts +1 -0
  65. package/src/index.ts +1 -0
  66. package/src/locale.ts +5 -0
  67. package/src/server/__tests__/actions.test.ts +181 -0
  68. package/src/server/actions/api-keys.ts +49 -0
  69. package/src/server/index.ts +1 -0
  70. package/src/server/locale/en-US.ts +1 -0
  71. package/src/server/locale/index.ts +2 -0
  72. package/src/server/locale/zh-CN.ts +3 -0
  73. package/src/server/plugin.ts +53 -0
@@ -0,0 +1,29 @@
1
+ import { SchemaComponentOptions, SettingsCenterProvider } from '@nocobase/client';
2
+ import React from 'react';
3
+ import { Configuration } from './Configuration';
4
+ import { useTranslation } from './locale';
5
+
6
+ const ApiKeysProvider = React.memo((props) => {
7
+ const { t } = useTranslation();
8
+ return (
9
+ <SettingsCenterProvider
10
+ settings={{
11
+ ['api-keys']: {
12
+ title: t('API keys'),
13
+ icon: 'EnvironmentOutlined',
14
+ tabs: {
15
+ configuration: {
16
+ title: t('Keys manager'),
17
+ component: Configuration,
18
+ },
19
+ },
20
+ },
21
+ }}
22
+ >
23
+ <SchemaComponentOptions components={{}}>{props.children}</SchemaComponentOptions>
24
+ </SettingsCenterProvider>
25
+ );
26
+ });
27
+ ApiKeysProvider.displayName = 'ApiKeysProvider';
28
+
29
+ export default ApiKeysProvider;
@@ -0,0 +1,3 @@
1
+ const locale = {};
2
+
3
+ export default locale;
@@ -0,0 +1,13 @@
1
+ import { i18n } from '@nocobase/client';
2
+ import { useTranslation as useT } from 'react-i18next';
3
+ import { NAMESPACE } from '../../constants';
4
+
5
+ export function lang(key: string) {
6
+ return i18n.t(key, { ns: NAMESPACE });
7
+ }
8
+
9
+ export function useTranslation() {
10
+ return useT([NAMESPACE, 'client'], {
11
+ nsMode: 'fallback',
12
+ });
13
+ }
@@ -0,0 +1,21 @@
1
+ const locale = {
2
+ 'API key created successfully': 'API key 创建成功',
3
+ 'Make sure to copy your personal access key now as you will not be able to see this again.':
4
+ '请确保现在复制你的个人访问密钥,因为你将无法再次看到这个密钥。',
5
+ 'Key name': '密钥名称',
6
+ Expiration: '过期时间',
7
+ 'Delete API key': '删除 API key',
8
+ Role: '角色',
9
+ 'Keys manager': '密钥管理',
10
+ 'Created at': '创建时间',
11
+ 'Add API key': '添加 API key',
12
+ Never: '永不',
13
+ Custom: '自定义',
14
+ 'Never expires': '永不过期',
15
+ '1 Day': '1 天',
16
+ '7 Days': '7 天',
17
+ '30 Days': '30 天',
18
+ '90 Days': '90 天',
19
+ };
20
+
21
+ export default locale;
@@ -0,0 +1,95 @@
1
+ import type { CollectionOptions } from '@nocobase/database';
2
+ import { generateNTemplate } from '../locale';
3
+
4
+ export default {
5
+ namespace: 'api-keys',
6
+ duplicator: 'optional',
7
+ name: 'apiKeys',
8
+ title: '{{t("API keys")}}',
9
+ sortable: 'sort',
10
+ model: 'ApiKeyModel',
11
+ createdBy: true,
12
+ updatedAt: false,
13
+ updatedBy: false,
14
+ logging: true,
15
+ fields: [
16
+ {
17
+ name: 'id',
18
+ type: 'bigInt',
19
+ autoIncrement: true,
20
+ primaryKey: true,
21
+ allowNull: false,
22
+ interface: 'id',
23
+ },
24
+ {
25
+ type: 'string',
26
+ name: 'name',
27
+ interface: 'input',
28
+ uiSchema: {
29
+ type: 'string',
30
+ title: '{{t("name")}}',
31
+ 'x-component': 'Input',
32
+ },
33
+ },
34
+ {
35
+ interface: 'obo',
36
+ type: 'belongsTo',
37
+ name: 'role',
38
+ target: 'roles',
39
+ foreignKey: 'roleName',
40
+ uiSchema: {
41
+ type: 'object',
42
+ title: '{{t("Roles")}}',
43
+ 'x-component': 'Select',
44
+ 'x-component-props': {
45
+ fieldNames: {
46
+ label: 'title',
47
+ value: 'name',
48
+ },
49
+ objectValue: true,
50
+ options: '{{ currentRoles }}',
51
+ },
52
+ },
53
+ },
54
+ {
55
+ name: 'expiresIn',
56
+ type: 'string',
57
+ uiSchema: {
58
+ type: 'string',
59
+ title: generateNTemplate('Expires'),
60
+ 'x-component': 'ExpiresSelect',
61
+ enum: [
62
+ {
63
+ label: generateNTemplate('1 Day'),
64
+ value: '1d',
65
+ },
66
+ {
67
+ label: generateNTemplate('7 Days'),
68
+ value: '7d',
69
+ },
70
+ {
71
+ label: generateNTemplate('30 Days'),
72
+ value: '30d',
73
+ },
74
+ {
75
+ label: generateNTemplate('90 Days'),
76
+ value: '90d',
77
+ },
78
+ {
79
+ label: generateNTemplate('Custom'),
80
+ value: 'custom',
81
+ },
82
+ {
83
+ label: generateNTemplate('Never'),
84
+ value: 'never',
85
+ },
86
+ ],
87
+ },
88
+ },
89
+ {
90
+ name: 'token',
91
+ type: 'string',
92
+ hidden: true,
93
+ },
94
+ ],
95
+ } as CollectionOptions;
@@ -0,0 +1 @@
1
+ export { default as apiKeysCollection } from './api-keys';
@@ -0,0 +1 @@
1
+ export const NAMESPACE = 'api-keys';
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from './server';
package/src/locale.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { NAMESPACE } from './constants';
2
+
3
+ export function generateNTemplate(key: string) {
4
+ return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
5
+ }
@@ -0,0 +1,181 @@
1
+ import Database, { Repository } from '@nocobase/database';
2
+ import { mockServer, MockServer } from '@nocobase/test';
3
+
4
+ describe('actions', () => {
5
+ let app: MockServer;
6
+ let db: Database;
7
+ let repo: Repository;
8
+ let agent;
9
+ let resource;
10
+
11
+ beforeEach(async () => {
12
+ app = mockServer({
13
+ registerActions: true,
14
+ acl: true,
15
+ plugins: ['users', 'auth', 'api-keys', 'acl'],
16
+ });
17
+
18
+ await app.loadAndInstall({ clean: true });
19
+ db = app.db;
20
+ repo = db.getRepository('apiKeys');
21
+ agent = app.agent();
22
+ resource = agent.set('X-Role', 'admin').resource('apiKeys');
23
+ });
24
+
25
+ afterEach(async () => {
26
+ await repo.destroy({
27
+ truncate: true,
28
+ });
29
+ await db.close();
30
+ });
31
+
32
+ let user;
33
+ let testUser;
34
+ let role;
35
+ let testRole;
36
+ let createData;
37
+ const expiresIn = 60 * 60 * 24;
38
+
39
+ beforeEach(async () => {
40
+ const userRepo = await db.getRepository('users');
41
+ user = await userRepo.findOne({
42
+ appends: ['roles'],
43
+ });
44
+ testUser = await userRepo.create({
45
+ values: {
46
+ nickname: 'test',
47
+ roles: user.roles,
48
+ },
49
+ });
50
+ const roleRepo = await db.getRepository('roles');
51
+ testRole = await roleRepo.create({
52
+ values: {
53
+ name: 'TEST_ROLE',
54
+ },
55
+ });
56
+
57
+ role = await (db.getRepository('users.roles', user.id) as unknown as Repository).findOne({
58
+ where: {
59
+ default: true,
60
+ },
61
+ });
62
+ createData = {
63
+ values: {
64
+ name: 'TEST',
65
+ role,
66
+ expiresIn,
67
+ },
68
+ };
69
+ await agent.login(user);
70
+ });
71
+
72
+ describe('create', () => {
73
+ let result;
74
+ let tokenData;
75
+
76
+ beforeEach(async () => {
77
+ result = (await resource.create(createData)).body.data;
78
+ tokenData = await app.authManager.jwt.decode(result.token);
79
+ });
80
+
81
+ it('basic', async () => {
82
+ expect(result).toHaveProperty('token');
83
+ });
84
+
85
+ it('the role that does not belong to you should throw error', async () => {
86
+ const res = await resource.create({
87
+ values: {
88
+ ...createData,
89
+ role: testRole,
90
+ },
91
+ });
92
+ expect(res.status).toBe(400);
93
+ expect(res.text).toBe('Role not found');
94
+ });
95
+
96
+ it('token should work', async () => {
97
+ const checkRes = await agent.set('Authorization', `Bearer ${result.token}`).resource('auth').check();
98
+ expect(checkRes.body.data.nickname).toBe(user.nickname);
99
+ });
100
+
101
+ it('token expiresIn correctly', async () => {
102
+ expect(tokenData.exp - tokenData.iat).toBe(expiresIn);
103
+ });
104
+
105
+ it('token roleName correctly', async () => {
106
+ expect(tokenData.roleName).toBe(role.name);
107
+ });
108
+ });
109
+
110
+ describe('list', () => {
111
+ beforeEach(async () => {
112
+ await resource.create(createData);
113
+ });
114
+
115
+ it('basic', async () => {
116
+ const res = await resource.list();
117
+ expect(res.body.data.length).toBe(1);
118
+ const data = res.body.data[0];
119
+ expect(data.name).toContain(createData.values.name);
120
+ expect(data.roleName).toContain(createData.values.role.name);
121
+ });
122
+
123
+ it("Only show current user's API keys", async () => {
124
+ expect((await resource.list()).body.data.length).toBe(1);
125
+ await agent.login(testUser);
126
+ expect((await resource.list()).body.data.length).toBe(0);
127
+ const values = {
128
+ name: 'TEST_USER_KEY',
129
+ expiresIn: 180 * 24 * 60 * 60,
130
+ role,
131
+ };
132
+ await resource.create({
133
+ values,
134
+ });
135
+ const listData = (await resource.list()).body.data;
136
+ expect(listData.length).toBe(1);
137
+ expect(listData[0].name).toBe(values.name);
138
+ });
139
+ });
140
+
141
+ describe('destroy', () => {
142
+ let result;
143
+
144
+ beforeEach(async () => {
145
+ result = (await resource.create(createData)).body.data;
146
+ });
147
+
148
+ it('basic', async () => {
149
+ const res = await resource.list();
150
+ expect(res.body.data.length).toBe(1);
151
+ const data = res.body.data[0];
152
+ await resource.destroy({
153
+ filterByTk: data.id,
154
+ });
155
+ expect((await resource.list()).body.data.length).toBe(0);
156
+ });
157
+
158
+ it("Cannot delete other user's API keys", async () => {
159
+ const res = await resource.list();
160
+ expect(res.body.data.length).toBe(1);
161
+ const data = res.body.data[0];
162
+ await agent.login(testUser);
163
+ await resource.destroy({
164
+ filterByTk: data.id,
165
+ });
166
+ await agent.login(user);
167
+ expect((await resource.list()).body.data.length).toBe(1);
168
+ });
169
+
170
+ it('The token should not work after removing the api key', async () => {
171
+ const res = await resource.list();
172
+ expect(res.body.data.length).toBe(1);
173
+ const data = res.body.data[0];
174
+ await resource.destroy({
175
+ filterByTk: data.id,
176
+ });
177
+ const response = await agent.set('Authorization', `Bearer ${result.token}`).resource('auth').check();
178
+ expect(response.status).toBe(401);
179
+ });
180
+ });
181
+ });
@@ -0,0 +1,49 @@
1
+ import actions, { Context, Next } from '@nocobase/actions';
2
+ import { Repository } from '@nocobase/database';
3
+
4
+ export async function create(ctx: Context, next: Next) {
5
+ const { values } = ctx.action.params;
6
+
7
+ if (!values.role) {
8
+ return;
9
+ }
10
+
11
+ const repository = ctx.db.getRepository('users.roles', ctx.auth.user.id) as unknown as Repository;
12
+ const role = await repository.findOne({
13
+ filter: {
14
+ name: values.role.name,
15
+ },
16
+ });
17
+ if (!role) {
18
+ throw ctx.throw(400, ctx.t('Role not found'));
19
+ }
20
+
21
+ const token = ctx.app.authManager.jwt.sign(
22
+ { userId: ctx.auth.user.id, roleName: role.name },
23
+ { expiresIn: values.expiresIn },
24
+ );
25
+ ctx.action.mergeParams({
26
+ values: {
27
+ token,
28
+ },
29
+ });
30
+ return actions.create(ctx, async () => {
31
+ ctx.body = {
32
+ token,
33
+ };
34
+ await next();
35
+ });
36
+ }
37
+
38
+ export async function destroy(ctx: Context, next: Next) {
39
+ const repo = ctx.db.getRepository(ctx.action.resourceName);
40
+ const { filterByTk } = ctx.action.params;
41
+
42
+ const data = await repo.findById(filterByTk);
43
+ const token = data?.get('token');
44
+ if (token) {
45
+ await ctx.app.authManager.jwt.block(token);
46
+ }
47
+
48
+ return actions.destroy(ctx, next);
49
+ }
@@ -0,0 +1 @@
1
+ export { default } from './plugin';
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1,2 @@
1
+ export { default as enUS } from './en-US';
2
+ export { default as zhCN } from './zh-CN';
@@ -0,0 +1,3 @@
1
+ export default {
2
+ 'Role not found': '角色不存在',
3
+ };
@@ -0,0 +1,53 @@
1
+ import { Plugin } from '@nocobase/server';
2
+ import { resolve } from 'path';
3
+ import { NAMESPACE } from '../constants';
4
+ import { create, destroy } from './actions/api-keys';
5
+ import { enUS, zhCN } from './locale';
6
+
7
+ export interface ApiKeysPluginConfig {
8
+ name?: string;
9
+ }
10
+
11
+ export default class ApiKeysPlugin extends Plugin<ApiKeysPluginConfig> {
12
+ resourceName = 'apiKeys';
13
+ constructor(app, options) {
14
+ super(app, options);
15
+ }
16
+
17
+ async beforeLoad() {
18
+ this.app.i18n.addResources('zh-CN', NAMESPACE, zhCN);
19
+ this.app.i18n.addResources('en-US', NAMESPACE, enUS);
20
+
21
+ await this.app.resourcer.define({
22
+ name: this.resourceName,
23
+ actions: {
24
+ create,
25
+ destroy,
26
+ },
27
+ only: ['list', 'create', 'destroy'],
28
+ });
29
+
30
+ this.app.acl.registerSnippet({
31
+ name: ['pm', this.name, 'configuration'].join('.'),
32
+ actions: ['apiKeys:list', 'apiKeys:create', 'apiKeys:destroy'],
33
+ });
34
+ }
35
+
36
+ async load() {
37
+ await this.db.import({
38
+ directory: resolve(__dirname, '../collections'),
39
+ });
40
+
41
+ this.app.resourcer.use(async (ctx, next) => {
42
+ const { resourceName, actionName } = ctx.action.params;
43
+ if (resourceName == this.resourceName && ['list', 'destroy'].includes(actionName)) {
44
+ ctx.action.mergeParams({
45
+ filter: {
46
+ createdById: ctx.auth.user.id,
47
+ },
48
+ });
49
+ }
50
+ await next();
51
+ });
52
+ }
53
+ }