@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.
Files changed (117) hide show
  1. package/dist/config.env.js +41 -2
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/filters/http-exception-log.filter.d.ts +4 -0
  4. package/dist/core/common/filters/http-exception-log.filter.js +30 -0
  5. package/dist/core/common/filters/http-exception-log.filter.js.map +1 -0
  6. package/dist/core/common/interceptors/check-security.interceptor.d.ts +5 -0
  7. package/dist/core/common/interceptors/check-security.interceptor.js +47 -0
  8. package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -0
  9. package/dist/core/common/interfaces/server-options.interface.d.ts +16 -6
  10. package/dist/core/common/models/core-model.model.d.ts +1 -0
  11. package/dist/core/common/models/core-model.model.js +3 -0
  12. package/dist/core/common/models/core-model.model.js.map +1 -1
  13. package/dist/core/common/plugins/complexity.plugin.d.ts +9 -0
  14. package/dist/core/common/plugins/complexity.plugin.js +47 -0
  15. package/dist/core/common/plugins/complexity.plugin.js.map +1 -0
  16. package/dist/core/common/plugins/mongoose-id.plugin.d.ts +1 -2
  17. package/dist/core/common/plugins/mongoose-id.plugin.js +7 -2
  18. package/dist/core/common/plugins/mongoose-id.plugin.js.map +1 -1
  19. package/dist/core/common/services/config.service.d.ts +4 -4
  20. package/dist/core/common/services/config.service.js.map +1 -1
  21. package/dist/core/common/services/module.service.js +2 -2
  22. package/dist/core/common/services/module.service.js.map +1 -1
  23. package/dist/core/modules/auth/core-auth.model.d.ts +4 -1
  24. package/dist/core/modules/auth/core-auth.model.js +12 -1
  25. package/dist/core/modules/auth/core-auth.model.js.map +1 -1
  26. package/dist/core/modules/auth/core-auth.module.d.ts +3 -1
  27. package/dist/core/modules/auth/core-auth.module.js +7 -2
  28. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  29. package/dist/core/modules/auth/core-auth.resolver.d.ts +22 -2
  30. package/dist/core/modules/auth/core-auth.resolver.js +77 -9
  31. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  32. package/dist/core/modules/auth/guards/auth.guard.d.ts +1 -1
  33. package/dist/core/modules/auth/guards/auth.guard.js +9 -4
  34. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  35. package/dist/core/modules/auth/guards/refresh-token.guard.d.ts +4 -0
  36. package/dist/core/modules/auth/guards/refresh-token.guard.js +18 -0
  37. package/dist/core/modules/auth/guards/refresh-token.guard.js.map +1 -0
  38. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  39. package/dist/core/modules/auth/inputs/core-auth-sign-in.input.d.ts +1 -0
  40. package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js +5 -0
  41. package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js.map +1 -1
  42. package/dist/core/modules/auth/inputs/core-auth-sign-up.input.d.ts +1 -0
  43. package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js +5 -0
  44. package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js.map +1 -1
  45. package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +3 -0
  46. package/dist/core/modules/auth/interfaces/jwt-payload.interface.d.ts +1 -1
  47. package/dist/core/modules/auth/services/core-auth-user.service.d.ts +3 -0
  48. package/dist/core/modules/auth/services/core-auth-user.service.js.map +1 -1
  49. package/dist/core/modules/auth/services/core-auth.service.d.ts +23 -5
  50. package/dist/core/modules/auth/services/core-auth.service.js +121 -13
  51. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  52. package/dist/core/modules/auth/strategies/jwt-refresh.strategy.d.ts +12 -0
  53. package/dist/core/modules/auth/strategies/jwt-refresh.strategy.js +61 -0
  54. package/dist/core/modules/auth/strategies/jwt-refresh.strategy.js.map +1 -0
  55. package/dist/core/modules/auth/{jwt.strategy.d.ts → strategies/jwt.strategy.d.ts} +4 -3
  56. package/dist/core/modules/auth/{jwt.strategy.js → strategies/jwt.strategy.js} +12 -5
  57. package/dist/core/modules/auth/strategies/jwt.strategy.js.map +1 -0
  58. package/dist/core/modules/user/core-user.model.d.ts +2 -0
  59. package/dist/core/modules/user/core-user.model.js +12 -0
  60. package/dist/core/modules/user/core-user.model.js.map +1 -1
  61. package/dist/core.module.js +12 -2
  62. package/dist/core.module.js.map +1 -1
  63. package/dist/index.d.ts +6 -1
  64. package/dist/index.js +6 -1
  65. package/dist/index.js.map +1 -1
  66. package/dist/main.js +23 -0
  67. package/dist/main.js.map +1 -1
  68. package/dist/server/modules/auth/auth.model.js.map +1 -1
  69. package/dist/server/modules/auth/auth.resolver.d.ts +13 -5
  70. package/dist/server/modules/auth/auth.resolver.js +21 -12
  71. package/dist/server/modules/auth/auth.resolver.js.map +1 -1
  72. package/dist/server/modules/auth/auth.service.d.ts +2 -1
  73. package/dist/server/modules/auth/auth.service.js +7 -48
  74. package/dist/server/modules/auth/auth.service.js.map +1 -1
  75. package/dist/server/modules/file/file.module.js +3 -3
  76. package/dist/server/modules/file/file.module.js.map +1 -1
  77. package/dist/server/modules/user/user.model.d.ts +1 -0
  78. package/dist/server/modules/user/user.model.js +19 -0
  79. package/dist/server/modules/user/user.model.js.map +1 -1
  80. package/dist/server/server.module.js +12 -1
  81. package/dist/server/server.module.js.map +1 -1
  82. package/dist/tsconfig.build.tsbuildinfo +1 -1
  83. package/package.json +32 -27
  84. package/src/config.env.ts +41 -2
  85. package/src/core/common/filters/http-exception-log.filter.ts +27 -0
  86. package/src/core/common/interceptors/check-security.interceptor.ts +51 -0
  87. package/src/core/common/interfaces/server-options.interface.ts +67 -30
  88. package/src/core/common/models/core-model.model.ts +7 -0
  89. package/src/core/common/plugins/complexity.plugin.ts +31 -0
  90. package/src/core/common/plugins/mongoose-id.plugin.js +4 -2
  91. package/src/core/common/services/config.service.ts +4 -4
  92. package/src/core/common/services/module.service.ts +2 -2
  93. package/src/core/modules/auth/core-auth.model.ts +15 -2
  94. package/src/core/modules/auth/core-auth.module.ts +8 -2
  95. package/src/core/modules/auth/core-auth.resolver.ts +93 -10
  96. package/src/core/modules/auth/guards/auth.guard.ts +12 -5
  97. package/src/core/modules/auth/guards/refresh-token.guard.ts +5 -0
  98. package/src/core/modules/auth/guards/roles.guard.ts +1 -1
  99. package/src/core/modules/auth/inputs/core-auth-sign-in.input.ts +3 -0
  100. package/src/core/modules/auth/inputs/core-auth-sign-up.input.ts +3 -0
  101. package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +15 -0
  102. package/src/core/modules/auth/interfaces/jwt-payload.interface.ts +1 -1
  103. package/src/core/modules/auth/services/core-auth-user.service.ts +15 -0
  104. package/src/core/modules/auth/services/core-auth.service.ts +216 -18
  105. package/src/core/modules/auth/strategies/jwt-refresh.strategy.ts +56 -0
  106. package/src/core/modules/auth/{jwt.strategy.ts → strategies/jwt.strategy.ts} +16 -5
  107. package/src/core/modules/user/core-user.model.ts +17 -1
  108. package/src/core.module.ts +14 -2
  109. package/src/index.ts +6 -1
  110. package/src/main.ts +29 -0
  111. package/src/server/modules/auth/auth.model.ts +1 -1
  112. package/src/server/modules/auth/auth.resolver.ts +26 -8
  113. package/src/server/modules/auth/auth.service.ts +20 -61
  114. package/src/server/modules/file/file.module.ts +3 -3
  115. package/src/server/modules/user/user.model.ts +29 -0
  116. package/src/server/server.module.ts +12 -1
  117. package/dist/core/modules/auth/jwt.strategy.js.map +0 -1
@@ -1,6 +1,14 @@
1
- import { Args, Info, Query, Resolver } from '@nestjs/graphql';
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
- // Queries
25
+ // Mutations
18
26
  // ===========================================================================
19
27
 
20
28
  /**
21
- * Get user via ID
29
+ * Sign in user via email and password (on specific device)
22
30
  */
23
- @Query((returns) => CoreAuthModel, { description: 'Get JWT token' })
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
- @Args('email') email: string,
26
- @Args('password') password: string,
27
- @Info() info: GraphQLResolveInfo
28
- ): Promise<Partial<CoreAuthModel>> {
29
- return await this.authService.signIn(email, password, { fieldSelection: { info, select: 'signIn' } });
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
- let request = this.getRequest(context);
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);
@@ -0,0 +1,5 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { AuthGuard } from './auth.guard';
3
+
4
+ @Injectable()
5
+ export class RefreshTokenGuard extends AuthGuard('jwt-refresh') {}
@@ -1,4 +1,4 @@
1
- import { ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
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
  }
@@ -2,5 +2,5 @@
2
2
  * Interface for jwt payload
3
3
  */
4
4
  export interface JwtPayload {
5
- email: string;
5
+ id: string;
6
6
  }
@@ -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
- constructor(protected readonly userService: CoreAuthUserService, protected readonly jwtService: JwtService) {}
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
- email: string,
22
- password: string,
23
- serviceOptions?: ServiceOptions
24
- ): Promise<{ token: string; user: ICoreAuthUser }> {
25
- serviceOptions = serviceOptions || {};
26
- serviceOptions.prepareOutput = null;
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
- // Return JWT
38
- const payload: JwtPayload = { email: user.email };
39
- return {
40
- token: this.jwtService.sign(payload),
41
- user: await this.userService.prepareOutput(user),
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
- return await this.userService.getViaEmail(payload.email);
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
- * Decode JWT
158
+ * Rest result with user and tokens
54
159
  */
55
- decodeJwt(token: string): JwtPayload {
56
- return this.jwtService.decode(token) as JwtPayload;
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 '../../common/services/config.service';
5
- import { JwtPayload } from './interfaces/jwt-payload.interface';
6
- import { CoreAuthService } from './services/core-auth.service';
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.fromAuthHeaderAsBearerToken(),
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
  */