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:
ChengLei Shao 2022-04-09 14:54:46 +08:00 committed by GitHub
parent 25339a4240
commit 7e4b60c410
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 247 additions and 26 deletions

View File

@ -12,6 +12,7 @@ export interface Context extends Koa.Context {
db: Database;
action: Action;
body: any;
app: any;
[key: string]: any;
}

View File

@ -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"
}

View 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);
});
});

View File

@ -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;
});

View File

@ -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);

View 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',
},
};

View File

@ -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();
}

View File

@ -65,12 +65,6 @@ export default {
type: 'string',
name: 'appLang',
},
{
type: 'string',
name: 'token',
unique: true,
hidden: true,
},
{
type: 'string',
name: 'resetToken',

View 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);
});
});
}
}

View File

@ -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, '');
}

View File

@ -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');

View File

@ -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"