Feat/plugin users with jwt (#258)
* feat: plugin users with jwt * update github actions env * feat: get jwt config from options * feat: jwtService * fix: type * fix: build error * fix: yarn repository * fix: yarn build * fix: yarn build
This commit is contained in:
		
							parent
							
								
									25339a4240
								
							
						
					
					
						commit
						7e4b60c410
					
				| @ -12,6 +12,7 @@ export interface Context extends Koa.Context { | ||||
|   db: Database; | ||||
|   action: Action; | ||||
|   body: any; | ||||
|   app: any; | ||||
|   [key: string]: any; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -8,9 +8,12 @@ | ||||
|     "build:cjs": "tsc --project tsconfig.build.json", | ||||
|     "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm" | ||||
|   }, | ||||
|   "dependencies": {}, | ||||
|   "dependencies": { | ||||
|     "jsonwebtoken": "^8.5.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@nocobase/test": "^0.6.0-alpha.0" | ||||
|     "@nocobase/test": "^0.6.0-alpha.0", | ||||
|     "@types/jsonwebtoken": "^8.5.8" | ||||
|   }, | ||||
|   "gitHead": "e7df1f93c4e23b9a666d99ee7372c02bdaec97c4" | ||||
| } | ||||
|  | ||||
							
								
								
									
										51
									
								
								packages/plugin-users/src/__tests__/actions.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								packages/plugin-users/src/__tests__/actions.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| import { mockServer, MockServer } from '@nocobase/test'; | ||||
| import Database from '@nocobase/database'; | ||||
| import PluginACL from '@nocobase/plugin-acl'; | ||||
| import PluginUsers from '../server'; | ||||
| import supertest from 'supertest'; | ||||
| import { userPluginConfig } from './utils'; | ||||
| 
 | ||||
| describe('actions', () => { | ||||
|   let api: MockServer; | ||||
|   let db: Database; | ||||
|   let agent; | ||||
|   let pluginUser; | ||||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     api = mockServer(); | ||||
|     await api.cleanDb(); | ||||
|     api.plugin(PluginUsers, userPluginConfig); | ||||
|     api.plugin(PluginACL); | ||||
| 
 | ||||
|     await api.loadAndInstall(); | ||||
|     db = api.db; | ||||
| 
 | ||||
|     agent = supertest.agent(api.callback()); | ||||
|     pluginUser = api.getPlugin('@nocobase/plugin-users'); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(async () => { | ||||
|     await db.close(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should login user with password', async () => { | ||||
|     const { adminEmail, adminPassword } = userPluginConfig.installing; | ||||
| 
 | ||||
|     let response = await api.agent().resource('users').check(); | ||||
|     expect(response.statusCode).toEqual(401); | ||||
| 
 | ||||
|     response = await agent.post('/users:signin').send({ | ||||
|       email: adminEmail, | ||||
|       password: adminPassword, | ||||
|     }); | ||||
| 
 | ||||
|     expect(response.statusCode).toEqual(200); | ||||
| 
 | ||||
|     const data = response.body.data; | ||||
|     const token = data.token; | ||||
|     expect(token).toBeDefined(); | ||||
| 
 | ||||
|     response = await agent.get('/users:check').set({ Authorization: 'Bearer ' + token }); | ||||
|     expect(response.statusCode).toEqual(200); | ||||
|   }); | ||||
| }); | ||||
| @ -1,5 +1,6 @@ | ||||
| import Database from '@nocobase/database'; | ||||
| import { mockServer, MockServer } from '@nocobase/test'; | ||||
| import { userPluginConfig } from './utils'; | ||||
| 
 | ||||
| describe('createdBy/updatedBy', () => { | ||||
|   let api: MockServer; | ||||
| @ -7,7 +8,7 @@ describe('createdBy/updatedBy', () => { | ||||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     api = mockServer(); | ||||
|     api.plugin(require('../server').default); | ||||
|     api.plugin(require('../server').default, userPluginConfig); | ||||
|     await api.loadAndInstall(); | ||||
|     db = api.db; | ||||
|   }); | ||||
|  | ||||
| @ -1,19 +1,24 @@ | ||||
| import Database, { BelongsToManyRepository } from '@nocobase/database'; | ||||
| import PluginACL from '@nocobase/plugin-acl'; | ||||
| import { MockServer, mockServer } from '@nocobase/test'; | ||||
| import { userPluginConfig } from './utils'; | ||||
| import UsersPlugin from '@nocobase/plugin-users'; | ||||
| 
 | ||||
| describe('role', () => { | ||||
|   let api: MockServer; | ||||
|   let db: Database; | ||||
| 
 | ||||
|   let usersPlugin: UsersPlugin; | ||||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     api = mockServer(); | ||||
|     await api.cleanDb(); | ||||
|     api.plugin(require('../server').default); | ||||
|     api.plugin(UsersPlugin, userPluginConfig); | ||||
|     api.plugin(PluginACL); | ||||
|     await api.loadAndInstall(); | ||||
| 
 | ||||
|     db = api.db; | ||||
|     usersPlugin = api.getPlugin('@nocobase/plugin-users'); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(async () => { | ||||
| @ -98,6 +103,7 @@ describe('role', () => { | ||||
|     await userRolesRepo.add('test1'); | ||||
|     await userRolesRepo.add('test2'); | ||||
| 
 | ||||
|     const userToken = usersPlugin.jwtService.sign({ userId: user.get('id') }); | ||||
|     const response = await api | ||||
|       .agent() | ||||
|       .post('/users:setDefaultRole') | ||||
| @ -105,7 +111,7 @@ describe('role', () => { | ||||
|         defaultRole: 'test2', | ||||
|       }) | ||||
|       .set({ | ||||
|         Authorization: `Bearer ${user.get('token')}`, | ||||
|         Authorization: `Bearer ${userToken}`, | ||||
|       }); | ||||
| 
 | ||||
|     expect(response.statusCode).toEqual(200); | ||||
|  | ||||
							
								
								
									
										12
									
								
								packages/plugin-users/src/__tests__/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/plugin-users/src/__tests__/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| import { UserPluginConfig } from '../server'; | ||||
| 
 | ||||
| export const userPluginConfig: UserPluginConfig = { | ||||
|   jwt: { | ||||
|     secret: '09f26e402586e2faa8da4c98a35f1b20d6b033c60', | ||||
|   }, | ||||
|   installing: { | ||||
|     adminEmail: 'test@test.com', | ||||
|     adminPassword: 'password', | ||||
|     adminNickname: 'test', | ||||
|   }, | ||||
| }; | ||||
| @ -1,5 +1,7 @@ | ||||
| import { Context, Next } from '@nocobase/actions'; | ||||
| import { PasswordField } from '@nocobase/database'; | ||||
| import UsersPlugin from '../server'; | ||||
| 
 | ||||
| import crypto from 'crypto'; | ||||
| 
 | ||||
| export async function check(ctx: Context, next: Next) { | ||||
| @ -14,7 +16,7 @@ export async function check(ctx: Context, next: Next) { | ||||
| 
 | ||||
| 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, '请填写邮箱账号'); | ||||
|   } | ||||
| @ -32,19 +34,19 @@ export async function signin(ctx: Context, next: Next) { | ||||
|   if (!isValid) { | ||||
|     ctx.throw(401, '密码错误,请您重新输入'); | ||||
|   } | ||||
|   if (!user.token) { | ||||
|     user.token = crypto.randomBytes(20).toString('hex'); | ||||
|     await user.save(); | ||||
|   } | ||||
| 
 | ||||
|   const pluginUser = ctx.app.getPlugin('@nocobase/plugin-users'); | ||||
| 
 | ||||
|   ctx.body = { | ||||
|     ...user.toJSON(), | ||||
|     token: user.get('token'), | ||||
|     token: pluginUser.jwtService.sign({ | ||||
|       userId: user.get('id'), | ||||
|     }), | ||||
|   }; | ||||
|   await next(); | ||||
| } | ||||
| 
 | ||||
| export async function signout(ctx: Context, next: Next) { | ||||
|   await ctx.state.currentUser.update({ token: null }); | ||||
|   ctx.body = ctx.state.currentUser; | ||||
|   await next(); | ||||
| } | ||||
|  | ||||
| @ -65,12 +65,6 @@ export default { | ||||
|       type: 'string', | ||||
|       name: 'appLang', | ||||
|     }, | ||||
|     { | ||||
|       type: 'string', | ||||
|       name: 'token', | ||||
|       unique: true, | ||||
|       hidden: true, | ||||
|     }, | ||||
|     { | ||||
|       type: 'string', | ||||
|       name: 'resetToken', | ||||
|  | ||||
							
								
								
									
										34
									
								
								packages/plugin-users/src/jwt-service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								packages/plugin-users/src/jwt-service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| import * as jwt from 'jsonwebtoken'; | ||||
| 
 | ||||
| export interface JwtOptions { | ||||
|   secret: string; | ||||
|   expiresIn?: string; | ||||
| } | ||||
| 
 | ||||
| export class JwtService { | ||||
|   constructor(protected options: JwtOptions) {} | ||||
| 
 | ||||
|   private expiresIn() { | ||||
|     return this.options.expiresIn || '7d'; | ||||
|   } | ||||
| 
 | ||||
|   private secret() { | ||||
|     return this.options.secret; | ||||
|   } | ||||
| 
 | ||||
|   sign(payload: any) { | ||||
|     return jwt.sign(payload, this.secret(), { expiresIn: this.expiresIn() }); | ||||
|   } | ||||
| 
 | ||||
|   decode(token: string): Promise<any> { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       jwt.verify(token, this.secret(), (err: any, decoded: any) => { | ||||
|         if (err) { | ||||
|           return reject(err); | ||||
|         } | ||||
| 
 | ||||
|         resolve(decoded); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| @ -1,4 +1,5 @@ | ||||
| import { Context, Next } from '@nocobase/actions'; | ||||
| import UsersPlugin from '../server'; | ||||
| 
 | ||||
| // TODO(feature): 表名应在 options 中配置
 | ||||
| // 中间件默认只解决解析 token 和附加对应 user 的工作,不解决是否提前报 401 退出。
 | ||||
| @ -33,14 +34,23 @@ function setCurrentRole(ctx, user) { | ||||
| } | ||||
| 
 | ||||
| async function findUserByToken(ctx: Context) { | ||||
|   const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, ''); | ||||
|   const User = ctx.db.getCollection('users'); | ||||
|   const user = await User.repository.findOne({ | ||||
|   const token = getTokenFromCtx(ctx); | ||||
|   if (!token) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   const pluginUser = ctx.app.getPlugin('@nocobase/plugin-users'); | ||||
| 
 | ||||
|   const { userId } = await pluginUser.jwtService.decode(token); | ||||
| 
 | ||||
|   return await ctx.db.getRepository('users').findOne({ | ||||
|     filter: { | ||||
|       token, | ||||
|       id: userId, | ||||
|     }, | ||||
|     appends: ['roles'], | ||||
|   }); | ||||
| 
 | ||||
|   return user; | ||||
| } | ||||
| 
 | ||||
| function getTokenFromCtx(ctx: Context) { | ||||
|   return ctx.get('Authorization').replace(/^Bearer\s+/gi, ''); | ||||
| } | ||||
|  | ||||
| @ -3,8 +3,26 @@ import { Plugin } from '@nocobase/server'; | ||||
| import { resolve } from 'path'; | ||||
| import * as actions from './actions/users'; | ||||
| import * as middlewares from './middlewares'; | ||||
| import { JwtOptions, JwtService } from './jwt-service'; | ||||
| 
 | ||||
| export interface UserPluginConfig { | ||||
|   jwt: JwtOptions; | ||||
| 
 | ||||
|   installing: { | ||||
|     adminNickname: string; | ||||
|     adminEmail: string; | ||||
|     adminPassword: string; | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export default class UsersPlugin extends Plugin<UserPluginConfig> { | ||||
|   public jwtService: JwtService; | ||||
| 
 | ||||
|   constructor(app, options) { | ||||
|     super(app, options); | ||||
|     this.jwtService = new JwtService(options?.jwt); | ||||
|   } | ||||
| 
 | ||||
| export default class UsersPlugin extends Plugin { | ||||
|   async beforeLoad() { | ||||
|     this.db.on('users.afterCreateWithAssociations', async (model, options) => { | ||||
|       const { transaction } = options; | ||||
| @ -82,7 +100,7 @@ export default class UsersPlugin extends Plugin { | ||||
|       adminNickname = 'Super Admin', | ||||
|       adminEmail = 'admin@nocobase.com', | ||||
|       adminPassword = 'admin123', | ||||
|     } = this.options; | ||||
|     } = this.options.installing; | ||||
| 
 | ||||
|     return { | ||||
|       adminNickname, | ||||
| @ -90,6 +108,7 @@ export default class UsersPlugin extends Plugin { | ||||
|       adminPassword, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   async install() { | ||||
|     const { adminNickname, adminPassword, adminEmail } = this.getRootUserInfo(); | ||||
| 
 | ||||
| @ -102,6 +121,7 @@ export default class UsersPlugin extends Plugin { | ||||
|         roles: ['admin'], | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     const repo = this.db.getRepository<any>('collections'); | ||||
|     if (repo) { | ||||
|       await repo.db2cm('users'); | ||||
|  | ||||
							
								
								
									
										87
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -3169,6 +3169,13 @@ | ||||
|   resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" | ||||
|   integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= | ||||
| 
 | ||||
| "@types/jsonwebtoken@^8.5.8": | ||||
|   version "8.5.8" | ||||
|   resolved "https://registry.npmjs.org/@types%2fjsonwebtoken/-/jsonwebtoken-8.5.8.tgz#01b39711eb844777b7af1d1f2b4cf22fda1c0c44" | ||||
|   integrity sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A== | ||||
|   dependencies: | ||||
|     "@types/node" "*" | ||||
| 
 | ||||
| "@types/keygrip@*": | ||||
|   version "1.0.2" | ||||
|   resolved "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" | ||||
| @ -4837,6 +4844,11 @@ bser@2.1.1: | ||||
|   dependencies: | ||||
|     node-int64 "^0.4.0" | ||||
| 
 | ||||
| buffer-equal-constant-time@1.0.1: | ||||
|   version "1.0.1" | ||||
|   resolved "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" | ||||
|   integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== | ||||
| 
 | ||||
| buffer-from@1.x, buffer-from@^1.0.0: | ||||
|   version "1.1.2" | ||||
|   resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" | ||||
| @ -6487,6 +6499,13 @@ ecc-jsbn@~0.1.1: | ||||
|     jsbn "~0.1.0" | ||||
|     safer-buffer "^2.1.0" | ||||
| 
 | ||||
| ecdsa-sig-formatter@1.0.11: | ||||
|   version "1.0.11" | ||||
|   resolved "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" | ||||
|   integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== | ||||
|   dependencies: | ||||
|     safe-buffer "^5.0.1" | ||||
| 
 | ||||
| ee-first@1.1.1, ee-first@~1.1.1: | ||||
|   version "1.1.1" | ||||
|   resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" | ||||
| @ -9923,6 +9942,22 @@ jsonparse@^1.2.0, jsonparse@^1.3.1: | ||||
|   resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" | ||||
|   integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= | ||||
| 
 | ||||
| jsonwebtoken@^8.5.1: | ||||
|   version "8.5.1" | ||||
|   resolved "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" | ||||
|   integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== | ||||
|   dependencies: | ||||
|     jws "^3.2.2" | ||||
|     lodash.includes "^4.3.0" | ||||
|     lodash.isboolean "^3.0.3" | ||||
|     lodash.isinteger "^4.0.4" | ||||
|     lodash.isnumber "^3.0.3" | ||||
|     lodash.isplainobject "^4.0.6" | ||||
|     lodash.isstring "^4.0.1" | ||||
|     lodash.once "^4.0.0" | ||||
|     ms "^2.1.1" | ||||
|     semver "^5.6.0" | ||||
| 
 | ||||
| jsprim@^1.2.2: | ||||
|   version "1.4.1" | ||||
|   resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" | ||||
| @ -9946,6 +9981,23 @@ jstoxml@^0.2.3: | ||||
|     array-includes "^3.1.3" | ||||
|     object.assign "^4.1.2" | ||||
| 
 | ||||
| jwa@^1.4.1: | ||||
|   version "1.4.1" | ||||
|   resolved "https://registry.npmmirror.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" | ||||
|   integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== | ||||
|   dependencies: | ||||
|     buffer-equal-constant-time "1.0.1" | ||||
|     ecdsa-sig-formatter "1.0.11" | ||||
|     safe-buffer "^5.0.1" | ||||
| 
 | ||||
| jws@^3.2.2: | ||||
|   version "3.2.2" | ||||
|   resolved "https://registry.npmmirror.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" | ||||
|   integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== | ||||
|   dependencies: | ||||
|     jwa "^1.4.1" | ||||
|     safe-buffer "^5.0.1" | ||||
| 
 | ||||
| katex@^0.12.0: | ||||
|   version "0.12.0" | ||||
|   resolved "https://registry.npmjs.org/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9" | ||||
| @ -10288,16 +10340,51 @@ lodash.debounce@^4.0.8: | ||||
|   resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" | ||||
|   integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= | ||||
| 
 | ||||
| lodash.includes@^4.3.0: | ||||
|   version "4.3.0" | ||||
|   resolved "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" | ||||
|   integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== | ||||
| 
 | ||||
| lodash.isboolean@^3.0.3: | ||||
|   version "3.0.3" | ||||
|   resolved "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" | ||||
|   integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== | ||||
| 
 | ||||
| lodash.isinteger@^4.0.4: | ||||
|   version "4.0.4" | ||||
|   resolved "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" | ||||
|   integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== | ||||
| 
 | ||||
| lodash.ismatch@^4.4.0: | ||||
|   version "4.4.0" | ||||
|   resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" | ||||
|   integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= | ||||
| 
 | ||||
| lodash.isnumber@^3.0.3: | ||||
|   version "3.0.3" | ||||
|   resolved "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" | ||||
|   integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== | ||||
| 
 | ||||
| lodash.isplainobject@^4.0.6: | ||||
|   version "4.0.6" | ||||
|   resolved "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" | ||||
|   integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== | ||||
| 
 | ||||
| lodash.isstring@^4.0.1: | ||||
|   version "4.0.1" | ||||
|   resolved "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" | ||||
|   integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== | ||||
| 
 | ||||
| lodash.merge@^4.6.2: | ||||
|   version "4.6.2" | ||||
|   resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" | ||||
|   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== | ||||
| 
 | ||||
| lodash.once@^4.0.0: | ||||
|   version "4.1.1" | ||||
|   resolved "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" | ||||
|   integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== | ||||
| 
 | ||||
| lodash.sortby@^4.7.0: | ||||
|   version "4.7.0" | ||||
|   resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user