@nocobase/plugin-users 0.10.0-alpha.5 → 0.11.0-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 (61) hide show
  1. package/lib/client/index.d.ts +5 -0
  2. package/lib/client/index.js +22 -0
  3. package/lib/index.d.ts +0 -1
  4. package/lib/index.js +1 -4
  5. package/lib/server/index.d.ts +2 -0
  6. package/lib/server/index.js +16 -0
  7. package/package.json +24 -11
  8. package/src/client/index.ts +7 -0
  9. package/src/index.ts +1 -0
  10. package/src/server/__tests__/actions.test.ts +79 -0
  11. package/src/server/__tests__/fields.test.ts +90 -0
  12. package/src/server/__tests__/utils.ts +8 -0
  13. package/src/server/actions/users.ts +148 -0
  14. package/src/server/authenticators/index.ts +26 -0
  15. package/src/server/authenticators/password.ts +34 -0
  16. package/src/server/collections/users.ts +89 -0
  17. package/src/server/index.ts +3 -0
  18. package/src/server/jwt-service.ts +34 -0
  19. package/src/server/locale/en-US.ts +10 -0
  20. package/src/server/locale/es-ES.ts +8 -0
  21. package/src/server/locale/index.ts +3 -0
  22. package/src/server/locale/ja-JP.ts +4 -0
  23. package/src/server/locale/pt-BR.ts +10 -0
  24. package/src/server/locale/zh-CN.ts +8 -0
  25. package/src/server/middlewares/check.ts +11 -0
  26. package/src/server/middlewares/index.ts +2 -0
  27. package/src/server/middlewares/parseToken.ts +41 -0
  28. package/src/server/migrations/20220818072639-add-users-phone.ts +39 -0
  29. package/src/server/server.ts +253 -0
  30. /package/lib/{actions → server/actions}/users.d.ts +0 -0
  31. /package/lib/{actions → server/actions}/users.js +0 -0
  32. /package/lib/{authenticators → server/authenticators}/index.d.ts +0 -0
  33. /package/lib/{authenticators → server/authenticators}/index.js +0 -0
  34. /package/lib/{authenticators → server/authenticators}/password.d.ts +0 -0
  35. /package/lib/{authenticators → server/authenticators}/password.js +0 -0
  36. /package/lib/{collections → server/collections}/users.d.ts +0 -0
  37. /package/lib/{collections → server/collections}/users.js +0 -0
  38. /package/lib/{jwt-service.d.ts → server/jwt-service.d.ts} +0 -0
  39. /package/lib/{jwt-service.js → server/jwt-service.js} +0 -0
  40. /package/lib/{locale → server/locale}/en-US.d.ts +0 -0
  41. /package/lib/{locale → server/locale}/en-US.js +0 -0
  42. /package/lib/{locale → server/locale}/es-ES.d.ts +0 -0
  43. /package/lib/{locale → server/locale}/es-ES.js +0 -0
  44. /package/lib/{locale → server/locale}/index.d.ts +0 -0
  45. /package/lib/{locale → server/locale}/index.js +0 -0
  46. /package/lib/{locale → server/locale}/ja-JP.d.ts +0 -0
  47. /package/lib/{locale → server/locale}/ja-JP.js +0 -0
  48. /package/lib/{locale → server/locale}/pt-BR.d.ts +0 -0
  49. /package/lib/{locale → server/locale}/pt-BR.js +0 -0
  50. /package/lib/{locale → server/locale}/zh-CN.d.ts +0 -0
  51. /package/lib/{locale → server/locale}/zh-CN.js +0 -0
  52. /package/lib/{middlewares → server/middlewares}/check.d.ts +0 -0
  53. /package/lib/{middlewares → server/middlewares}/check.js +0 -0
  54. /package/lib/{middlewares → server/middlewares}/index.d.ts +0 -0
  55. /package/lib/{middlewares → server/middlewares}/index.js +0 -0
  56. /package/lib/{middlewares → server/middlewares}/parseToken.d.ts +0 -0
  57. /package/lib/{middlewares → server/middlewares}/parseToken.js +0 -0
  58. /package/lib/{migrations → server/migrations}/20220818072639-add-users-phone.d.ts +0 -0
  59. /package/lib/{migrations → server/migrations}/20220818072639-add-users-phone.js +0 -0
  60. /package/lib/{server.d.ts → server/server.d.ts} +0 -0
  61. /package/lib/{server.js → server/server.js} +0 -0
@@ -0,0 +1,5 @@
1
+ import { Plugin } from '@nocobase/client';
2
+ declare class UsersPlugin extends Plugin {
3
+ load(): Promise<void>;
4
+ }
5
+ export default UsersPlugin;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ function _client() {
8
+ const data = require("@nocobase/client");
9
+ _client = function _client() {
10
+ return data;
11
+ };
12
+ return data;
13
+ }
14
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
15
+ function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
16
+ class UsersPlugin extends _client().Plugin {
17
+ load() {
18
+ return _asyncToGenerator(function* () {})();
19
+ }
20
+ }
21
+ var _default = UsersPlugin;
22
+ exports.default = _default;
package/lib/index.d.ts CHANGED
@@ -1,2 +1 @@
1
1
  export { default } from './server';
2
- export declare const namespace: any;
package/lib/index.js CHANGED
@@ -9,8 +9,5 @@ Object.defineProperty(exports, "default", {
9
9
  return _server.default;
10
10
  }
11
11
  });
12
- exports.namespace = void 0;
13
12
  var _server = _interopRequireDefault(require("./server"));
14
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
- const namespace = require('../package.json').name;
16
- exports.namespace = namespace;
13
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -0,0 +1,2 @@
1
+ export { default } from './server';
2
+ export declare const namespace: any;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "default", {
7
+ enumerable: true,
8
+ get: function get() {
9
+ return _server.default;
10
+ }
11
+ });
12
+ exports.namespace = void 0;
13
+ var _server = _interopRequireDefault(require("./server"));
14
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
+ const namespace = require('../../package.json').name;
16
+ exports.namespace = namespace;
package/package.json CHANGED
@@ -4,21 +4,34 @@
4
4
  "displayName.zh-CN": "用户管理",
5
5
  "description": "manage the uses of nocobase system",
6
6
  "description.zh-CN": "管理系统用户",
7
- "version": "0.10.0-alpha.5",
7
+ "version": "0.11.0-alpha.1",
8
8
  "license": "AGPL-3.0",
9
- "main": "./lib/index.js",
10
- "types": "./lib/index.d.ts",
9
+ "main": "./lib/server/index.js",
10
+ "files": [
11
+ "lib",
12
+ "src",
13
+ "README.md",
14
+ "README.zh-CN.md",
15
+ "CHANGELOG.md",
16
+ "server.js",
17
+ "server.d.ts",
18
+ "client.js",
19
+ "client.d.ts"
20
+ ],
11
21
  "dependencies": {
12
- "@nocobase/actions": "0.10.0-alpha.5",
13
- "@nocobase/database": "0.10.0-alpha.5",
14
- "@nocobase/resourcer": "0.10.0-alpha.5",
15
- "@nocobase/server": "0.10.0-alpha.5",
16
- "@nocobase/utils": "0.10.0-alpha.5",
22
+ "@types/jsonwebtoken": "^8.5.8",
17
23
  "jsonwebtoken": "^8.5.1"
18
24
  },
19
25
  "devDependencies": {
20
- "@nocobase/test": "0.10.0-alpha.5",
21
- "@types/jsonwebtoken": "^8.5.8"
26
+ "@nocobase/actions": "0.11.0-alpha.1",
27
+ "@nocobase/client": "0.11.0-alpha.1",
28
+ "@nocobase/database": "0.11.0-alpha.1",
29
+ "@nocobase/plugin-acl": "0.11.0-alpha.1",
30
+ "@nocobase/plugin-auth": "0.11.0-alpha.1",
31
+ "@nocobase/resourcer": "0.11.0-alpha.1",
32
+ "@nocobase/server": "0.11.0-alpha.1",
33
+ "@nocobase/test": "0.11.0-alpha.1",
34
+ "@nocobase/utils": "0.11.0-alpha.1"
22
35
  },
23
- "gitHead": "1be8fcdad42688064460761cea22830cf392c7c0"
36
+ "gitHead": "7581b6d3a3a54f09f06a9effb7e3e65328281b2b"
24
37
  }
@@ -0,0 +1,7 @@
1
+ import { Plugin } from '@nocobase/client';
2
+
3
+ class UsersPlugin extends Plugin {
4
+ async load() { }
5
+ }
6
+
7
+ export default UsersPlugin;
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from './server';
@@ -0,0 +1,79 @@
1
+ import Database from '@nocobase/database';
2
+ import AuthPlugin from '@nocobase/plugin-auth';
3
+ import { mockServer, MockServer } from '@nocobase/test';
4
+ import PluginUsers from '..';
5
+ import { userPluginConfig } from './utils';
6
+
7
+ describe('actions', () => {
8
+ let app: MockServer;
9
+ let db: Database;
10
+ let adminUser;
11
+ let agent;
12
+ let adminAgent;
13
+ let pluginUser;
14
+
15
+ beforeEach(async () => {
16
+ app = mockServer();
17
+ await app.cleanDb();
18
+ process.env.INIT_ROOT_EMAIL = 'test@nocobase.com';
19
+ process.env.INIT_ROOT_PASSWORD = '123456';
20
+ process.env.INIT_ROOT_NICKNAME = 'Test';
21
+ app.plugin(PluginUsers, userPluginConfig);
22
+ app.plugin(AuthPlugin);
23
+ await app.loadAndInstall();
24
+ db = app.db;
25
+
26
+ pluginUser = app.getPlugin('users');
27
+ adminUser = await db.getRepository('users').findOne({
28
+ filter: {
29
+ email: process.env.INIT_ROOT_EMAIL,
30
+ },
31
+ });
32
+
33
+ agent = app.agent();
34
+ adminAgent = app.agent().login(adminUser);
35
+ });
36
+
37
+ afterEach(async () => {
38
+ await db.close();
39
+ });
40
+
41
+ // it('should login user with password', async () => {
42
+ // const { INIT_ROOT_EMAIL, INIT_ROOT_PASSWORD } = process.env;
43
+
44
+ // let response = await agent.resource('users').check();
45
+ // expect(response.body.data.id).toBeUndefined();
46
+
47
+ // response = await agent.post('/users:signin').send({
48
+ // email: INIT_ROOT_EMAIL,
49
+ // password: INIT_ROOT_PASSWORD,
50
+ // });
51
+
52
+ // expect(response.statusCode).toEqual(200);
53
+
54
+ // const data = response.body.data;
55
+ // const token = data.token;
56
+ // expect(token).toBeDefined();
57
+
58
+ // response = await agent.get('/users:check').set({ Authorization: 'Bearer ' + token });
59
+ // expect(response.body.data.id).toBeDefined();
60
+ // });
61
+
62
+ it('update profile', async () => {
63
+ const res1 = await agent.resource('users').updateProfile({
64
+ filterByTk: adminUser.id,
65
+ values: {
66
+ nickname: 'a',
67
+ },
68
+ });
69
+ expect(res1.status).toBe(401);
70
+
71
+ const res2 = await adminAgent.resource('users').updateProfile({
72
+ filterByTk: adminUser.id,
73
+ values: {
74
+ nickname: 'a',
75
+ },
76
+ });
77
+ expect(res2.status).toBe(200);
78
+ });
79
+ });
@@ -0,0 +1,90 @@
1
+ import Database from '@nocobase/database';
2
+ import PluginACL from '@nocobase/plugin-acl';
3
+ import UsersPlugin from '@nocobase/plugin-users';
4
+ import { mockServer, MockServer } from '@nocobase/test';
5
+ import { userPluginConfig } from './utils';
6
+ describe('createdBy/updatedBy', () => {
7
+ let api: MockServer;
8
+ let db: Database;
9
+
10
+ beforeEach(async () => {
11
+ api = mockServer();
12
+ api.plugin(UsersPlugin, userPluginConfig);
13
+ api.plugin(PluginACL, { name: 'acl' });
14
+ await api.loadAndInstall({ clean: true });
15
+ db = api.db;
16
+ });
17
+
18
+ afterEach(async () => {
19
+ await db.close();
20
+ });
21
+
22
+ describe('collection definition', () => {
23
+ it('case 1', async () => {
24
+ const Post = db.collection({
25
+ name: 'posts',
26
+ createdBy: true,
27
+ updatedBy: true,
28
+ });
29
+ expect(Post.hasField('createdBy')).toBeTruthy();
30
+ expect(Post.hasField('updatedBy')).toBeTruthy();
31
+ });
32
+
33
+ it('case 2', async () => {
34
+ const Post = db.collection({
35
+ name: 'posts',
36
+ createdBy: true,
37
+ updatedBy: true,
38
+ });
39
+ await db.sync();
40
+ const currentUser = await db.getCollection('users').model.create();
41
+ await Post.repository.create({
42
+ context: {
43
+ state: {
44
+ currentUser,
45
+ },
46
+ },
47
+ });
48
+ const p2 = await Post.repository.findOne({
49
+ appends: ['createdBy', 'updatedBy'],
50
+ });
51
+
52
+ const data = p2.toJSON();
53
+ expect(data.createdBy.id).toBe(currentUser.get('id'));
54
+ expect(data.updatedBy.id).toBe(currentUser.get('id'));
55
+ });
56
+
57
+ it('case 3', async () => {
58
+ const Post = db.collection({
59
+ name: 'posts',
60
+ createdBy: true,
61
+ updatedBy: true,
62
+ });
63
+ await db.sync();
64
+ const user1 = await db.getCollection('users').model.create();
65
+ const user2 = await db.getCollection('users').model.create();
66
+ const p1 = await Post.repository.create({
67
+ context: {
68
+ state: {
69
+ currentUser: user1,
70
+ },
71
+ },
72
+ });
73
+ await Post.repository.update({
74
+ values: {},
75
+ filterByTk: p1.id,
76
+ context: {
77
+ state: {
78
+ currentUser: user2,
79
+ },
80
+ },
81
+ });
82
+ const p2 = await Post.repository.findOne({
83
+ appends: ['createdBy', 'updatedBy'],
84
+ });
85
+ const data = p2.toJSON();
86
+ expect(data.createdBy.id).toBe(user1.get('id'));
87
+ expect(data.updatedBy.id).toBe(user2.get('id'));
88
+ });
89
+ });
90
+ });
@@ -0,0 +1,8 @@
1
+ import { UserPluginConfig } from '..';
2
+
3
+ export const userPluginConfig: UserPluginConfig = {
4
+ name: 'users',
5
+ jwt: {
6
+ secret: '09f26e402586e2faa8da4c98a35f1b20d6b033c60',
7
+ },
8
+ };
@@ -0,0 +1,148 @@
1
+ import { Context, Next } from '@nocobase/actions';
2
+ import { PasswordField } from '@nocobase/database';
3
+ import { branch } from '@nocobase/resourcer';
4
+ import crypto from 'crypto';
5
+ import { namespace } from '../';
6
+
7
+ export async function check(ctx: Context, next: Next) {
8
+ if (ctx.state.currentUser) {
9
+ const user = ctx.state.currentUser.toJSON();
10
+ ctx.body = user;
11
+ } else {
12
+ ctx.body = {};
13
+ }
14
+ await next();
15
+ }
16
+
17
+ export async function signin(ctx: Context, next: Next) {
18
+ const { authenticators, jwtService } = ctx.app.getPlugin('users');
19
+ const branches = {};
20
+ for (const [name, authenticator] of authenticators.getEntities()) {
21
+ branches[name] = authenticator;
22
+ }
23
+
24
+ return branch(branches, (context) => context.action.params.authenticator ?? 'password')(ctx, () => {
25
+ const user = ctx.state.currentUser.toJSON();
26
+ const token = jwtService.sign({ userId: user.id });
27
+ ctx.body = {
28
+ user,
29
+ token,
30
+ };
31
+
32
+ return next();
33
+ });
34
+ }
35
+
36
+ export async function signout(ctx: Context, next: Next) {
37
+ ctx.body = ctx.state.currentUser;
38
+ await next();
39
+ }
40
+
41
+ export async function signup(ctx: Context, next: Next) {
42
+ const User = ctx.db.getRepository('users');
43
+ const { values } = ctx.action.params;
44
+ const user = await User.create({ values });
45
+ ctx.body = user;
46
+ await next();
47
+ }
48
+
49
+ export async function lostpassword(ctx: Context, next: Next) {
50
+ const {
51
+ values: { email },
52
+ } = ctx.action.params;
53
+ if (!email) {
54
+ ctx.throw(400, { code: 'InvalidUserData', message: ctx.t('Please fill in your email address', { ns: namespace }) });
55
+ }
56
+ const User = ctx.db.getCollection('users');
57
+ const user = await User.model.findOne<any>({
58
+ where: {
59
+ email,
60
+ },
61
+ });
62
+ if (!user) {
63
+ ctx.throw(404, {
64
+ code: 'InvalidUserData',
65
+ message: ctx.t('The email is incorrect, please re-enter', { ns: namespace }),
66
+ });
67
+ }
68
+ user.resetToken = crypto.randomBytes(20).toString('hex');
69
+ await user.save();
70
+ ctx.body = user;
71
+ await next();
72
+ }
73
+
74
+ export async function resetpassword(ctx: Context, next: Next) {
75
+ const {
76
+ values: { email, password, resetToken },
77
+ } = ctx.action.params;
78
+ const User = ctx.db.getCollection('users');
79
+ const user = await User.model.findOne<any>({
80
+ where: {
81
+ email,
82
+ resetToken,
83
+ },
84
+ });
85
+ if (!user) {
86
+ ctx.throw(404);
87
+ }
88
+ user.token = null;
89
+ user.resetToken = null;
90
+ user.password = password;
91
+ await user.save();
92
+ ctx.body = user;
93
+ await next();
94
+ }
95
+
96
+ export async function getUserByResetToken(ctx: Context, next: Next) {
97
+ const { token } = ctx.action.params;
98
+ const User = ctx.db.getCollection('users');
99
+ const user = await User.model.findOne({
100
+ where: {
101
+ resetToken: token,
102
+ },
103
+ });
104
+ if (!user) {
105
+ ctx.throw(401);
106
+ }
107
+ ctx.body = user;
108
+ await next();
109
+ }
110
+
111
+ export async function updateProfile(ctx: Context, next: Next) {
112
+ const { values } = ctx.action.params;
113
+ const { currentUser } = ctx.state;
114
+ if (!currentUser) {
115
+ ctx.throw(401);
116
+ }
117
+ const UserRepo = ctx.db.getRepository('users');
118
+ const result = await UserRepo.update({
119
+ filterByTk: currentUser.id,
120
+ values,
121
+ });
122
+ ctx.body = result;
123
+ await next();
124
+ }
125
+
126
+ export async function changePassword(ctx: Context, next: Next) {
127
+ const {
128
+ values: { oldPassword, newPassword },
129
+ } = ctx.action.params;
130
+ if (!ctx.state.currentUser) {
131
+ ctx.throw(401);
132
+ }
133
+ const User = ctx.db.getCollection('users');
134
+ const user = await User.model.findOne<any>({
135
+ where: {
136
+ email: ctx.state.currentUser.email,
137
+ },
138
+ });
139
+ const pwd = User.getField<PasswordField>('password');
140
+ const isValid = await pwd.verify(oldPassword, user.password);
141
+ if (!isValid) {
142
+ ctx.throw(401, ctx.t('The password is incorrect, please re-enter', { ns: namespace }));
143
+ }
144
+ user.password = newPassword;
145
+ user.save();
146
+ ctx.body = ctx.state.currentUser.toJSON();
147
+ await next();
148
+ }
@@ -0,0 +1,26 @@
1
+ import path from 'path';
2
+
3
+ import { requireModule } from '@nocobase/utils';
4
+ import { HandlerType } from '@nocobase/resourcer';
5
+
6
+ import Plugin from '..';
7
+
8
+ interface Authenticators {
9
+ [key: string]: HandlerType;
10
+ }
11
+
12
+ export default function (plugin: Plugin, more: Authenticators = {}) {
13
+ const { authenticators } = plugin;
14
+
15
+ const natives = ['password'].reduce(
16
+ (result, key) =>
17
+ Object.assign(result, {
18
+ [key]: requireModule(path.isAbsolute(key) ? key : path.join(__dirname, key)) as HandlerType,
19
+ }),
20
+ {},
21
+ );
22
+
23
+ for (const [name, authenticator] of Object.entries(<Authenticators>{ ...more, ...natives })) {
24
+ authenticators.register(name, authenticator);
25
+ }
26
+ }
@@ -0,0 +1,34 @@
1
+ import { PasswordField } from '@nocobase/database';
2
+ import { Context, Next } from '@nocobase/actions';
3
+ import { namespace } from '..';
4
+
5
+ export default async function (ctx: Context, next: Next) {
6
+ const { uniqueField = 'email', values } = ctx.action.params;
7
+
8
+ if (!values[uniqueField]) {
9
+ return ctx.throw(400, {
10
+ code: 'InvalidUserData',
11
+ message: ctx.t('Please fill in your email address', { ns: namespace }),
12
+ });
13
+ }
14
+ const User = ctx.db.getCollection('users');
15
+ const user = await User.model.findOne<any>({
16
+ where: {
17
+ [uniqueField]: values[uniqueField],
18
+ },
19
+ });
20
+
21
+ if (!user) {
22
+ return ctx.throw(404, ctx.t('The email is incorrect, please re-enter', { ns: namespace }));
23
+ }
24
+
25
+ const field = User.getField<PasswordField>('password');
26
+ const valid = await field.verify(values.password, user.password);
27
+ if (!valid) {
28
+ return ctx.throw(404, ctx.t('The password is incorrect, please re-enter', { ns: namespace }));
29
+ }
30
+
31
+ ctx.state.currentUser = user;
32
+
33
+ return next();
34
+ }
@@ -0,0 +1,89 @@
1
+ import { CollectionOptions } from '@nocobase/database';
2
+
3
+ export default {
4
+ namespace: 'users.users',
5
+ duplicator: {
6
+ dumpable: 'optional',
7
+ with: 'rolesUsers',
8
+ },
9
+ name: 'users',
10
+ title: '{{t("Users")}}',
11
+ sortable: 'sort',
12
+ model: 'UserModel',
13
+ createdBy: true,
14
+ updatedBy: true,
15
+ logging: true,
16
+ fields: [
17
+ {
18
+ name: 'id',
19
+ type: 'bigInt',
20
+ autoIncrement: true,
21
+ primaryKey: true,
22
+ allowNull: false,
23
+ uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true },
24
+ interface: 'id',
25
+ },
26
+ {
27
+ interface: 'input',
28
+ type: 'string',
29
+ name: 'nickname',
30
+ uiSchema: {
31
+ type: 'string',
32
+ title: '{{t("Nickname")}}',
33
+ 'x-component': 'Input',
34
+ },
35
+ },
36
+ {
37
+ interface: 'email',
38
+ type: 'string',
39
+ name: 'email',
40
+ unique: true,
41
+ uiSchema: {
42
+ type: 'string',
43
+ title: '{{t("Email")}}',
44
+ 'x-component': 'Input',
45
+ 'x-validator': 'email',
46
+ required: true,
47
+ },
48
+ },
49
+ {
50
+ interface: 'phone',
51
+ type: 'string',
52
+ name: 'phone',
53
+ unique: true,
54
+ uiSchema: {
55
+ type: 'string',
56
+ title: '{{t("Phone")}}',
57
+ 'x-component': 'Input',
58
+ 'x-validator': 'phone',
59
+ required: true,
60
+ },
61
+ },
62
+ {
63
+ interface: 'password',
64
+ type: 'password',
65
+ name: 'password',
66
+ hidden: true,
67
+ uiSchema: {
68
+ type: 'string',
69
+ title: '{{t("Password")}}',
70
+ 'x-component': 'Password',
71
+ },
72
+ },
73
+ {
74
+ type: 'string',
75
+ name: 'appLang',
76
+ },
77
+ {
78
+ type: 'string',
79
+ name: 'resetToken',
80
+ unique: true,
81
+ hidden: true,
82
+ },
83
+ {
84
+ type: 'json',
85
+ name: 'systemSettings',
86
+ defaultValue: {},
87
+ },
88
+ ],
89
+ } as CollectionOptions;
@@ -0,0 +1,3 @@
1
+ export { default } from './server';
2
+
3
+ export const namespace = require('../../package.json').name;
@@ -0,0 +1,34 @@
1
+ import jwt from 'jsonwebtoken';
2
+
3
+ export interface JwtOptions {
4
+ secret: string;
5
+ expiresIn?: string;
6
+ }
7
+
8
+ export class JwtService {
9
+ constructor(protected options: JwtOptions) {}
10
+
11
+ private expiresIn() {
12
+ return this.options.expiresIn || process.env.JWT_EXPIRES_IN || '7d';
13
+ }
14
+
15
+ private secret() {
16
+ return this.options.secret || process.env.APP_KEY;
17
+ }
18
+
19
+ sign(payload: any) {
20
+ return jwt.sign(payload, this.secret(), { expiresIn: this.expiresIn() });
21
+ }
22
+
23
+ decode(token: string): Promise<any> {
24
+ return new Promise((resolve, reject) => {
25
+ jwt.verify(token, this.secret(), (err: any, decoded: any) => {
26
+ if (err) {
27
+ return reject(err);
28
+ }
29
+
30
+ resolve(decoded);
31
+ });
32
+ });
33
+ }
34
+ }
@@ -0,0 +1,10 @@
1
+ export default {
2
+ 'The email is incorrect, please re-enter': 'The email is incorrect, please re-enter',
3
+ 'Please fill in your email address': 'Please fill in your email address',
4
+ 'The password is incorrect, please re-enter': 'The password is incorrect, please re-enter',
5
+ 'Not a valid cellphone number, please re-enter': 'Not a valid cellphone number, please re-enter',
6
+ 'The phone number has been registered, please login directly':
7
+ 'The phone number has been registered, please login directly',
8
+ 'The phone number is not registered, please register first':
9
+ 'The phone number is not registered, please register first',
10
+ };
@@ -0,0 +1,8 @@
1
+ export default {
2
+ "The email is incorrect, please re-enter": "El correo electrónico es incorrecto, por favor vuelva a introducirlo",
3
+ "Please fill in your email address": "Por favor, introduzca su dirección de correo electrónico",
4
+ "The password is incorrect, please re-enter": "La contraseña es incorrecta, por favor, vuelva a introducirla",
5
+ "Not a valid cellphone number, please re-enter": "No es un número de móvil válido, por favor, vuelva a introducirlo",
6
+ "The phone number has been registered, please login directly": "El número de teléfono ha sido registrado, por favor, inicie sesión directamente",
7
+ "The phone number is not registered, please register first": "El número de teléfono no está registrado, por favor regístrese primero"
8
+ };
@@ -0,0 +1,3 @@
1
+ export { default as enUS } from './en-US';
2
+ export { default as zhCN } from './zh-CN';
3
+ export { default as ptBR } from './pt-BR';
@@ -0,0 +1,4 @@
1
+ export default {
2
+ 'Please fill in your email address': 'メールアドレスを入力してください',
3
+ 'The password is incorrect, please re-enter': 'パスワードが正しくありません。再度入力してください。',
4
+ };
@@ -0,0 +1,10 @@
1
+ export default {
2
+ 'The email is incorrect, please re-enter': 'O e-mail está incorreto, por favor, digite novamente',
3
+ 'Please fill in your email address': 'Por favor, preencha o seu endereço de e-mail',
4
+ 'The password is incorrect, please re-enter': 'A senha está incorreta, por favor, digite novamente',
5
+ 'Not a valid cellphone number, please re-enter': 'Número de celular inválido, por favor, digite novamente',
6
+ 'The phone number has been registered, please login directly':
7
+ 'O número de celular já está registrado, por favor, faça login diretamente',
8
+ 'The phone number is not registered, please register first':
9
+ 'O número de celular não está registrado, por favor, registre-se primeiro',
10
+ };
@@ -0,0 +1,8 @@
1
+ export default {
2
+ 'The email is incorrect, please re-enter': '邮箱有误,请重新输入',
3
+ 'Please fill in your email address': '请填写邮箱',
4
+ 'The password is incorrect, please re-enter': '密码有误,请重新输入',
5
+ 'Not a valid cellphone number, please re-enter': '不是有效的手机号,请重新输入',
6
+ 'The phone number has been registered, please login directly': '手机号已注册,请直接登录',
7
+ 'The phone number is not registered, please register first': '手机号未注册,请先注册',
8
+ };
@@ -0,0 +1,11 @@
1
+ // TODO(usage): 拦截用户的处理暂时作为一个中间件导出,应用需要的时候可以直接使用这个中间件
2
+ export function check(options) {
3
+ return async function check(ctx, next) {
4
+ const { currentUser } = ctx.state;
5
+
6
+ if (!currentUser) {
7
+ return ctx.throw(401, 'Unauthorized');
8
+ }
9
+ return next();
10
+ };
11
+ }
@@ -0,0 +1,2 @@
1
+ export { check } from './check';
2
+ export { parseToken } from './parseToken';
@@ -0,0 +1,41 @@
1
+ import { Context, Next } from '@nocobase/actions';
2
+
3
+ export async function parseToken(ctx: Context, next: Next) {
4
+ const user = await findUserByToken(ctx);
5
+ if (user) {
6
+ ctx.state.currentUser = user;
7
+ }
8
+ return next();
9
+ }
10
+
11
+ async function findUserByToken(ctx: Context) {
12
+ const token = ctx.getBearerToken();
13
+ if (!token) {
14
+ return null;
15
+ }
16
+ const { jwtService } = ctx.app.getPlugin('users');
17
+ try {
18
+ const { userId } = await jwtService.decode(token);
19
+ const collection = ctx.db.getCollection('users');
20
+ ctx.state.currentUserAppends = ctx.state.currentUserAppends || [];
21
+ for (const [, field] of collection.fields) {
22
+ if (field.type === 'belongsTo') {
23
+ ctx.state.currentUserAppends.push(field.name);
24
+ }
25
+ }
26
+
27
+ const user = await ctx.db.getRepository('users').findOne({
28
+ appends: ctx.state.currentUserAppends,
29
+ filter: {
30
+ id: userId,
31
+ },
32
+ });
33
+
34
+ ctx.logger.info(`Current user id: ${userId}`);
35
+ return user;
36
+ } catch (error) {
37
+ console.log(error);
38
+ ctx.logger.error(error);
39
+ return null;
40
+ }
41
+ }
@@ -0,0 +1,39 @@
1
+ import { Migration } from '@nocobase/server';
2
+
3
+ export default class AddUsersPhoneMigration extends Migration {
4
+ async up() {
5
+ const match = await this.app.version.satisfies('<=0.7.4-alpha.7');
6
+ if (!match) {
7
+ return;
8
+ }
9
+ const Field = this.context.db.getRepository('fields');
10
+ const existed = await Field.count({
11
+ filter: {
12
+ name: 'phone',
13
+ collectionName: 'users',
14
+ },
15
+ });
16
+ if (!existed) {
17
+ await Field.create({
18
+ values: {
19
+ name: 'phone',
20
+ collectionName: 'users',
21
+ type: 'string',
22
+ unique: true,
23
+ interface: 'phone',
24
+ uiSchema: {
25
+ type: 'string',
26
+ title: '{{t("Phone")}}',
27
+ 'x-component': 'Input',
28
+ 'x-validator': 'phone',
29
+ require: true,
30
+ },
31
+ },
32
+ // NOTE: to trigger hook
33
+ context: {},
34
+ });
35
+ }
36
+ }
37
+
38
+ async down() {}
39
+ }
@@ -0,0 +1,253 @@
1
+ import { Collection, Op } from '@nocobase/database';
2
+ import { HandlerType } from '@nocobase/resourcer';
3
+ import { Plugin } from '@nocobase/server';
4
+ import { Registry, parse } from '@nocobase/utils';
5
+ import { resolve } from 'path';
6
+
7
+ import { namespace } from './';
8
+ import * as actions from './actions/users';
9
+ import initAuthenticators from './authenticators';
10
+ import { JwtOptions, JwtService } from './jwt-service';
11
+ import { enUS, zhCN } from './locale';
12
+ import { parseToken } from './middlewares';
13
+
14
+ export interface UserPluginConfig {
15
+ name?: string;
16
+ jwt: JwtOptions;
17
+ }
18
+
19
+ export default class UsersPlugin extends Plugin<UserPluginConfig> {
20
+ public jwtService: JwtService;
21
+
22
+ public authenticators: Registry<HandlerType> = new Registry();
23
+
24
+ constructor(app, options) {
25
+ super(app, options);
26
+ this.jwtService = new JwtService(options?.jwt || {});
27
+ }
28
+
29
+ async beforeLoad() {
30
+ this.app.i18n.addResources('zh-CN', namespace, zhCN);
31
+ this.app.i18n.addResources('en-US', namespace, enUS);
32
+ const cmd = this.app.findCommand('install');
33
+ if (cmd) {
34
+ cmd.requiredOption('-e, --root-email <rootEmail>', '', process.env.INIT_ROOT_EMAIL);
35
+ cmd.requiredOption('-p, --root-password <rootPassword>', '', process.env.INIT_ROOT_PASSWORD);
36
+ cmd.option('-n, --root-nickname <rootNickname>');
37
+ }
38
+ this.db.registerOperators({
39
+ $isCurrentUser(_, ctx) {
40
+ return {
41
+ [Op.eq]: ctx?.app?.ctx?.state?.currentUser?.id || -1,
42
+ };
43
+ },
44
+ $isNotCurrentUser(_, ctx) {
45
+ return {
46
+ [Op.ne]: ctx?.app?.ctx?.state?.currentUser?.id || -1,
47
+ };
48
+ },
49
+ $isVar(val, ctx) {
50
+ const obj = parse({ val: `{{${val}}}` })(JSON.parse(JSON.stringify(ctx?.app?.ctx?.state)));
51
+ return {
52
+ [Op.eq]: obj.val,
53
+ };
54
+ },
55
+ });
56
+
57
+ this.db.on('afterDefineCollection', (collection: Collection) => {
58
+ const { createdBy, updatedBy } = collection.options;
59
+ if (createdBy === true) {
60
+ collection.setField('createdById', {
61
+ type: 'context',
62
+ dataType: 'bigInt',
63
+ dataIndex: 'state.currentUser.id',
64
+ createOnly: true,
65
+ visible: true,
66
+ index: true,
67
+ });
68
+ collection.setField('createdBy', {
69
+ type: 'belongsTo',
70
+ target: 'users',
71
+ foreignKey: 'createdById',
72
+ targetKey: 'id',
73
+ });
74
+ }
75
+ if (updatedBy === true) {
76
+ collection.setField('updatedById', {
77
+ type: 'context',
78
+ dataType: 'bigInt',
79
+ dataIndex: 'state.currentUser.id',
80
+ visible: true,
81
+ index: true,
82
+ });
83
+ collection.setField('updatedBy', {
84
+ type: 'belongsTo',
85
+ target: 'users',
86
+ foreignKey: 'updatedById',
87
+ targetKey: 'id',
88
+ });
89
+ }
90
+ });
91
+
92
+ for (const [key, action] of Object.entries(actions)) {
93
+ this.app.resourcer.registerActionHandler(`users:${key}`, action);
94
+ }
95
+
96
+ // this.app.resourcer.use(parseToken, { tag: 'parseToken' });
97
+
98
+ this.app.acl.addFixedParams('users', 'destroy', () => {
99
+ return {
100
+ filter: {
101
+ 'id.$ne': 1,
102
+ },
103
+ };
104
+ });
105
+
106
+ this.app.acl.addFixedParams('collections', 'destroy', () => {
107
+ return {
108
+ filter: {
109
+ 'name.$ne': 'users',
110
+ },
111
+ };
112
+ });
113
+
114
+ const publicActions = ['check', 'signin', 'signup', 'lostpassword', 'resetpassword', 'getUserByResetToken'];
115
+ const loggedInActions = ['signout', 'updateProfile', 'changePassword'];
116
+
117
+ publicActions.forEach((action) => this.app.acl.allow('users', action));
118
+ loggedInActions.forEach((action) => this.app.acl.allow('users', action, 'loggedIn'));
119
+
120
+ this.app.on('beforeStart', () => this.initVerification());
121
+ }
122
+
123
+ async load() {
124
+ await this.db.import({
125
+ directory: resolve(__dirname, 'collections'),
126
+ });
127
+
128
+ this.db.addMigrations({
129
+ namespace: 'users',
130
+ directory: resolve(__dirname, 'migrations'),
131
+ context: {
132
+ plugin: this,
133
+ },
134
+ });
135
+
136
+ initAuthenticators(this);
137
+ }
138
+
139
+ getInstallingData(options: any = {}) {
140
+ const { INIT_ROOT_NICKNAME, INIT_ROOT_PASSWORD, INIT_ROOT_EMAIL } = process.env;
141
+ const {
142
+ rootEmail = INIT_ROOT_EMAIL,
143
+ rootPassword = INIT_ROOT_PASSWORD,
144
+ rootNickname = INIT_ROOT_NICKNAME || 'Super Admin',
145
+ } = options.users || options?.cliArgs?.[0] || {};
146
+ return {
147
+ rootEmail,
148
+ rootPassword,
149
+ rootNickname,
150
+ };
151
+ }
152
+
153
+ async install(options) {
154
+ const { rootNickname, rootPassword, rootEmail } = this.getInstallingData(options);
155
+ const User = this.db.getCollection('users');
156
+ if (await User.repository.findOne({ filter: { email: rootEmail } })) {
157
+ return;
158
+ }
159
+
160
+ const user = await User.repository.create({
161
+ values: {
162
+ email: rootEmail,
163
+ password: rootPassword,
164
+ nickname: rootNickname,
165
+ },
166
+ });
167
+
168
+ const repo = this.db.getRepository<any>('collections');
169
+ if (repo) {
170
+ await repo.db2cm('users');
171
+ }
172
+ }
173
+
174
+ // TODO(module): should move to preset or dynamic configuration panel
175
+ async initVerification() {
176
+ const verificationPlugin = this.app.getPlugin('verification') as any;
177
+ if (!verificationPlugin) {
178
+ return;
179
+ }
180
+ const systemSettingsRepo = this.db.getRepository('systemSettings');
181
+ const settings = await systemSettingsRepo.findOne();
182
+ if (!settings.smsAuthEnabled) {
183
+ return;
184
+ }
185
+
186
+ verificationPlugin.interceptors.register('users:signin', {
187
+ manual: true,
188
+ getReceiver(ctx) {
189
+ return ctx.action.params.values.phone;
190
+ },
191
+ expiresIn: 120,
192
+ validate: async (ctx, phone) => {
193
+ if (!phone) {
194
+ throw new Error(ctx.t('Not a valid cellphone number, please re-enter'));
195
+ }
196
+ const User = this.db.getCollection('users');
197
+ const exists = await User.model.count({
198
+ where: {
199
+ phone,
200
+ },
201
+ });
202
+ if (!exists) {
203
+ throw new Error(ctx.t('The phone number is not registered, please register first', { ns: namespace }));
204
+ }
205
+
206
+ return true;
207
+ },
208
+ });
209
+
210
+ verificationPlugin.interceptors.register('users:signup', {
211
+ getReceiver(ctx) {
212
+ return ctx.action.params.values.phone;
213
+ },
214
+ expiresIn: 120,
215
+ validate: async (ctx, phone) => {
216
+ if (!phone) {
217
+ throw new Error(ctx.t('Not a valid cellphone number, please re-enter', { ns: namespace }));
218
+ }
219
+ const User = this.db.getCollection('users');
220
+ const exists = await User.model.count({
221
+ where: {
222
+ phone,
223
+ },
224
+ });
225
+ if (exists) {
226
+ throw new Error(ctx.t('The phone number has been registered, please login directly', { ns: namespace }));
227
+ }
228
+
229
+ return true;
230
+ },
231
+ });
232
+
233
+ this.authenticators.register('sms', (ctx, next) =>
234
+ verificationPlugin.intercept(ctx, async () => {
235
+ const { values } = ctx.action.params;
236
+
237
+ const User = ctx.db.getCollection('users');
238
+ const user = await User.model.findOne({
239
+ where: {
240
+ phone: values.phone,
241
+ },
242
+ });
243
+ if (!user) {
244
+ return ctx.throw(404, ctx.t('The phone number is incorrect, please re-enter', { ns: namespace }));
245
+ }
246
+
247
+ ctx.state.currentUser = user;
248
+
249
+ return next();
250
+ }),
251
+ );
252
+ }
253
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes