Skip to content

Commit

Permalink
perf: improves validateUserByPayload logic
Browse files Browse the repository at this point in the history
  • Loading branch information
SolidZORO committed May 27, 2020
1 parent 9c18c02 commit 6208557
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 23 deletions.
11 changes: 7 additions & 4 deletions packages/_leaa-common/src/entrys/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,23 @@ export class User extends Base {
@Column({ type: 'int', default: 0 })
status?: number;

@Exclude({ toPlainOnly: true })
@Exclude()
@Column({ type: 'varchar', length: 64, select: false })
password!: string;

@Column({ type: 'int', default: 0 })
is_admin?: number;

@Column({ type: 'varchar', length: 32, nullable: true })
@Exclude()
@Column({ type: 'varchar', length: 32, nullable: true, select: false })
last_login_ip?: string;

@Column({ type: 'timestamp', nullable: true })
@Exclude()
@Column({ type: 'timestamp', nullable: true, select: false })
last_login_at?: Date;

@Column({ type: 'timestamp', nullable: true })
@Exclude()
@Column({ type: 'timestamp', nullable: true, select: false })
last_token_at?: Date;

// @Exclude({ toPlainOnly: true })
Expand Down
2 changes: 2 additions & 0 deletions packages/leaa-api/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './not-found-ip.exception';
export * from './not-found-token.exception';
export * from './not-found-user.exception';
11 changes: 11 additions & 0 deletions packages/leaa-api/src/exceptions/not-found-token.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { HttpException, HttpStatus } from '@nestjs/common';

export class NotFoundTokenException extends HttpException {
/**
* @param objectOrError string or object describing the error condition.
* @param description a short description of the HTTP error.
*/
constructor(objectOrError?: string | object | any, description = 'Not Found Token') {
super(HttpException.createBody(objectOrError, description, HttpStatus.NOT_FOUND), HttpStatus.NOT_FOUND);
}
}
11 changes: 11 additions & 0 deletions packages/leaa-api/src/exceptions/not-found-user.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { HttpException, HttpStatus } from '@nestjs/common';

export class NotFoundUserException extends HttpException {
/**
* @param objectOrError string or object describing the error condition.
* @param description a short description of the HTTP error.
*/
constructor(objectOrError?: string | object | any, description = 'Not Found User') {
super(HttpException.createBody(objectOrError, description, HttpStatus.NOT_FOUND), HttpStatus.NOT_FOUND);
}
}
44 changes: 28 additions & 16 deletions packages/leaa-api/src/modules/v1/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { InjectRepository } from '@nestjs/typeorm';

import { User, Verification, Action, Auth } from '@leaa/common/src/entrys';
import { AuthLoginInput } from '@leaa/common/src/dtos/auth';
import { checkAvailableUser, logger } from '@leaa/api/src/utils';
import { checkUserIsEnable, logger } from '@leaa/api/src/utils';
import { ActionService } from '@leaa/api/src/modules/v1/action/action.service';
import { ICrudRequest } from '@leaa/api/src/interfaces';
import { IJwtPayload } from '@leaa/common/src/interfaces';
import moment from 'moment';
import { ConfigService } from '@leaa/api/src/modules/v1/config/config.service';
import { RoleService } from '@leaa/api/src/modules/v1/role/role.service';
import { NotFoundIpException } from '@leaa/api/src/exceptions';
import { NotFoundIpException, NotFoundTokenException, NotFoundUserException } from '@leaa/api/src/exceptions';

const CLS_NAME = 'AuthService';

Expand Down Expand Up @@ -60,9 +60,9 @@ export class AuthService {
account,
});

const user = checkAvailableUser(findUser);
const user = checkUserIsEnable(findUser);

if (!user) throw new UnauthorizedException();
if (!user) throw new NotFoundTokenException();
if (user) user.flatPermissions = await this.roleService.getFlatPermissionsByUser(user);
//
// // log with user_id
Expand Down Expand Up @@ -124,20 +124,28 @@ export class AuthService {
return this.addTokenToUser(user);
}

// MUST DO minimal cost query
async validateUserByPayload(jwtPayload: IJwtPayload): Promise<User | undefined> {
if (!jwtPayload) throw new UnauthorizedException();
if (!jwtPayload.iat || !jwtPayload.exp || !jwtPayload.id) throw new UnauthorizedException();
if (!jwtPayload) throw new NotFoundTokenException();
if (!jwtPayload.iat || !jwtPayload.exp || !jwtPayload.id) throw new NotFoundTokenException();

const hasUser = await this.userRepo.findOne({
let user = await this.userRepo.findOne({
select: ['id', 'status', 'is_admin', 'name', 'avatar_url', 'last_token_at'],
relations: ['roles'],
where: { id: jwtPayload.id },
});

const user = checkAvailableUser(hasUser);
if (!user) throw new NotFoundUserException();

// @TIPS IMPORTANT! if user info is changed, Compare jwt `iattz` and `last_token_at`
// 对比 jwtPayload.iattz(原来的 iat 可能会存在客户端与服务器 tz 不相同带问题,所以这里加了一个带 timezome 的 iat)
user = checkUserIsEnable(user);

/**
* IMPORTANT! if user info is changed, Compare jwt `iattz` and `last_token_at`
*
* @ideaNotes
* 对比 jwtPayload.iattz(jwt 默认的 iat 会存在 clinet 与 server 时间戳不相等问题,所以给 jwt 加了一个带 timezome 的 iat)
* 修改 user 的 password、status、is_admin 都会更新 last_token_at 为 now(),这样保证 last_token_at 一定会大于 iattz
* 这样用户在下一次访问的可以顺利被弹出用 : > 此方案不动用 DB 但做到了类似 backlist 的方案,非常环保
*/
if (moment(jwtPayload.iattz).isBefore(moment(user.last_token_at))) throw new UnauthorizedException();

const flatPermissions = await this.roleService.getFlatPermissionsByUser(user);
Expand All @@ -151,7 +159,7 @@ export class AuthService {
async userByToken(body?: { token?: string }): Promise<User | undefined> {
const token = body?.token;

if (!token) throw new NotFoundException();
if (!token) throw new NotFoundTokenException();

// @ts-ignore
const jwtPayload: { id: any } = this.jwtService.decode(token);
Expand All @@ -165,6 +173,7 @@ export class AuthService {
action: In(['login', 'guest']),
token: In([token, 'NO-TOKEN']),
});

await this.verificationRepo.delete({ token });
}

Expand All @@ -180,7 +189,10 @@ export class AuthService {

async createToken(user: User): Promise<{ authExpiresIn: number; authToken: string }> {
// iattz = iat timezone
const jwtPayload: IJwtPayload = { id: user.id, iattz: moment().toISOString() };
const jwtPayload: IJwtPayload = {
id: user.id,
iattz: moment().toISOString(),
};

// https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback
const authExpiresIn = this.configService.SERVER_COOKIE_EXPIRES_SECOND;
Expand Down Expand Up @@ -317,10 +329,10 @@ export class AuthService {
// import { AuthsWithPaginationObject, CreateAuthInput } from '@leaa/common/src/dtos/auth';
// import { IJwtPayload } from '@leaa/common/src/interfaces';
// import { ConfigService } from '@leaa/api/src/modules/config/config.service';
// import { checkAvailableUser, argsFormat, calcQbPageInfo, commonDelete, errorMsg } from '@leaa/api/src/utils';
// import { checkUserIsEnable, argsFormat, calcQbPageInfo, commonDelete, errorMsg } from '@leaa/api/src/utils';
// import { User, Verification, Action, Auth } from '@leaa/common/src/entrys';
// import { AuthLoginInput } from '@leaa/common/src/dtos/auth';
// import { checkAvailableUser, logger } from '@leaa/api/src/utils';
// import { checkUserIsEnable, logger } from '@leaa/api/src/utils';
// import { UserService } from '@leaa/api/src/modules/user/user.service';
// import { permissionConfig } from '@leaa/api/src/configs';
// import { AuthService } from '@leaa/api/src/modules/auth/auth.service';
Expand Down Expand Up @@ -495,7 +507,7 @@ export class AuthService {
// account,
// });
//
// const user = checkAvailableUser(findUser);
// const user = checkUserIsEnable(findUser);
//
// // IMPORTANT! if user info is changed, Compare `iat` and `last_token_at`
// if (moment(payload.iattz).isBefore(moment(user.last_token_at))) {
Expand Down
4 changes: 2 additions & 2 deletions packages/leaa-api/src/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { JwtService } from '@nestjs/jwt';
import { IJwtPayload } from '@leaa/common/src/interfaces';
import { ConfigService } from '@leaa/api/src/modules/v1/config/config.service';
import { AuthService } from '@leaa/api/src/modules/v1/auth/auth.service';
import { checkAvailableUser } from '@leaa/api/src/utils';
import { checkUserIsEnable } from '@leaa/api/src/utils';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
Expand All @@ -29,6 +29,6 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {

if (!user) return new UnauthorizedException();

return checkAvailableUser(user);
return checkUserIsEnable(user);
}
}
8 changes: 7 additions & 1 deletion packages/leaa-api/src/utils/auth.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import { User } from '@leaa/common/src/entrys';
import { IPermissionSlug } from '@leaa/common/src/interfaces';
import { UnauthorizedException } from '@nestjs/common';

export const checkAvailableUser = (user?: User): User => {
export const checkUserIsEnable = (user?: User): User => {
if (!user || (user && user.status !== 1)) throw new UnauthorizedException();

return user;
};

export const checkUserIsAdmin = (user?: User): User => {
if (!user || (user && user.status !== 1 && user.is_admin !== 1)) throw new UnauthorizedException();

return user;
};

export const can = (user: User, permissionName: IPermissionSlug): boolean => {
if (!user || !permissionName || !user.flatPermissions) return false;

Expand Down

0 comments on commit 6208557

Please sign in to comment.