@lenne.tech/nest-server 9.1.0 → 9.2.0
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.
- package/dist/config.env.js +41 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/filters/http-exception-log.filter.d.ts +4 -0
- package/dist/core/common/filters/http-exception-log.filter.js +30 -0
- package/dist/core/common/filters/http-exception-log.filter.js.map +1 -0
- package/dist/core/common/interceptors/check-security.interceptor.d.ts +5 -0
- package/dist/core/common/interceptors/check-security.interceptor.js +47 -0
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +16 -6
- package/dist/core/common/models/core-model.model.d.ts +1 -0
- package/dist/core/common/models/core-model.model.js +3 -0
- package/dist/core/common/models/core-model.model.js.map +1 -1
- package/dist/core/common/plugins/complexity.plugin.d.ts +9 -0
- package/dist/core/common/plugins/complexity.plugin.js +47 -0
- package/dist/core/common/plugins/complexity.plugin.js.map +1 -0
- package/dist/core/common/plugins/mongoose-id.plugin.d.ts +1 -2
- package/dist/core/common/plugins/mongoose-id.plugin.js +7 -2
- package/dist/core/common/plugins/mongoose-id.plugin.js.map +1 -1
- package/dist/core/common/services/config.service.d.ts +4 -4
- package/dist/core/common/services/config.service.js.map +1 -1
- package/dist/core/common/services/module.service.js +2 -2
- package/dist/core/common/services/module.service.js.map +1 -1
- package/dist/core/modules/auth/core-auth.model.d.ts +4 -1
- package/dist/core/modules/auth/core-auth.model.js +12 -1
- package/dist/core/modules/auth/core-auth.model.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.d.ts +3 -1
- package/dist/core/modules/auth/core-auth.module.js +7 -2
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.d.ts +22 -2
- package/dist/core/modules/auth/core-auth.resolver.js +77 -9
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/auth/guards/auth.guard.d.ts +1 -1
- package/dist/core/modules/auth/guards/auth.guard.js +9 -4
- package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
- package/dist/core/modules/auth/guards/refresh-token.guard.d.ts +4 -0
- package/dist/core/modules/auth/guards/refresh-token.guard.js +18 -0
- package/dist/core/modules/auth/guards/refresh-token.guard.js.map +1 -0
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/auth/inputs/core-auth-sign-in.input.d.ts +1 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js +5 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js.map +1 -1
- package/dist/core/modules/auth/inputs/core-auth-sign-up.input.d.ts +1 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js +5 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js.map +1 -1
- package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +3 -0
- package/dist/core/modules/auth/interfaces/jwt-payload.interface.d.ts +1 -1
- package/dist/core/modules/auth/services/core-auth-user.service.d.ts +3 -0
- package/dist/core/modules/auth/services/core-auth-user.service.js.map +1 -1
- package/dist/core/modules/auth/services/core-auth.service.d.ts +23 -5
- package/dist/core/modules/auth/services/core-auth.service.js +121 -13
- package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
- package/dist/core/modules/auth/strategies/jwt-refresh.strategy.d.ts +12 -0
- package/dist/core/modules/auth/strategies/jwt-refresh.strategy.js +61 -0
- package/dist/core/modules/auth/strategies/jwt-refresh.strategy.js.map +1 -0
- package/dist/core/modules/auth/{jwt.strategy.d.ts → strategies/jwt.strategy.d.ts} +4 -3
- package/dist/core/modules/auth/{jwt.strategy.js → strategies/jwt.strategy.js} +12 -5
- package/dist/core/modules/auth/strategies/jwt.strategy.js.map +1 -0
- package/dist/core/modules/user/core-user.model.d.ts +2 -0
- package/dist/core/modules/user/core-user.model.js +12 -0
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/core.module.js +12 -2
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/main.js +23 -0
- package/dist/main.js.map +1 -1
- package/dist/server/modules/auth/auth.model.js.map +1 -1
- package/dist/server/modules/auth/auth.resolver.d.ts +13 -5
- package/dist/server/modules/auth/auth.resolver.js +21 -12
- package/dist/server/modules/auth/auth.resolver.js.map +1 -1
- package/dist/server/modules/auth/auth.service.d.ts +2 -1
- package/dist/server/modules/auth/auth.service.js +7 -48
- package/dist/server/modules/auth/auth.service.js.map +1 -1
- package/dist/server/modules/file/file.module.js +3 -3
- package/dist/server/modules/file/file.module.js.map +1 -1
- package/dist/server/modules/user/user.model.d.ts +1 -0
- package/dist/server/modules/user/user.model.js +19 -0
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/server.module.js +12 -1
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +32 -27
- package/src/config.env.ts +41 -2
- package/src/core/common/filters/http-exception-log.filter.ts +27 -0
- package/src/core/common/interceptors/check-security.interceptor.ts +51 -0
- package/src/core/common/interfaces/server-options.interface.ts +67 -30
- package/src/core/common/models/core-model.model.ts +7 -0
- package/src/core/common/plugins/complexity.plugin.ts +31 -0
- package/src/core/common/plugins/mongoose-id.plugin.js +4 -2
- package/src/core/common/services/config.service.ts +4 -4
- package/src/core/common/services/module.service.ts +2 -2
- package/src/core/modules/auth/core-auth.model.ts +15 -2
- package/src/core/modules/auth/core-auth.module.ts +8 -2
- package/src/core/modules/auth/core-auth.resolver.ts +93 -10
- package/src/core/modules/auth/guards/auth.guard.ts +12 -5
- package/src/core/modules/auth/guards/refresh-token.guard.ts +5 -0
- package/src/core/modules/auth/guards/roles.guard.ts +1 -1
- package/src/core/modules/auth/inputs/core-auth-sign-in.input.ts +3 -0
- package/src/core/modules/auth/inputs/core-auth-sign-up.input.ts +3 -0
- package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +15 -0
- package/src/core/modules/auth/interfaces/jwt-payload.interface.ts +1 -1
- package/src/core/modules/auth/services/core-auth-user.service.ts +15 -0
- package/src/core/modules/auth/services/core-auth.service.ts +216 -18
- package/src/core/modules/auth/strategies/jwt-refresh.strategy.ts +56 -0
- package/src/core/modules/auth/{jwt.strategy.ts → strategies/jwt.strategy.ts} +16 -5
- package/src/core/modules/user/core-user.model.ts +17 -1
- package/src/core.module.ts +14 -2
- package/src/index.ts +6 -1
- package/src/main.ts +29 -0
- package/src/server/modules/auth/auth.model.ts +1 -1
- package/src/server/modules/auth/auth.resolver.ts +26 -8
- package/src/server/modules/auth/auth.service.ts +20 -61
- package/src/server/modules/file/file.module.ts +3 -3
- package/src/server/modules/user/user.model.ts +29 -0
- package/src/server/server.module.ts +12 -1
- package/dist/core/modules/auth/jwt.strategy.js.map +0 -1
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UseGuards } from '@nestjs/common';
|
|
2
|
+
import { Args, Context, Info, Mutation, Resolver } from '@nestjs/graphql';
|
|
3
|
+
import { Response as ResponseType } from 'express';
|
|
2
4
|
import { GraphQLResolveInfo } from 'graphql';
|
|
5
|
+
import { GraphQLUser } from '../../common/decorators/graphql-user.decorator';
|
|
6
|
+
import { ConfigService } from '../../common/services/config.service';
|
|
3
7
|
import { CoreAuthModel } from './core-auth.model';
|
|
8
|
+
import { AuthGuard } from './guards/auth.guard';
|
|
9
|
+
import { CoreAuthSignInInput } from './inputs/core-auth-sign-in.input';
|
|
10
|
+
import { CoreAuthSignUpInput } from './inputs/core-auth-sign-up.input';
|
|
11
|
+
import { ICoreAuthUser } from './interfaces/core-auth-user.interface';
|
|
4
12
|
import { CoreAuthService } from './services/core-auth.service';
|
|
5
13
|
|
|
6
14
|
/**
|
|
@@ -11,21 +19,96 @@ export class CoreAuthResolver {
|
|
|
11
19
|
/**
|
|
12
20
|
* Import services
|
|
13
21
|
*/
|
|
14
|
-
constructor(protected readonly authService: CoreAuthService) {}
|
|
22
|
+
constructor(protected readonly authService: CoreAuthService, protected readonly configService: ConfigService) {}
|
|
15
23
|
|
|
16
24
|
// ===========================================================================
|
|
17
|
-
//
|
|
25
|
+
// Mutations
|
|
18
26
|
// ===========================================================================
|
|
19
27
|
|
|
20
28
|
/**
|
|
21
|
-
*
|
|
29
|
+
* Sign in user via email and password (on specific device)
|
|
22
30
|
*/
|
|
23
|
-
@
|
|
31
|
+
@Mutation((returns) => CoreAuthModel, {
|
|
32
|
+
description: 'Sign in user via email and password and get JWT tokens (for specific device)',
|
|
33
|
+
})
|
|
24
34
|
async signIn(
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
@
|
|
28
|
-
): Promise<
|
|
29
|
-
|
|
35
|
+
@Info() info: GraphQLResolveInfo,
|
|
36
|
+
@Context() ctx: { res: ResponseType },
|
|
37
|
+
@Args('input') input: CoreAuthSignInInput
|
|
38
|
+
): Promise<CoreAuthModel> {
|
|
39
|
+
const result = await this.authService.signIn(input, { fieldSelection: { info, select: 'signIn' } });
|
|
40
|
+
return this.processCookies(ctx, result);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Logout user (from specific device)
|
|
45
|
+
*/
|
|
46
|
+
@Mutation((returns) => CoreAuthModel, { description: 'Logout user (from specific device)' })
|
|
47
|
+
async logout(
|
|
48
|
+
@GraphQLUser() currentUser: ICoreAuthUser,
|
|
49
|
+
@Context() ctx: { res: ResponseType },
|
|
50
|
+
@Args('deviceId', { nullable: true }) deviceId?: string
|
|
51
|
+
): Promise<boolean> {
|
|
52
|
+
const result = await this.authService.logout({ currentUser, deviceId });
|
|
53
|
+
return this.processCookies(ctx, result);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Refresh token (for specific device)
|
|
58
|
+
*/
|
|
59
|
+
@UseGuards(AuthGuard('jwt-refresh'))
|
|
60
|
+
@Mutation((returns) => CoreAuthModel, { description: 'Refresh tokens (for specific device)' })
|
|
61
|
+
async refreshToken(
|
|
62
|
+
@GraphQLUser() user: ICoreAuthUser,
|
|
63
|
+
@Context() ctx: { res: ResponseType },
|
|
64
|
+
@Args('deviceId', { nullable: true }) deviceId?: string
|
|
65
|
+
): Promise<CoreAuthModel> {
|
|
66
|
+
const result = await this.authService.refreshTokens(user, deviceId);
|
|
67
|
+
return this.processCookies(ctx, result);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Register a new user account (on specific device)
|
|
72
|
+
*/
|
|
73
|
+
@Mutation((returns) => CoreAuthModel, { description: 'Register a new user account (on specific device)' })
|
|
74
|
+
async signUp(
|
|
75
|
+
@Info() info: GraphQLResolveInfo,
|
|
76
|
+
@Context() ctx: { res: ResponseType },
|
|
77
|
+
@Args('input') input: CoreAuthSignUpInput
|
|
78
|
+
): Promise<CoreAuthModel> {
|
|
79
|
+
const result = await this.authService.signUp(input, { fieldSelection: { info, select: 'signUp' } });
|
|
80
|
+
return this.processCookies(ctx, result);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ===================================================================================================================
|
|
84
|
+
// Helper
|
|
85
|
+
// ===================================================================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Process cookies
|
|
89
|
+
*/
|
|
90
|
+
protected processCookies(ctx: { res: ResponseType }, result: any) {
|
|
91
|
+
// Check if cookie handling is activated
|
|
92
|
+
if (this.configService.getFastButReadOnly('cookies')) {
|
|
93
|
+
// Set cookies
|
|
94
|
+
if (typeof result !== 'object') {
|
|
95
|
+
ctx.res.cookie('token', '', { httpOnly: true });
|
|
96
|
+
ctx.res.cookie('refreshToken', '', { httpOnly: true });
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
ctx.res.cookie('token', result?.token || '', { httpOnly: true });
|
|
100
|
+
ctx.res.cookie('refreshToken', result?.refreshToken || '', { httpOnly: true });
|
|
101
|
+
|
|
102
|
+
// Remove tokens from result
|
|
103
|
+
if (result.token) {
|
|
104
|
+
delete result.token;
|
|
105
|
+
}
|
|
106
|
+
if (result.refreshToken) {
|
|
107
|
+
delete result.refreshToken;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Return prepared result
|
|
112
|
+
return result;
|
|
30
113
|
}
|
|
31
114
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CanActivate, ExecutionContext, Logger, mixin, Optional, UnauthorizedException } from '@nestjs/common';
|
|
2
|
+
import { GqlExecutionContext } from '@nestjs/graphql';
|
|
2
3
|
import { AuthModuleOptions, Type } from '@nestjs/passport';
|
|
3
4
|
import { defaultOptions } from '@nestjs/passport/dist/options';
|
|
4
5
|
import { memoize } from '@nestjs/passport/dist/utils/memoize.util';
|
|
@@ -65,10 +66,7 @@ function createAuthGuard(type?: string): Type<CanActivate> {
|
|
|
65
66
|
|
|
66
67
|
const options = { ...defaultOptions, ...this.options };
|
|
67
68
|
const response = context?.switchToHttp()?.getResponse();
|
|
68
|
-
|
|
69
|
-
if (!request) {
|
|
70
|
-
request = context?.switchToHttp()?.getRequest();
|
|
71
|
-
}
|
|
69
|
+
const request = this.getRequest(context);
|
|
72
70
|
const passportFn = createPassportContext(request, response);
|
|
73
71
|
const user = await passportFn(type || this.options.defaultStrategy, options, (err, currentUser, info) =>
|
|
74
72
|
this.handleRequest(err, currentUser, info, context)
|
|
@@ -81,6 +79,15 @@ function createAuthGuard(type?: string): Type<CanActivate> {
|
|
|
81
79
|
* Prepare request
|
|
82
80
|
*/
|
|
83
81
|
getRequest<T = any>(context: ExecutionContext): T {
|
|
82
|
+
// Try to get request GraphQL context
|
|
83
|
+
try {
|
|
84
|
+
const ctx = GqlExecutionContext.create(context)?.getContext();
|
|
85
|
+
if (ctx?.req) {
|
|
86
|
+
return ctx.req;
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {}
|
|
89
|
+
|
|
90
|
+
// Else return HTTP request
|
|
84
91
|
return context && context.switchToHttp() ? context.switchToHttp().getRequest() : null;
|
|
85
92
|
}
|
|
86
93
|
|
|
@@ -110,4 +117,4 @@ function createAuthGuard(type?: string): Type<CanActivate> {
|
|
|
110
117
|
/**
|
|
111
118
|
* Export AuthGuard
|
|
112
119
|
*/
|
|
113
|
-
export const AuthGuard: (type?: string) => Type<IAuthGuard> = memoize(createAuthGuard);
|
|
120
|
+
export const AuthGuard: (type?: string | string[]) => Type<IAuthGuard> = memoize(createAuthGuard);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExecutionContext,
|
|
1
|
+
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
2
2
|
import { Reflector } from '@nestjs/core';
|
|
3
3
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
|
4
4
|
import { RoleEnum } from '../../../common/enums/role.enum';
|
|
@@ -10,6 +10,9 @@ export class CoreAuthSignInInput extends CoreInput {
|
|
|
10
10
|
// Properties
|
|
11
11
|
// ===================================================================================================================
|
|
12
12
|
|
|
13
|
+
@Field({ description: 'Device ID', nullable: true })
|
|
14
|
+
deviceId?: string = undefined;
|
|
15
|
+
|
|
13
16
|
@Field({ description: 'Email', nullable: false })
|
|
14
17
|
email: string = undefined;
|
|
15
18
|
|
|
@@ -10,6 +10,9 @@ export class CoreAuthSignUpInput extends CoreInput {
|
|
|
10
10
|
// Properties
|
|
11
11
|
// ===================================================================================================================
|
|
12
12
|
|
|
13
|
+
@Field({ description: 'Device ID', nullable: true })
|
|
14
|
+
deviceId?: string = undefined;
|
|
15
|
+
|
|
13
16
|
@Field({ description: 'Email', nullable: false })
|
|
14
17
|
email: string = undefined;
|
|
15
18
|
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Interface for user used in authorization module
|
|
3
3
|
*/
|
|
4
4
|
export interface ICoreAuthUser {
|
|
5
|
+
/**
|
|
6
|
+
* ID of the user
|
|
7
|
+
*/
|
|
8
|
+
id: string;
|
|
9
|
+
|
|
5
10
|
/**
|
|
6
11
|
* Email of the user
|
|
7
12
|
*/
|
|
@@ -11,4 +16,14 @@ export interface ICoreAuthUser {
|
|
|
11
16
|
* Password of the user
|
|
12
17
|
*/
|
|
13
18
|
password: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Refresh token
|
|
22
|
+
*/
|
|
23
|
+
refreshToken?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Refresh tokens for different devices
|
|
27
|
+
*/
|
|
28
|
+
refreshTokens?: Record<string, string>;
|
|
14
29
|
}
|
|
@@ -5,6 +5,16 @@ import { ICoreAuthUser } from '../interfaces/core-auth-user.interface';
|
|
|
5
5
|
* Abstract class for user service in authorization module
|
|
6
6
|
*/
|
|
7
7
|
export abstract class CoreAuthUserService {
|
|
8
|
+
/**
|
|
9
|
+
* Create user
|
|
10
|
+
*/
|
|
11
|
+
abstract create(input: any, serviceOptions?: ServiceOptions): Promise<ICoreAuthUser>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get user via ID
|
|
15
|
+
*/
|
|
16
|
+
abstract get(id: string, serviceOptions?: ServiceOptions): Promise<ICoreAuthUser>;
|
|
17
|
+
|
|
8
18
|
/**
|
|
9
19
|
* Get user via email
|
|
10
20
|
*/
|
|
@@ -14,4 +24,9 @@ export abstract class CoreAuthUserService {
|
|
|
14
24
|
* Prepare output
|
|
15
25
|
*/
|
|
16
26
|
abstract prepareOutput(output: any, options?: ServiceOptions): Promise<ICoreAuthUser>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Update user
|
|
30
|
+
*/
|
|
31
|
+
abstract update(id: string, input: any, serviceOptions?: ServiceOptions): Promise<ICoreAuthUser>;
|
|
17
32
|
}
|
|
@@ -2,7 +2,13 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
|
2
2
|
import { JwtService } from '@nestjs/jwt';
|
|
3
3
|
import * as bcrypt from 'bcrypt';
|
|
4
4
|
import { sha256 } from 'js-sha256';
|
|
5
|
+
import { getStringIds } from '../../../common/helpers/db.helper';
|
|
6
|
+
import { prepareServiceOptions } from '../../../common/helpers/service.helper';
|
|
5
7
|
import { ServiceOptions } from '../../../common/interfaces/service-options.interface';
|
|
8
|
+
import { ConfigService } from '../../../common/services/config.service';
|
|
9
|
+
import { CoreAuthModel } from '../core-auth.model';
|
|
10
|
+
import { CoreAuthSignInInput } from '../inputs/core-auth-sign-in.input';
|
|
11
|
+
import { CoreAuthSignUpInput } from '../inputs/core-auth-sign-up.input';
|
|
6
12
|
import { ICoreAuthUser } from '../interfaces/core-auth-user.interface';
|
|
7
13
|
import { JwtPayload } from '../interfaces/jwt-payload.interface';
|
|
8
14
|
import { CoreAuthUserService } from './core-auth-user.service';
|
|
@@ -12,18 +18,82 @@ import { CoreAuthUserService } from './core-auth-user.service';
|
|
|
12
18
|
*/
|
|
13
19
|
@Injectable()
|
|
14
20
|
export class CoreAuthService {
|
|
15
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Integrate services
|
|
23
|
+
*/
|
|
24
|
+
constructor(
|
|
25
|
+
protected readonly userService: CoreAuthUserService,
|
|
26
|
+
protected readonly jwtService: JwtService,
|
|
27
|
+
protected readonly configService: ConfigService
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Decode JWT
|
|
32
|
+
*/
|
|
33
|
+
decodeJwt(token: string): JwtPayload {
|
|
34
|
+
return this.jwtService.decode(token) as JwtPayload;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Logout user (from device)
|
|
39
|
+
*/
|
|
40
|
+
async logout(serviceOptions: ServiceOptions & { deviceId?: string }): Promise<boolean> {
|
|
41
|
+
const user = serviceOptions.currentUser;
|
|
42
|
+
if (!serviceOptions.currentUser) {
|
|
43
|
+
throw new UnauthorizedException();
|
|
44
|
+
}
|
|
45
|
+
const deviceId = serviceOptions.deviceId;
|
|
46
|
+
if (deviceId) {
|
|
47
|
+
if (!user.refreshTokens[deviceId]) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
delete user.refreshTokens[deviceId];
|
|
51
|
+
await this.userService.update(user.id, { refreshTokens: user.refreshTokens }, serviceOptions);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
user.refreshToken = null;
|
|
55
|
+
user.refreshTokens = {};
|
|
56
|
+
await this.userService.update(
|
|
57
|
+
user.id,
|
|
58
|
+
{
|
|
59
|
+
refreshToken: user.refreshToken,
|
|
60
|
+
refreshTokens: user.refreshTokens,
|
|
61
|
+
},
|
|
62
|
+
serviceOptions
|
|
63
|
+
);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Refresh tokens
|
|
69
|
+
*/
|
|
70
|
+
async refreshTokens(user: ICoreAuthUser, deviceId?: string) {
|
|
71
|
+
// Create new tokens
|
|
72
|
+
const tokens = await this.getTokens(user.id);
|
|
73
|
+
await this.updateRefreshToken(user, tokens.refreshToken, { deviceId });
|
|
74
|
+
|
|
75
|
+
// Return
|
|
76
|
+
return CoreAuthModel.map({
|
|
77
|
+
...tokens,
|
|
78
|
+
user: await this.userService.prepareOutput(user),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
16
81
|
|
|
17
82
|
/**
|
|
18
83
|
* User sign in via email
|
|
19
84
|
*/
|
|
20
|
-
async signIn(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
85
|
+
async signIn(input: CoreAuthSignInInput, serviceOptions?: ServiceOptions): Promise<CoreAuthModel> {
|
|
86
|
+
// Prepare service options
|
|
87
|
+
const serviceOptionsForUserService = prepareServiceOptions(serviceOptions, {
|
|
88
|
+
// We need password, so we can't use prepare output handling and have to deactivate it
|
|
89
|
+
prepareOutput: null,
|
|
90
|
+
|
|
91
|
+
// Select user field for automatic populate handling via user service
|
|
92
|
+
subFieldSelection: 'user',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Inputs
|
|
96
|
+
const { email, password, deviceId } = input;
|
|
27
97
|
|
|
28
98
|
// Get user
|
|
29
99
|
const user = await this.userService.getViaEmail(email, serviceOptions);
|
|
@@ -34,25 +104,153 @@ export class CoreAuthService {
|
|
|
34
104
|
throw new UnauthorizedException();
|
|
35
105
|
}
|
|
36
106
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
107
|
+
// Set device ID
|
|
108
|
+
serviceOptionsForUserService.deviceId = input.deviceId;
|
|
109
|
+
|
|
110
|
+
// Return tokens and user
|
|
111
|
+
return this.getResult(user, serviceOptions);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Register a new user account
|
|
116
|
+
*/
|
|
117
|
+
async signUp(input: CoreAuthSignUpInput, serviceOptions?: ServiceOptions): Promise<CoreAuthModel> {
|
|
118
|
+
// Prepare service options
|
|
119
|
+
const serviceOptionsForUserService = prepareServiceOptions(serviceOptions, {
|
|
120
|
+
// Select user field for automatic populate handling via user service
|
|
121
|
+
subFieldSelection: 'user',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Get and check user
|
|
125
|
+
const user = await this.userService.create(input, serviceOptionsForUserService);
|
|
126
|
+
if (!user) {
|
|
127
|
+
throw Error('Email Address already in use');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Set device ID
|
|
131
|
+
serviceOptionsForUserService.deviceId = input.deviceId;
|
|
132
|
+
|
|
133
|
+
// Return tokens and user
|
|
134
|
+
return this.getResult(user, serviceOptionsForUserService);
|
|
43
135
|
}
|
|
44
136
|
|
|
45
137
|
/**
|
|
46
138
|
* Validate user
|
|
47
139
|
*/
|
|
48
140
|
async validateUser(payload: JwtPayload): Promise<any> {
|
|
49
|
-
|
|
141
|
+
// Get user
|
|
142
|
+
const user = await this.userService.get(payload.id);
|
|
143
|
+
|
|
144
|
+
// Check if user exists and is logged in
|
|
145
|
+
if (!user?.refreshToken) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Return user
|
|
150
|
+
return user;
|
|
50
151
|
}
|
|
51
152
|
|
|
153
|
+
// ===================================================================================================================
|
|
154
|
+
// Helper
|
|
155
|
+
// ===================================================================================================================
|
|
156
|
+
|
|
52
157
|
/**
|
|
53
|
-
*
|
|
158
|
+
* Rest result with user and tokens
|
|
54
159
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
160
|
+
protected async getResult(user: ICoreAuthUser, serviceOptions: ServiceOptions & { deviceId?: string }) {
|
|
161
|
+
// Create new tokens
|
|
162
|
+
const tokens = await this.getTokens(user.id);
|
|
163
|
+
|
|
164
|
+
// Set refresh token
|
|
165
|
+
await this.updateRefreshToken(user, tokens.refreshToken, serviceOptions);
|
|
166
|
+
|
|
167
|
+
// Return tokens and user
|
|
168
|
+
return CoreAuthModel.map({
|
|
169
|
+
...tokens,
|
|
170
|
+
user: await this.userService.prepareOutput(user),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get secret from JWT or refresh config
|
|
176
|
+
*/
|
|
177
|
+
protected getSecretFromConfig(refresh?: boolean) {
|
|
178
|
+
let path = 'jwt';
|
|
179
|
+
if (refresh) {
|
|
180
|
+
path += '.refresh';
|
|
181
|
+
}
|
|
182
|
+
return (
|
|
183
|
+
this.configService.getFastButReadOnly(path + '.signInOptions.secret') ||
|
|
184
|
+
this.configService.getFastButReadOnly(path + '.signInOptions.secretOrPrivateKey') ||
|
|
185
|
+
this.configService.getFastButReadOnly(path + '.secret') ||
|
|
186
|
+
this.configService.getFastButReadOnly(path + '.secretOrPrivateKey')
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get JWT and refresh token
|
|
192
|
+
*/
|
|
193
|
+
protected async getTokens(userId: string) {
|
|
194
|
+
const [token, refreshToken] = await Promise.all([
|
|
195
|
+
this.jwtService.signAsync(
|
|
196
|
+
{ id: userId },
|
|
197
|
+
{
|
|
198
|
+
secret: this.getSecretFromConfig(false),
|
|
199
|
+
...this.configService.getFastButReadOnly('jwt.signInOptions', {}),
|
|
200
|
+
}
|
|
201
|
+
),
|
|
202
|
+
this.jwtService.signAsync(
|
|
203
|
+
{ id: userId },
|
|
204
|
+
{
|
|
205
|
+
secret: this.getSecretFromConfig(true),
|
|
206
|
+
...this.configService.getFastButReadOnly('jwt.refresh.signInOptions', {}),
|
|
207
|
+
}
|
|
208
|
+
),
|
|
209
|
+
]);
|
|
210
|
+
return {
|
|
211
|
+
token,
|
|
212
|
+
refreshToken,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Update refresh token(s)
|
|
218
|
+
*/
|
|
219
|
+
protected async updateRefreshToken(
|
|
220
|
+
user: ICoreAuthUser,
|
|
221
|
+
refreshToken: string,
|
|
222
|
+
serviceOptions: ServiceOptions & { deviceId?: string } = {}
|
|
223
|
+
) {
|
|
224
|
+
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
|
225
|
+
const deviceId = serviceOptions?.deviceId;
|
|
226
|
+
if (deviceId) {
|
|
227
|
+
if (!user.refreshTokens) {
|
|
228
|
+
user.refreshTokens = {};
|
|
229
|
+
}
|
|
230
|
+
user.refreshTokens[deviceId] = hashedRefreshToken;
|
|
231
|
+
|
|
232
|
+
// Refresh token must be set even if only a specific device is logged in, because of the check in the validateUser method
|
|
233
|
+
if (!user.refreshToken) {
|
|
234
|
+
user.refreshToken = hashedRefreshToken;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return await this.userService.update(
|
|
238
|
+
getStringIds(user),
|
|
239
|
+
{ refreshTokens: user.refreshTokens, refreshToken: user.refreshToken },
|
|
240
|
+
{
|
|
241
|
+
...serviceOptions,
|
|
242
|
+
force: true,
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
user.refreshToken = hashedRefreshToken;
|
|
247
|
+
return await this.userService.update(
|
|
248
|
+
getStringIds(user),
|
|
249
|
+
{ refreshToken: hashedRefreshToken },
|
|
250
|
+
{
|
|
251
|
+
...serviceOptions,
|
|
252
|
+
force: true,
|
|
253
|
+
}
|
|
254
|
+
);
|
|
57
255
|
}
|
|
58
256
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
2
|
+
import { PassportStrategy } from '@nestjs/passport';
|
|
3
|
+
import * as bcrypt from 'bcrypt';
|
|
4
|
+
import { Request as RequestType, Request } from 'express';
|
|
5
|
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
6
|
+
import { ConfigService } from '../../../common/services/config.service';
|
|
7
|
+
import { CoreAuthService } from '../services/core-auth.service';
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
|
11
|
+
constructor(protected readonly authService: CoreAuthService, protected readonly configService: ConfigService) {
|
|
12
|
+
super({
|
|
13
|
+
jwtFromRequest: ExtractJwt.fromExtractors([
|
|
14
|
+
JwtRefreshStrategy.extractJWTFromCookie,
|
|
15
|
+
ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
16
|
+
]),
|
|
17
|
+
privateKey: configService.get('jwt.refresh.privateKey'),
|
|
18
|
+
publicKey: configService.get('jwt.refresh.publicKey'),
|
|
19
|
+
secret: configService.get('jwt.refresh.secret') || configService.get('jwt.refresh.secretOrPrivateKey'),
|
|
20
|
+
secretOrKey: configService.get('jwt.refresh.secretOrPrivateKey') || configService.get('jwt.refresh.secret'),
|
|
21
|
+
secretOrKeyProvider: configService.get('jwt.refresh.secretOrKeyProvider'),
|
|
22
|
+
passReqToCallback: true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract JWT from cookie
|
|
28
|
+
*/
|
|
29
|
+
private static extractJWTFromCookie(req: RequestType): string | null {
|
|
30
|
+
return req?.cookies?.refreshToken || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validate user via JWT payload
|
|
35
|
+
*/
|
|
36
|
+
async validate(req: Request, payload: any) {
|
|
37
|
+
// Check user
|
|
38
|
+
const user = await this.authService.validateUser(payload);
|
|
39
|
+
if (!user) {
|
|
40
|
+
throw new UnauthorizedException();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check refresh token
|
|
44
|
+
const refreshToken = req
|
|
45
|
+
.get('Authorization')
|
|
46
|
+
.replace(/bearer/i, '')
|
|
47
|
+
.trim();
|
|
48
|
+
const refreshTokenMatches = await bcrypt.compare(refreshToken, user.refreshToken);
|
|
49
|
+
if (!refreshTokenMatches) {
|
|
50
|
+
throw new ForbiddenException('Access Denied');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Return user
|
|
54
|
+
return user;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
2
2
|
import { PassportStrategy } from '@nestjs/passport';
|
|
3
3
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
4
|
-
import { ConfigService } from '
|
|
5
|
-
import { JwtPayload } from '
|
|
6
|
-
import { CoreAuthService } from '
|
|
4
|
+
import { ConfigService } from '../../../common/services/config.service';
|
|
5
|
+
import { JwtPayload } from '../interfaces/jwt-payload.interface';
|
|
6
|
+
import { CoreAuthService } from '../services/core-auth.service';
|
|
7
|
+
import { Request as RequestType } from 'express';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Use JWT strategy for passport
|
|
10
11
|
*/
|
|
11
12
|
@Injectable()
|
|
12
|
-
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
13
|
+
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|
13
14
|
/**
|
|
14
15
|
* Init JWT strategy
|
|
15
16
|
*/
|
|
16
17
|
constructor(protected readonly authService: CoreAuthService, protected readonly configService: ConfigService) {
|
|
17
18
|
super({
|
|
18
|
-
jwtFromRequest: ExtractJwt.
|
|
19
|
+
jwtFromRequest: ExtractJwt.fromExtractors([
|
|
20
|
+
JwtStrategy.extractJWTFromCookie,
|
|
21
|
+
ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
22
|
+
]),
|
|
19
23
|
privateKey: configService.get('jwt.privateKey'),
|
|
20
24
|
publicKey: configService.get('jwt.publicKey'),
|
|
21
25
|
secret: configService.get('jwt.secret') || configService.get('jwt.secretOrPrivateKey'),
|
|
@@ -24,6 +28,13 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
|
24
28
|
});
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Extract JWT from cookie
|
|
33
|
+
*/
|
|
34
|
+
private static extractJWTFromCookie(req: RequestType): string | null {
|
|
35
|
+
return req?.cookies?.token || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
/**
|
|
28
39
|
* Validate user via JWT payload
|
|
29
40
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Field, ObjectType } from '@nestjs/graphql';
|
|
2
|
-
import { Prop, Schema as MongooseSchema } from '@nestjs/mongoose';
|
|
2
|
+
import { Prop, raw, Schema as MongooseSchema } from '@nestjs/mongoose';
|
|
3
3
|
import { IsEmail, IsOptional } from 'class-validator';
|
|
4
4
|
import { Document } from 'mongoose';
|
|
5
5
|
import { User } from '../../../server/modules/user/user.model';
|
|
@@ -70,6 +70,22 @@ export abstract class CoreUserModel extends CorePersistenceModel {
|
|
|
70
70
|
@Prop()
|
|
71
71
|
passwordResetToken: string = undefined;
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Hashed refresh JWT
|
|
75
|
+
*/
|
|
76
|
+
@IsOptional()
|
|
77
|
+
@Prop()
|
|
78
|
+
refreshToken: string = undefined;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Refresh tokens for devices
|
|
82
|
+
* key: deviceID
|
|
83
|
+
* value: hashed JWT
|
|
84
|
+
*/
|
|
85
|
+
@IsOptional()
|
|
86
|
+
@Prop(raw({}))
|
|
87
|
+
refreshTokens: Record<string, string> = undefined;
|
|
88
|
+
|
|
73
89
|
/**
|
|
74
90
|
* Verification token of the user
|
|
75
91
|
*/
|