@lenne.tech/nest-server 9.0.31 → 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 (135) 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/helpers/db.helper.d.ts +6 -2
  7. package/dist/core/common/helpers/db.helper.js +9 -4
  8. package/dist/core/common/helpers/db.helper.js.map +1 -1
  9. package/dist/core/common/helpers/input.helper.d.ts +3 -0
  10. package/dist/core/common/helpers/input.helper.js +30 -1
  11. package/dist/core/common/helpers/input.helper.js.map +1 -1
  12. package/dist/core/common/interceptors/check-security.interceptor.d.ts +5 -0
  13. package/dist/core/common/interceptors/check-security.interceptor.js +47 -0
  14. package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -0
  15. package/dist/core/common/interfaces/server-options.interface.d.ts +17 -6
  16. package/dist/core/common/interfaces/service-options.interface.d.ts +1 -0
  17. package/dist/core/common/models/core-model.model.d.ts +1 -0
  18. package/dist/core/common/models/core-model.model.js +3 -0
  19. package/dist/core/common/models/core-model.model.js.map +1 -1
  20. package/dist/core/common/plugins/complexity.plugin.d.ts +9 -0
  21. package/dist/core/common/plugins/complexity.plugin.js +47 -0
  22. package/dist/core/common/plugins/complexity.plugin.js.map +1 -0
  23. package/dist/core/common/plugins/mongoose-id.plugin.d.ts +1 -2
  24. package/dist/core/common/plugins/mongoose-id.plugin.js +7 -2
  25. package/dist/core/common/plugins/mongoose-id.plugin.js.map +1 -1
  26. package/dist/core/common/services/config.service.d.ts +4 -4
  27. package/dist/core/common/services/config.service.js.map +1 -1
  28. package/dist/core/common/services/module.service.d.ts +5 -1
  29. package/dist/core/common/services/module.service.js +15 -3
  30. package/dist/core/common/services/module.service.js.map +1 -1
  31. package/dist/core/modules/auth/core-auth.model.d.ts +4 -1
  32. package/dist/core/modules/auth/core-auth.model.js +12 -1
  33. package/dist/core/modules/auth/core-auth.model.js.map +1 -1
  34. package/dist/core/modules/auth/core-auth.module.d.ts +3 -1
  35. package/dist/core/modules/auth/core-auth.module.js +7 -2
  36. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  37. package/dist/core/modules/auth/core-auth.resolver.d.ts +22 -2
  38. package/dist/core/modules/auth/core-auth.resolver.js +77 -9
  39. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  40. package/dist/core/modules/auth/guards/auth.guard.d.ts +1 -1
  41. package/dist/core/modules/auth/guards/auth.guard.js +9 -4
  42. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  43. package/dist/core/modules/auth/guards/refresh-token.guard.d.ts +4 -0
  44. package/dist/core/modules/auth/guards/refresh-token.guard.js +18 -0
  45. package/dist/core/modules/auth/guards/refresh-token.guard.js.map +1 -0
  46. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  47. package/dist/core/modules/auth/inputs/core-auth-sign-in.input.d.ts +1 -0
  48. package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js +5 -0
  49. package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js.map +1 -1
  50. package/dist/core/modules/auth/inputs/core-auth-sign-up.input.d.ts +1 -0
  51. package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js +5 -0
  52. package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js.map +1 -1
  53. package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +3 -0
  54. package/dist/core/modules/auth/interfaces/jwt-payload.interface.d.ts +1 -1
  55. package/dist/core/modules/auth/services/core-auth-user.service.d.ts +3 -0
  56. package/dist/core/modules/auth/services/core-auth-user.service.js.map +1 -1
  57. package/dist/core/modules/auth/services/core-auth.service.d.ts +23 -5
  58. package/dist/core/modules/auth/services/core-auth.service.js +121 -13
  59. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  60. package/dist/core/modules/auth/strategies/jwt-refresh.strategy.d.ts +12 -0
  61. package/dist/core/modules/auth/strategies/jwt-refresh.strategy.js +61 -0
  62. package/dist/core/modules/auth/strategies/jwt-refresh.strategy.js.map +1 -0
  63. package/dist/core/modules/auth/{jwt.strategy.d.ts → strategies/jwt.strategy.d.ts} +4 -3
  64. package/dist/core/modules/auth/{jwt.strategy.js → strategies/jwt.strategy.js} +12 -5
  65. package/dist/core/modules/auth/strategies/jwt.strategy.js.map +1 -0
  66. package/dist/core/modules/user/core-user.model.d.ts +2 -0
  67. package/dist/core/modules/user/core-user.model.js +12 -0
  68. package/dist/core/modules/user/core-user.model.js.map +1 -1
  69. package/dist/core/modules/user/core-user.service.d.ts +2 -2
  70. package/dist/core/modules/user/core-user.service.js +2 -2
  71. package/dist/core/modules/user/core-user.service.js.map +1 -1
  72. package/dist/core.module.js +12 -2
  73. package/dist/core.module.js.map +1 -1
  74. package/dist/index.d.ts +6 -1
  75. package/dist/index.js +6 -1
  76. package/dist/index.js.map +1 -1
  77. package/dist/main.js +23 -0
  78. package/dist/main.js.map +1 -1
  79. package/dist/server/modules/auth/auth.model.js.map +1 -1
  80. package/dist/server/modules/auth/auth.resolver.d.ts +13 -5
  81. package/dist/server/modules/auth/auth.resolver.js +21 -12
  82. package/dist/server/modules/auth/auth.resolver.js.map +1 -1
  83. package/dist/server/modules/auth/auth.service.d.ts +2 -1
  84. package/dist/server/modules/auth/auth.service.js +7 -48
  85. package/dist/server/modules/auth/auth.service.js.map +1 -1
  86. package/dist/server/modules/file/file.module.js +3 -3
  87. package/dist/server/modules/file/file.module.js.map +1 -1
  88. package/dist/server/modules/user/user.model.d.ts +1 -0
  89. package/dist/server/modules/user/user.model.js +19 -0
  90. package/dist/server/modules/user/user.model.js.map +1 -1
  91. package/dist/server/modules/user/user.service.js +1 -1
  92. package/dist/server/modules/user/user.service.js.map +1 -1
  93. package/dist/server/server.module.js +12 -1
  94. package/dist/server/server.module.js.map +1 -1
  95. package/dist/tsconfig.build.tsbuildinfo +1 -1
  96. package/package.json +32 -27
  97. package/src/config.env.ts +41 -2
  98. package/src/core/common/filters/http-exception-log.filter.ts +27 -0
  99. package/src/core/common/helpers/db.helper.ts +15 -5
  100. package/src/core/common/helpers/input.helper.ts +49 -0
  101. package/src/core/common/interceptors/check-security.interceptor.ts +51 -0
  102. package/src/core/common/interfaces/server-options.interface.ts +75 -30
  103. package/src/core/common/interfaces/service-options.interface.ts +4 -0
  104. package/src/core/common/models/core-model.model.ts +7 -0
  105. package/src/core/common/plugins/complexity.plugin.ts +31 -0
  106. package/src/core/common/plugins/mongoose-id.plugin.js +4 -2
  107. package/src/core/common/services/config.service.ts +4 -4
  108. package/src/core/common/services/module.service.ts +26 -4
  109. package/src/core/modules/auth/core-auth.model.ts +15 -2
  110. package/src/core/modules/auth/core-auth.module.ts +8 -2
  111. package/src/core/modules/auth/core-auth.resolver.ts +93 -10
  112. package/src/core/modules/auth/guards/auth.guard.ts +12 -5
  113. package/src/core/modules/auth/guards/refresh-token.guard.ts +5 -0
  114. package/src/core/modules/auth/guards/roles.guard.ts +1 -1
  115. package/src/core/modules/auth/inputs/core-auth-sign-in.input.ts +3 -0
  116. package/src/core/modules/auth/inputs/core-auth-sign-up.input.ts +3 -0
  117. package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +15 -0
  118. package/src/core/modules/auth/interfaces/jwt-payload.interface.ts +1 -1
  119. package/src/core/modules/auth/services/core-auth-user.service.ts +15 -0
  120. package/src/core/modules/auth/services/core-auth.service.ts +216 -18
  121. package/src/core/modules/auth/strategies/jwt-refresh.strategy.ts +56 -0
  122. package/src/core/modules/auth/{jwt.strategy.ts → strategies/jwt.strategy.ts} +16 -5
  123. package/src/core/modules/user/core-user.model.ts +17 -1
  124. package/src/core/modules/user/core-user.service.ts +2 -2
  125. package/src/core.module.ts +14 -2
  126. package/src/index.ts +6 -1
  127. package/src/main.ts +29 -0
  128. package/src/server/modules/auth/auth.model.ts +1 -1
  129. package/src/server/modules/auth/auth.resolver.ts +26 -8
  130. package/src/server/modules/auth/auth.service.ts +20 -61
  131. package/src/server/modules/file/file.module.ts +3 -3
  132. package/src/server/modules/user/user.model.ts +29 -0
  133. package/src/server/modules/user/user.service.ts +1 -1
  134. package/src/server/server.module.ts +12 -1
  135. package/dist/core/modules/auth/jwt.strategy.js.map +0 -1
@@ -7,11 +7,17 @@ import { prepareInput, prepareOutput } from '../helpers/service.helper';
7
7
  import { ServiceOptions } from '../interfaces/service-options.interface';
8
8
  import { CoreModel } from '../models/core-model.model';
9
9
  import { FieldSelection } from '../types/field-selection.type';
10
+ import { ConfigService } from './config.service';
10
11
 
11
12
  /**
12
13
  * Module service class to be extended by concrete module services
13
14
  */
14
15
  export abstract class ModuleService<T extends CoreModel = any> {
16
+ /**
17
+ * Config service, is used to determine certain behavior
18
+ */
19
+ protected configService: ConfigService;
20
+
15
21
  /**
16
22
  * Main model constructor of the service, will be used as default for populate and mapping
17
23
  */
@@ -26,9 +32,11 @@ export abstract class ModuleService<T extends CoreModel = any> {
26
32
  * Set main properties
27
33
  */
28
34
  protected constructor(options?: {
29
- mainDbModel: Model<T & Document>;
35
+ configService?: ConfigService;
36
+ mainDbModel?: Model<T & Document>;
30
37
  mainModelConstructor?: new (...args: any[]) => T;
31
38
  }) {
39
+ this.configService = options?.configService;
32
40
  this.mainDbModel = options?.mainDbModel;
33
41
  this.mainModelConstructor = options?.mainModelConstructor;
34
42
  }
@@ -90,13 +98,21 @@ export abstract class ModuleService<T extends CoreModel = any> {
90
98
  ...options?.serviceOptions,
91
99
  };
92
100
 
101
+ // Set default for ignoreSelections if not set
102
+ const ignoreSelections = this.configService?.getFastButReadOnly('ignoreSelectionsForPopulate', false);
103
+ if (ignoreSelections) {
104
+ if (config.processFieldSelection.ignoreSelections === undefined) {
105
+ config.processFieldSelection.ignoreSelections = ignoreSelections;
106
+ }
107
+ }
108
+
93
109
  // Note force configuration
94
110
  if (config.force) {
95
111
  config.checkRights = false;
96
- if (typeof config.prepareInput === 'object') {
112
+ if (config.prepareInput && typeof config.prepareInput === 'object') {
97
113
  config.prepareInput.checkRoles = false;
98
114
  }
99
- if (typeof config.prepareOutput === 'object') {
115
+ if (config.prepareOutput && typeof config.prepareOutput === 'object') {
100
116
  config.prepareOutput.removeSecrets = false;
101
117
  }
102
118
  }
@@ -104,6 +120,9 @@ export abstract class ModuleService<T extends CoreModel = any> {
104
120
  // Note populate
105
121
  if (config.populate) {
106
122
  config.fieldSelection = config.populate;
123
+ if (config.processFieldSelection?.ignoreSelections) {
124
+ config.processFieldSelection.ignoreSelections = false;
125
+ }
107
126
  }
108
127
 
109
128
  // Prepare input
@@ -220,6 +239,7 @@ export abstract class ModuleService<T extends CoreModel = any> {
220
239
  options: {
221
240
  model?: new (...args: any[]) => T;
222
241
  dbModel?: Model<T & Document>;
242
+ ignoreSelections?: boolean;
223
243
  } = {}
224
244
  ) {
225
245
  const config = {
@@ -227,6 +247,8 @@ export abstract class ModuleService<T extends CoreModel = any> {
227
247
  dbModel: this.mainDbModel,
228
248
  ...options,
229
249
  };
230
- return popAndMap(data, fieldsSelection, config.model, config.dbModel);
250
+ return popAndMap(data, fieldsSelection, config.model, config.dbModel, {
251
+ ignoreSelections: config.ignoreSelections,
252
+ });
231
253
  }
232
254
  }
@@ -1,5 +1,6 @@
1
1
  import { Field, ObjectType } from '@nestjs/graphql';
2
2
  import { CoreModel } from '../../common/models/core-model.model';
3
+ import { CoreUserModel } from '../user/core-user.model';
3
4
 
4
5
  /**
5
6
  * CoreAuth model for the response after the sign in
@@ -13,8 +14,20 @@ export class CoreAuthModel extends CoreModel {
13
14
  /**
14
15
  * JavaScript Web Token (JWT)
15
16
  */
16
- @Field({ description: 'JavaScript Web Token (JWT)' })
17
- token: string = undefined;
17
+ @Field({ description: 'JavaScript Web Token (JWT)', nullable: true })
18
+ token?: string = undefined;
19
+
20
+ /**
21
+ * Refresh token
22
+ */
23
+ @Field({ description: 'Refresh token', nullable: true })
24
+ refreshToken?: string = undefined;
25
+
26
+ /**
27
+ * Current user
28
+ */
29
+ @Field({ description: 'Current user' })
30
+ user: CoreUserModel = undefined;
18
31
 
19
32
  // ===================================================================================================================
20
33
  // Methods
@@ -3,7 +3,8 @@ import { APP_GUARD } from '@nestjs/core';
3
3
  import { JwtModule, JwtModuleOptions } from '@nestjs/jwt';
4
4
  import { PassportModule } from '@nestjs/passport';
5
5
  import { RolesGuard } from './guards/roles.guard';
6
- import { JwtStrategy } from './jwt.strategy';
6
+ import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy';
7
+ import { JwtStrategy } from './strategies/jwt.strategy';
7
8
  import { CoreAuthUserService } from './services/core-auth-user.service';
8
9
  import { CoreAuthService } from './services/core-auth.service';
9
10
  import { PubSub } from 'graphql-subscriptions';
@@ -23,6 +24,7 @@ export class CoreAuthModule {
23
24
  options: JwtModuleOptions & {
24
25
  authService?: Type<CoreAuthService>;
25
26
  jwtStrategy?: Type<JwtStrategy>;
27
+ jwtRefreshStrategy?: Type<JwtRefreshStrategy>;
26
28
  imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
27
29
  providers?: Provider[];
28
30
  }
@@ -56,6 +58,10 @@ export class CoreAuthModule {
56
58
  provide: JwtStrategy,
57
59
  useClass: options.jwtStrategy || JwtStrategy,
58
60
  },
61
+ {
62
+ provide: JwtRefreshStrategy,
63
+ useClass: options.jwtRefreshStrategy || JwtRefreshStrategy,
64
+ },
59
65
  ];
60
66
  if (Array.isArray(options?.providers)) {
61
67
  providers = imports.concat(options.providers);
@@ -66,7 +72,7 @@ export class CoreAuthModule {
66
72
  module: CoreAuthModule,
67
73
  imports: imports,
68
74
  providers: providers,
69
- exports: [CoreAuthService, JwtModule, JwtStrategy, PassportModule, UserModule],
75
+ exports: [CoreAuthService, JwtModule, JwtStrategy, JwtRefreshStrategy, PassportModule, UserModule],
70
76
  };
71
77
  }
72
78
  }
@@ -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
  }