feat: upgrade plugin-users
This commit is contained in:
		
							parent
							
								
									bf2840b089
								
							
						
					
					
						commit
						f09991d932
					
				@ -36,7 +36,7 @@ export class BelongsToField extends RelationField {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // inverse relation
 | 
					    // inverse relation
 | 
				
			||||||
    this.TargetModel.hasMany(collection.model);
 | 
					    // this.TargetModel.hasMany(collection.model);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 建立关系之后从 pending 列表中删除
 | 
					    // 建立关系之后从 pending 列表中删除
 | 
				
			||||||
    database.removePendingField(this);
 | 
					    database.removePendingField(this);
 | 
				
			||||||
@ -76,4 +76,5 @@ export class BelongsToField extends RelationField {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface BelongsToFieldOptions extends BaseRelationFieldOptions, SequelizeBelongsToOptions {
 | 
					export interface BelongsToFieldOptions extends BaseRelationFieldOptions, SequelizeBelongsToOptions {
 | 
				
			||||||
  type: 'belongsTo';
 | 
					  type: 'belongsTo';
 | 
				
			||||||
 | 
					  target?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -102,7 +102,7 @@ export class HasManyField extends RelationField {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // inverse relation
 | 
					    // inverse relation
 | 
				
			||||||
    this.TargetModel.belongsTo(collection.model);
 | 
					    // this.TargetModel.belongsTo(collection.model);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 建立关系之后从 pending 列表中删除
 | 
					    // 建立关系之后从 pending 列表中删除
 | 
				
			||||||
    database.removePendingField(this);
 | 
					    database.removePendingField(this);
 | 
				
			||||||
 | 
				
			|||||||
@ -283,7 +283,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
 | 
				
			|||||||
   * @param options
 | 
					   * @param options
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  @transaction()
 | 
					  @transaction()
 | 
				
			||||||
  async create(options: CreateOptions): Promise<Model> {
 | 
					  async create<M extends Model>(options: CreateOptions): Promise<M> {
 | 
				
			||||||
    const transaction = await this.getTransaction(options);
 | 
					    const transaction = await this.getTransaction(options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const guard = UpdateGuard.fromOptions(this.model, options);
 | 
					    const guard = UpdateGuard.fromOptions(this.model, options);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								packages/plugin-notifications/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugin-notifications/src/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export { default } from './server';
 | 
				
			||||||
							
								
								
									
										1
									
								
								packages/plugin-system-settings/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugin-system-settings/src/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export { default } from './server';
 | 
				
			||||||
@ -6,7 +6,7 @@ export default {
 | 
				
			|||||||
  async load() {
 | 
					  async load() {
 | 
				
			||||||
    const database = this.app.db;
 | 
					    const database = this.app.db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    database.import({
 | 
					    await database.import({
 | 
				
			||||||
      directory: path.resolve(__dirname, 'collections'),
 | 
					      directory: path.resolve(__dirname, 'collections'),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								packages/plugin-users/.npmignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/plugin-users/.npmignore
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
 | 
					docs
 | 
				
			||||||
 | 
					__tests__
 | 
				
			||||||
 | 
					tsconfig.json
 | 
				
			||||||
 | 
					src
 | 
				
			||||||
 | 
					.fatherrc.ts
 | 
				
			||||||
							
								
								
									
										18
									
								
								packages/plugin-users/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								packages/plugin-users/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "@nocobase/plugin-users",
 | 
				
			||||||
 | 
					  "version": "0.6.0-alpha.0",
 | 
				
			||||||
 | 
					  "main": "lib/index.js",
 | 
				
			||||||
 | 
					  "license": "MIT",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm",
 | 
				
			||||||
 | 
					    "build:cjs": "tsc --project tsconfig.build.json",
 | 
				
			||||||
 | 
					    "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "crypto-random-string": "^3.3.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@nocobase/test": "^0.6.0-alpha.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "gitHead": "e7df1f93c4e23b9a666d99ee7372c02bdaec97c4"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										84
									
								
								packages/plugin-users/src/__tests__/fields.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								packages/plugin-users/src/__tests__/fields.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					import Database from '@nocobase/database';
 | 
				
			||||||
 | 
					import { mockServer, MockServer } from '@nocobase/test';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('createdBy/updatedBy', () => {
 | 
				
			||||||
 | 
					  let api: MockServer;
 | 
				
			||||||
 | 
					  let db: Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    api = mockServer();
 | 
				
			||||||
 | 
					    api.plugin(require('../server').default);
 | 
				
			||||||
 | 
					    await api.load();
 | 
				
			||||||
 | 
					    db = api.db;
 | 
				
			||||||
 | 
					    await db.sync();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(async () => {
 | 
				
			||||||
 | 
					    await db.close();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('collection definition', () => {
 | 
				
			||||||
 | 
					    it('case 1', async () => {
 | 
				
			||||||
 | 
					      const Post = db.collection({
 | 
				
			||||||
 | 
					        name: 'posts',
 | 
				
			||||||
 | 
					        createdBy: true,
 | 
				
			||||||
 | 
					        updatedBy: true,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(Post.hasField('createdBy')).toBeTruthy();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('case 2', async () => {
 | 
				
			||||||
 | 
					      const Post = db.collection({
 | 
				
			||||||
 | 
					        name: 'posts',
 | 
				
			||||||
 | 
					        createdBy: true,
 | 
				
			||||||
 | 
					        updatedBy: true,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      await db.sync();
 | 
				
			||||||
 | 
					      const currentUser = await db.getCollection('users').model.create();
 | 
				
			||||||
 | 
					      const p1 = await Post.repository.create({
 | 
				
			||||||
 | 
					        context: {
 | 
				
			||||||
 | 
					          state: {
 | 
				
			||||||
 | 
					            currentUser,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      const p2 = await Post.repository.findOne({
 | 
				
			||||||
 | 
					        appends: ['createdBy', 'updatedBy'],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(p2.get('createdBy')).toMatchObject(currentUser.toJSON());
 | 
				
			||||||
 | 
					      expect(p2.get('updatedBy')).toMatchObject(currentUser.toJSON());
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('case 3', async () => {
 | 
				
			||||||
 | 
					      const Post = db.collection({
 | 
				
			||||||
 | 
					        name: 'posts',
 | 
				
			||||||
 | 
					        createdBy: true,
 | 
				
			||||||
 | 
					        updatedBy: true,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      await db.sync();
 | 
				
			||||||
 | 
					      const user1 = await db.getCollection('users').model.create();
 | 
				
			||||||
 | 
					      const user2 = await db.getCollection('users').model.create();
 | 
				
			||||||
 | 
					      const p1 = await Post.repository.create<any>({
 | 
				
			||||||
 | 
					        context: {
 | 
				
			||||||
 | 
					          state: {
 | 
				
			||||||
 | 
					            currentUser: user1,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      await Post.repository.update({
 | 
				
			||||||
 | 
					        values: {},
 | 
				
			||||||
 | 
					        filterByPk: p1.id,
 | 
				
			||||||
 | 
					        context: {
 | 
				
			||||||
 | 
					          state: {
 | 
				
			||||||
 | 
					            currentUser: user2,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      const p2 = await Post.repository.findOne({
 | 
				
			||||||
 | 
					        appends: ['createdBy', 'updatedBy'],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(p2.get('createdBy')).toMatchObject(user1.toJSON());
 | 
				
			||||||
 | 
					      expect(p2.get('updatedBy')).toMatchObject(user2.toJSON());
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										156
									
								
								packages/plugin-users/src/actions/users.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								packages/plugin-users/src/actions/users.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,156 @@
 | 
				
			|||||||
 | 
					import { Context, Next } from '@nocobase/actions';
 | 
				
			||||||
 | 
					import { PasswordField } from '@nocobase/database';
 | 
				
			||||||
 | 
					import cryptoRandomString from 'crypto-random-string';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function check(ctx: Context, next: Next) {
 | 
				
			||||||
 | 
					  if (ctx.state.currentUser) {
 | 
				
			||||||
 | 
					    const user = ctx.state.currentUser.toJSON();
 | 
				
			||||||
 | 
					    ctx.body = user;
 | 
				
			||||||
 | 
					    await next();
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    ctx.throw(401, 'Unauthorized');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function signin(ctx: Context, next: Next) {
 | 
				
			||||||
 | 
					  const { uniqueField = 'email', values } = ctx.action.params;
 | 
				
			||||||
 | 
					  console.log('signin.values', values);
 | 
				
			||||||
 | 
					  if (!values[uniqueField]) {
 | 
				
			||||||
 | 
					    ctx.throw(401, '请填写邮箱账号');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const User = ctx.db.getCollection('users');
 | 
				
			||||||
 | 
					  const user = await User.model.scope('withPassword').findOne<any>({
 | 
				
			||||||
 | 
					    where: {
 | 
				
			||||||
 | 
					      [uniqueField]: values[uniqueField],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (!user) {
 | 
				
			||||||
 | 
					    ctx.throw(401, '邮箱账号未注册');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const pwd = User.getField<PasswordField>('password');
 | 
				
			||||||
 | 
					  const isValid = await pwd.verify(values.password, user.password);
 | 
				
			||||||
 | 
					  if (!isValid) {
 | 
				
			||||||
 | 
					    ctx.throw(401, '密码错误,请您重新输入');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!user.token) {
 | 
				
			||||||
 | 
					    user.token = cryptoRandomString({ length: 20 });
 | 
				
			||||||
 | 
					    await user.save();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  ctx.body = user.toJSON();
 | 
				
			||||||
 | 
					  delete ctx.body.password;
 | 
				
			||||||
 | 
					  await next();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function signout(ctx: Context, next: Next) {
 | 
				
			||||||
 | 
					  ctx.body = {};
 | 
				
			||||||
 | 
					  await next();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function signup(ctx: Context, next: Next) {
 | 
				
			||||||
 | 
					  const User = ctx.db.getCollection('users');
 | 
				
			||||||
 | 
					  const { values } = ctx.action.params;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const user = await User.model.create(values);
 | 
				
			||||||
 | 
					    ctx.body = user;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    if (error.errors) {
 | 
				
			||||||
 | 
					      ctx.throw(401, error.errors.map((data) => data.message).join(', '));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      ctx.throw(401, '注册失败');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  await next();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function lostpassword(ctx: Context, next: Next) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    values: { email },
 | 
				
			||||||
 | 
					  } = ctx.action.params;
 | 
				
			||||||
 | 
					  if (!email) {
 | 
				
			||||||
 | 
					    ctx.throw(401, '请填写邮箱账号');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const User = ctx.db.getCollection('users');
 | 
				
			||||||
 | 
					  const user = await User.model.findOne<any>({
 | 
				
			||||||
 | 
					    where: {
 | 
				
			||||||
 | 
					      email,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (!user) {
 | 
				
			||||||
 | 
					    ctx.throw(401, '邮箱账号未注册');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  user.resetToken = cryptoRandomString({ length: 20 });
 | 
				
			||||||
 | 
					  await user.save();
 | 
				
			||||||
 | 
					  ctx.body = user;
 | 
				
			||||||
 | 
					  await next();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function resetpassword(ctx: Context, next: Next) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    values: { email, password, resetToken },
 | 
				
			||||||
 | 
					  } = ctx.action.params;
 | 
				
			||||||
 | 
					  const User = ctx.db.getCollection('users');
 | 
				
			||||||
 | 
					  const user = await User.model.findOne<any>({
 | 
				
			||||||
 | 
					    where: {
 | 
				
			||||||
 | 
					      email,
 | 
				
			||||||
 | 
					      resetToken,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (!user) {
 | 
				
			||||||
 | 
					    ctx.throw(401, 'Unauthorized');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  user.token = null;
 | 
				
			||||||
 | 
					  user.resetToken = null;
 | 
				
			||||||
 | 
					  user.password = password;
 | 
				
			||||||
 | 
					  await user.save();
 | 
				
			||||||
 | 
					  ctx.body = user;
 | 
				
			||||||
 | 
					  await next();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getUserByResetToken(ctx: Context, next: Next) {
 | 
				
			||||||
 | 
					  const { token } = ctx.action.params;
 | 
				
			||||||
 | 
					  const User = ctx.db.getCollection('users');
 | 
				
			||||||
 | 
					  const user = await User.model.findOne({
 | 
				
			||||||
 | 
					    where: {
 | 
				
			||||||
 | 
					      resetToken: token,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (!user) {
 | 
				
			||||||
 | 
					    ctx.throw(401, 'Unauthorized');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  ctx.body = user;
 | 
				
			||||||
 | 
					  await next();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function updateProfile(ctx: Context, next: Next) {
 | 
				
			||||||
 | 
					  const { values } = ctx.action.params;
 | 
				
			||||||
 | 
					  if (!ctx.state.currentUser) {
 | 
				
			||||||
 | 
					    ctx.throw(401, 'Unauthorized');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  await ctx.state.currentUser.update(values);
 | 
				
			||||||
 | 
					  ctx.body = ctx.state.currentUser;
 | 
				
			||||||
 | 
					  await next();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function changePassword(ctx: Context, next: Next) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    values: { oldPassword, newPassword },
 | 
				
			||||||
 | 
					  } = ctx.action.params;
 | 
				
			||||||
 | 
					  if (!ctx.state.currentUser) {
 | 
				
			||||||
 | 
					    ctx.throw(401, 'Unauthorized');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const User = ctx.db.getCollection('users');
 | 
				
			||||||
 | 
					  const user = await User.model.scope('withPassword').findOne<any>({
 | 
				
			||||||
 | 
					    where: {
 | 
				
			||||||
 | 
					      email: ctx.state.currentUser.email,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const pwd = User.getField<PasswordField>('password');
 | 
				
			||||||
 | 
					  const isValid = await pwd.verify(oldPassword, user.password);
 | 
				
			||||||
 | 
					  if (!isValid) {
 | 
				
			||||||
 | 
					    ctx.throw(401, '密码错误,请您重新输入');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  user.password = newPassword;
 | 
				
			||||||
 | 
					  user.save();
 | 
				
			||||||
 | 
					  ctx.body = ctx.state.currentUser.toJSON();
 | 
				
			||||||
 | 
					  await next();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										90
									
								
								packages/plugin-users/src/collections/users.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								packages/plugin-users/src/collections/users.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					import { CollectionOptions } from '@nocobase/database';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'users',
 | 
				
			||||||
 | 
					  title: '{{t("Users")}}',
 | 
				
			||||||
 | 
					  sortable: 'sort',
 | 
				
			||||||
 | 
					  scopes: {
 | 
				
			||||||
 | 
					    withPassword: {
 | 
				
			||||||
 | 
					      attributes: { include: ['password'] },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  defaultScope: {
 | 
				
			||||||
 | 
					    attributes: { exclude: ['password'] },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  fields: [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      interface: 'string',
 | 
				
			||||||
 | 
					      type: 'string',
 | 
				
			||||||
 | 
					      name: 'nickname',
 | 
				
			||||||
 | 
					      uiSchema: {
 | 
				
			||||||
 | 
					        type: 'string',
 | 
				
			||||||
 | 
					        title: '{{t("Nickname")}}',
 | 
				
			||||||
 | 
					        'x-component': 'Input',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      interface: 'email',
 | 
				
			||||||
 | 
					      type: 'string',
 | 
				
			||||||
 | 
					      name: 'email',
 | 
				
			||||||
 | 
					      unique: true,
 | 
				
			||||||
 | 
					      uiSchema: {
 | 
				
			||||||
 | 
					        type: 'string',
 | 
				
			||||||
 | 
					        title: '{{t("Email")}}',
 | 
				
			||||||
 | 
					        'x-component': 'Input',
 | 
				
			||||||
 | 
					        require: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      interface: 'password',
 | 
				
			||||||
 | 
					      type: 'password',
 | 
				
			||||||
 | 
					      name: 'password',
 | 
				
			||||||
 | 
					      uiSchema: {
 | 
				
			||||||
 | 
					        type: 'string',
 | 
				
			||||||
 | 
					        title: '{{t("Password")}}',
 | 
				
			||||||
 | 
					        'x-component': 'Password',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      interface: 'linkTo',
 | 
				
			||||||
 | 
					      type: 'belongsToMany',
 | 
				
			||||||
 | 
					      name: 'roles',
 | 
				
			||||||
 | 
					      target: 'roles',
 | 
				
			||||||
 | 
					      foreignKey: 'user_id',
 | 
				
			||||||
 | 
					      otherKey: 'role_name',
 | 
				
			||||||
 | 
					      sourceKey: 'id',
 | 
				
			||||||
 | 
					      targetKey: 'name',
 | 
				
			||||||
 | 
					      uiSchema: {
 | 
				
			||||||
 | 
					        type: 'array',
 | 
				
			||||||
 | 
					        title: '{{t("Roles")}}',
 | 
				
			||||||
 | 
					        'x-component': 'Select.Drawer',
 | 
				
			||||||
 | 
					        'x-component-props': {
 | 
				
			||||||
 | 
					          multiple: true,
 | 
				
			||||||
 | 
					          fieldNames: {
 | 
				
			||||||
 | 
					            label: 'title',
 | 
				
			||||||
 | 
					            value: 'name',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        'x-decorator': 'FormItem',
 | 
				
			||||||
 | 
					        'x-designable-bar': 'Select.Drawer.DesignableBar',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      interface: 'select',
 | 
				
			||||||
 | 
					      type: 'string',
 | 
				
			||||||
 | 
					      name: 'appLang',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      type: 'string',
 | 
				
			||||||
 | 
					      name: 'token',
 | 
				
			||||||
 | 
					      unique: true,
 | 
				
			||||||
 | 
					      hidden: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      type: 'string',
 | 
				
			||||||
 | 
					      name: 'resetToken',
 | 
				
			||||||
 | 
					      unique: true,
 | 
				
			||||||
 | 
					      hidden: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					} as CollectionOptions;
 | 
				
			||||||
							
								
								
									
										1
									
								
								packages/plugin-users/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugin-users/src/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export { default } from './server';
 | 
				
			||||||
							
								
								
									
										10
									
								
								packages/plugin-users/src/middlewares/check.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								packages/plugin-users/src/middlewares/check.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					// TODO(usage): 拦截用户的处理暂时作为一个中间件导出,应用需要的时候可以直接使用这个中间件
 | 
				
			||||||
 | 
					export function check(options) {
 | 
				
			||||||
 | 
					  return async function check(ctx, next) {
 | 
				
			||||||
 | 
					    const { currentUser } = ctx.state;
 | 
				
			||||||
 | 
					    if (!currentUser) {
 | 
				
			||||||
 | 
					      return ctx.throw(401, 'Unauthorized');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return next();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								packages/plugin-users/src/middlewares/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/plugin-users/src/middlewares/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					export { check } from './check';
 | 
				
			||||||
 | 
					export { parseToken } from './parseToken';
 | 
				
			||||||
							
								
								
									
										23
									
								
								packages/plugin-users/src/middlewares/parseToken.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/plugin-users/src/middlewares/parseToken.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import { Context, Next } from '@nocobase/actions';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO(feature): 表名应在 options 中配置
 | 
				
			||||||
 | 
					// 中间件默认只解决解析 token 和附加对应 user 的工作,不解决是否提前报 401 退出。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 因为是否提供匿名访问资源是应用决定的,不是使用插件就一定不能匿名访问。
 | 
				
			||||||
 | 
					export function parseToken(options) {
 | 
				
			||||||
 | 
					  return async function parseToken(ctx: Context, next: Next) {
 | 
				
			||||||
 | 
					    const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
 | 
				
			||||||
 | 
					    const User = ctx.db.getCollection('users');
 | 
				
			||||||
 | 
					    const user = await User.model.findOne({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        token,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (user) {
 | 
				
			||||||
 | 
					      ctx.state.currentUser = user;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return next();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										63
									
								
								packages/plugin-users/src/server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								packages/plugin-users/src/server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					import path from 'path';
 | 
				
			||||||
 | 
					import * as actions from './actions/users';
 | 
				
			||||||
 | 
					import * as middlewares from './middlewares';
 | 
				
			||||||
 | 
					import { Collection } from '@nocobase/database';
 | 
				
			||||||
 | 
					import { PluginOptions } from '@nocobase/server';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'users',
 | 
				
			||||||
 | 
					  async load() {
 | 
				
			||||||
 | 
					    const database = this.app.db;
 | 
				
			||||||
 | 
					    const resourcer = this.app.resourcer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.on('db.init', async () => {
 | 
				
			||||||
 | 
					      const User = database.getCollection('users');
 | 
				
			||||||
 | 
					      await User.model.create({
 | 
				
			||||||
 | 
					        nickname: 'Super Admin',
 | 
				
			||||||
 | 
					        email: process.env.ADMIN_EMAIL || 'admin@nocobase.com',
 | 
				
			||||||
 | 
					        password: process.env.ADMIN_PASSWORD || 'admin123',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    database.on('afterDefineCollection', (collection: Collection) => {
 | 
				
			||||||
 | 
					      let { createdBy, updatedBy } = collection.options;
 | 
				
			||||||
 | 
					      if (createdBy === true) {
 | 
				
			||||||
 | 
					        collection.setField('createdById', {
 | 
				
			||||||
 | 
					          type: 'context',
 | 
				
			||||||
 | 
					          dataType: 'integer',
 | 
				
			||||||
 | 
					          dataIndex: 'state.currentUser.id',
 | 
				
			||||||
 | 
					          createOnly: true,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        collection.setField('createdBy', {
 | 
				
			||||||
 | 
					          type: 'belongsTo',
 | 
				
			||||||
 | 
					          target: 'users',
 | 
				
			||||||
 | 
					          foreignKey: 'createdById',
 | 
				
			||||||
 | 
					          targetKey: 'id',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (updatedBy === true) {
 | 
				
			||||||
 | 
					        collection.setField('updatedById', {
 | 
				
			||||||
 | 
					          type: 'context',
 | 
				
			||||||
 | 
					          dataType: 'integer',
 | 
				
			||||||
 | 
					          dataIndex: 'state.currentUser.id',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        collection.setField('updatedBy', {
 | 
				
			||||||
 | 
					          type: 'belongsTo',
 | 
				
			||||||
 | 
					          target: 'users',
 | 
				
			||||||
 | 
					          foreignKey: 'updatedById',
 | 
				
			||||||
 | 
					          targetKey: 'id',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await database.import({
 | 
				
			||||||
 | 
					      directory: path.resolve(__dirname, 'collections'),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const [key, action] of Object.entries(actions)) {
 | 
				
			||||||
 | 
					      resourcer.registerActionHandler(`users:${key}`, action);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    resourcer.use(middlewares.parseToken({}));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					} as PluginOptions;
 | 
				
			||||||
							
								
								
									
										9
									
								
								packages/plugin-users/tsconfig.build.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/plugin-users/tsconfig.build.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "extends": "../../tsconfig.build.json",
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "outDir": "./lib",
 | 
				
			||||||
 | 
					    "declaration": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["./src/**/*.ts", "./src/**/*.tsx"],
 | 
				
			||||||
 | 
					  "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										5
									
								
								packages/plugin-users/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								packages/plugin-users/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "extends": "../../tsconfig.json",
 | 
				
			||||||
 | 
					  "include": ["./src/**/*.ts", "./src/**/*.tsx"],
 | 
				
			||||||
 | 
					  "exclude": ["./esm/*", "./lib/*"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -4647,6 +4647,13 @@ crypto-random-string@^1.0.0:
 | 
				
			|||||||
  resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
 | 
					  resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
 | 
				
			||||||
  integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
 | 
					  integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					crypto-random-string@^3.3.0:
 | 
				
			||||||
 | 
					  version "3.3.1"
 | 
				
			||||||
 | 
					  resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.1.tgz#13cee94cac8001e4842501608ef779e0ed08f82d"
 | 
				
			||||||
 | 
					  integrity sha512-5j88ECEn6h17UePrLi6pn1JcLtAiANa3KExyr9y9Z5vo2mv56Gh3I4Aja/B9P9uyMwyxNHAHWv+nE72f30T5Dg==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    type-fest "^0.8.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
css-blank-pseudo@^0.1.4:
 | 
					css-blank-pseudo@^0.1.4:
 | 
				
			||||||
  version "0.1.4"
 | 
					  version "0.1.4"
 | 
				
			||||||
  resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
 | 
					  resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user