@mshima/generator-jhipster-nodejs 0.0.0-stage → 0.0.2

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 (216) hide show
  1. package/LICENSE.txt +204 -0
  2. package/README.md +243 -2
  3. package/build-info.json +3 -0
  4. package/cli/cli-customizations.cjs +9 -0
  5. package/cli/cli.cjs +40 -0
  6. package/cli/logo.js +31 -0
  7. package/cli/version.cjs +12 -0
  8. package/generators/ci-cd/USAGE +5 -0
  9. package/generators/ci-cd/command.js +5 -0
  10. package/generators/ci-cd/generator.js +158 -0
  11. package/generators/ci-cd/index.js +2 -0
  12. package/generators/ci-cd/templates/.gitlab-ci.yml.ejs +137 -0
  13. package/generators/ci-cd/templates/azure-pipelines.yml.ejs +68 -0
  14. package/generators/ci-cd/templates/circle.yml.ejs +59 -0
  15. package/generators/ci-cd/templates/docker-registry.yml.ejs +58 -0
  16. package/generators/ci-cd/templates/github-ci.yml.ejs +90 -0
  17. package/generators/ci-cd/templates/jenkins/Jenkinsfile.ejs +104 -0
  18. package/generators/ci-cd/templates/jenkins/idea.gdsl +90 -0
  19. package/generators/ci-cd/templates/jenkins/jenkins.yml.ejs +33 -0
  20. package/generators/ci-cd/templates/travis.yml.ejs +75 -0
  21. package/generators/client/command.js +5 -0
  22. package/generators/client/generator.js +21 -0
  23. package/generators/client/generators/bootstrap/command.js +5 -0
  24. package/generators/client/generators/bootstrap/generator.js +28 -0
  25. package/generators/client/generators/bootstrap/index.js +2 -0
  26. package/generators/client/index.js +2 -0
  27. package/generators/common/generators/bootstrap/command.js +5 -0
  28. package/generators/common/generators/bootstrap/generator.js +37 -0
  29. package/generators/common/generators/bootstrap/index.js +2 -0
  30. package/generators/docker/command.js +5 -0
  31. package/generators/docker/generator.js +16 -0
  32. package/generators/docker/generators/bootstrap/command.js +5 -0
  33. package/generators/docker/generators/bootstrap/generator.js +56 -0
  34. package/generators/docker/generators/bootstrap/index.js +2 -0
  35. package/generators/docker/index.js +2 -0
  36. package/generators/docker/templates/docker/app.yml.ejs +25 -0
  37. package/generators/generator-nodejs-constants.js +32 -0
  38. package/generators/languages/command.js +5 -0
  39. package/generators/languages/generator.js +22 -0
  40. package/generators/languages/index.js +2 -0
  41. package/generators/languages/templates/src/main/webapp/i18n/en/home.json +19 -0
  42. package/generators/languages/templates/src/main/webapp/i18n/es/home.json +19 -0
  43. package/generators/languages/templates/src/main/webapp/i18n/it/home.json +19 -0
  44. package/generators/node-server/command.js +96 -0
  45. package/generators/node-server/entity-files.js +26 -0
  46. package/generators/node-server/files.js +254 -0
  47. package/generators/node-server/generator.js +705 -0
  48. package/generators/node-server/generators/bootstrap/command.js +5 -0
  49. package/generators/node-server/generators/bootstrap/generator.js +84 -0
  50. package/generators/node-server/generators/bootstrap/index.js +2 -0
  51. package/generators/node-server/generators/migrations/command.js +5 -0
  52. package/generators/node-server/generators/migrations/files.js +42 -0
  53. package/generators/node-server/generators/migrations/generator.js +108 -0
  54. package/generators/node-server/generators/migrations/index.js +2 -0
  55. package/generators/node-server/generators/migrations/templates/server/src/migrations/_entityConstraintsMigration_.ts.ejs +40 -0
  56. package/generators/node-server/generators/migrations/templates/server/src/migrations/_entityMigration_.ts.ejs +26 -0
  57. package/generators/node-server/generators/migrations/templates/server/src/migrations/_entityRemovedMigration_.ts.ejs +15 -0
  58. package/generators/node-server/generators/migrations/templates/server/src/migrations/_entityUpdatedConstraintsMigration_.ts.ejs +60 -0
  59. package/generators/node-server/generators/migrations/templates/server/src/migrations/_entityUpdatedMigration_.ts.ejs +28 -0
  60. package/generators/node-server/index.js +2 -0
  61. package/generators/node-server/resources/package.json +76 -0
  62. package/generators/node-server/support/api-fake-data.js +54 -0
  63. package/generators/node-server/support/columns.js +73 -0
  64. package/generators/node-server/support/database.js +24 -0
  65. package/generators/node-server/support/entity-authorities.js +32 -0
  66. package/generators/node-server/support/entity-paths.js +13 -0
  67. package/generators/node-server/support/enums.js +11 -0
  68. package/generators/node-server/support/index.js +9 -0
  69. package/generators/node-server/support/mail-templates.js +82 -0
  70. package/generators/node-server/support/migrations.js +376 -0
  71. package/generators/node-server/support/validators.js +25 -0
  72. package/generators/node-server/templates/.dockerignore.ejs +16 -0
  73. package/generators/node-server/templates/.gitignore.jhi.nodejs.ejs +24 -0
  74. package/generators/node-server/templates/Dockerfile.ejs +54 -0
  75. package/generators/node-server/templates/README.md.jhi.nodejs.ejs +220 -0
  76. package/generators/node-server/templates/package.json.ejs +82 -0
  77. package/generators/node-server/templates/server/README.md.ejs +277 -0
  78. package/generators/node-server/templates/server/e2e/_entityFileName_.database.e2e-spec.ts.ejs +192 -0
  79. package/generators/node-server/templates/server/e2e/_entityFileName_.e2e-spec.ts.ejs +402 -0
  80. package/generators/node-server/templates/server/e2e/account.e2e-spec.ts.ejs +349 -0
  81. package/generators/node-server/templates/server/e2e/app.e2e-spec.ts.ejs +188 -0
  82. package/generators/node-server/templates/server/e2e/authority.e2e-spec.ts.ejs +50 -0
  83. package/generators/node-server/templates/server/e2e/cors.e2e-spec.ts.ejs +110 -0
  84. package/generators/node-server/templates/server/e2e/management.e2e-spec.ts.ejs +180 -0
  85. package/generators/node-server/templates/server/e2e/oauth2-bearer.e2e-spec.ts.ejs +71 -0
  86. package/generators/node-server/templates/server/e2e/session.e2e-spec.ts.ejs +234 -0
  87. package/generators/node-server/templates/server/e2e/setup.test.js.ejs +1 -0
  88. package/generators/node-server/templates/server/e2e/user.e2e-spec.ts.ejs +318 -0
  89. package/generators/node-server/templates/server/env.ejs +1 -0
  90. package/generators/node-server/templates/server/eslint.config.mjs.ejs +48 -0
  91. package/generators/node-server/templates/server/nest-cli.json.ejs +11 -0
  92. package/generators/node-server/templates/server/ormconfig.ts.ejs +35 -0
  93. package/generators/node-server/templates/server/package.json.ejs +143 -0
  94. package/generators/node-server/templates/server/sonar-project.properties.ejs +10 -0
  95. package/generators/node-server/templates/server/src/app.config.spec.ts.ejs +13 -0
  96. package/generators/node-server/templates/server/src/app.config.ts.ejs +179 -0
  97. package/generators/node-server/templates/server/src/app.module.ts.ejs +80 -0
  98. package/generators/node-server/templates/server/src/application-config.ts.ejs +153 -0
  99. package/generators/node-server/templates/server/src/auth/account.controller.ts.ejs +183 -0
  100. package/generators/node-server/templates/server/src/auth/auth.module.ts.ejs +104 -0
  101. package/generators/node-server/templates/server/src/auth/auth.service.ts.ejs +294 -0
  102. package/generators/node-server/templates/server/src/auth/csrf.middleware.spec.ts.ejs +76 -0
  103. package/generators/node-server/templates/server/src/auth/csrf.middleware.ts.ejs +85 -0
  104. package/generators/node-server/templates/server/src/auth/decorators/auth-user.decorator.ts.ejs +8 -0
  105. package/generators/node-server/templates/server/src/auth/decorators/roles.decorator.ts.ejs +4 -0
  106. package/generators/node-server/templates/server/src/auth/dto/key-and-password.dto.ts.ejs +17 -0
  107. package/generators/node-server/templates/server/src/auth/dto/password-change.dto.ts.ejs +17 -0
  108. package/generators/node-server/templates/server/src/auth/dto/user-login.dto.ts.ejs +21 -0
  109. package/generators/node-server/templates/server/src/auth/guards/auth.guard.ts.ejs +50 -0
  110. package/generators/node-server/templates/server/src/auth/guards/oauth2-login.guard.ts.ejs +6 -0
  111. package/generators/node-server/templates/server/src/auth/guards/roles.guard.ts.ejs +25 -0
  112. package/generators/node-server/templates/server/src/auth/index.ts.ejs +8 -0
  113. package/generators/node-server/templates/server/src/auth/jwt.config.spec.ts.ejs +22 -0
  114. package/generators/node-server/templates/server/src/auth/jwt.config.ts.ejs +29 -0
  115. package/generators/node-server/templates/server/src/auth/oauth2.authorities.spec.ts.ejs +24 -0
  116. package/generators/node-server/templates/server/src/auth/oauth2.authorities.ts.ejs +33 -0
  117. package/generators/node-server/templates/server/src/auth/oauth2.config.ts.ejs +51 -0
  118. package/generators/node-server/templates/server/src/auth/passport.jwt.strategy.ts.ejs +45 -0
  119. package/generators/node-server/templates/server/src/auth/passport.oauth2-bearer.strategy.ts.ejs +32 -0
  120. package/generators/node-server/templates/server/src/auth/passport.oauth2.strategy.spec.ts.ejs +27 -0
  121. package/generators/node-server/templates/server/src/auth/passport.oauth2.strategy.ts.ejs +44 -0
  122. package/generators/node-server/templates/server/src/auth/password-util.ts.ejs +10 -0
  123. package/generators/node-server/templates/server/src/auth/password.service.ts.ejs +34 -0
  124. package/generators/node-server/templates/server/src/auth/payload.interface.ts.ejs +14 -0
  125. package/generators/node-server/templates/server/src/auth/role-type.ts.ejs +7 -0
  126. package/generators/node-server/templates/server/src/auth/session-user.middleware.ts.ejs +33 -0
  127. package/generators/node-server/templates/server/src/auth/session.entity.ts.ejs +36 -0
  128. package/generators/node-server/templates/server/src/auth/session.store.spec.ts.ejs +99 -0
  129. package/generators/node-server/templates/server/src/auth/session.store.ts.ejs +116 -0
  130. package/generators/node-server/templates/server/src/auth/user.jwt.controller.ts.ejs +26 -0
  131. package/generators/node-server/templates/server/src/auth/user.oauth2.controller.ts.ejs +78 -0
  132. package/generators/node-server/templates/server/src/auth/user.session.controller.ts.ejs +103 -0
  133. package/generators/node-server/templates/server/src/common/common.module.ts.ejs +19 -0
  134. package/generators/node-server/templates/server/src/common/database/base.entity.ts.ejs +13 -0
  135. package/generators/node-server/templates/server/src/common/database/column-transformers.spec.ts.ejs +46 -0
  136. package/generators/node-server/templates/server/src/common/database/column-transformers.ts.ejs +69 -0
  137. package/generators/node-server/templates/server/src/common/database/column-types.spec.ts.ejs +21 -0
  138. package/generators/node-server/templates/server/src/common/database/column-types.ts.ejs +29 -0
  139. package/generators/node-server/templates/server/src/common/database/pagination.entity.spec.ts.ejs +66 -0
  140. package/generators/node-server/templates/server/src/common/database/pagination.entity.ts.ejs +108 -0
  141. package/generators/node-server/templates/server/src/common/dto/base.dto.ts.ejs +19 -0
  142. package/generators/node-server/templates/server/src/common/errors/error-constants.ts.ejs +10 -0
  143. package/generators/node-server/templates/server/src/common/errors/problem-details.filter.ts.ejs +63 -0
  144. package/generators/node-server/templates/server/src/common/errors/problem.exception.ts.ejs +92 -0
  145. package/generators/node-server/templates/server/src/common/field_validators.ejs +77 -0
  146. package/generators/node-server/templates/server/src/common/header-util.ts.ejs +105 -0
  147. package/generators/node-server/templates/server/src/common/interceptors/logging.interceptor.spec.ts.ejs +93 -0
  148. package/generators/node-server/templates/server/src/common/interceptors/logging.interceptor.ts.ejs +48 -0
  149. package/generators/node-server/templates/server/src/common/logging.spec.ts.ejs +83 -0
  150. package/generators/node-server/templates/server/src/common/logging.ts.ejs +129 -0
  151. package/generators/node-server/templates/server/src/common/metrics.spec.ts.ejs +43 -0
  152. package/generators/node-server/templates/server/src/common/metrics.ts.ejs +198 -0
  153. package/generators/node-server/templates/server/src/common/request.ts.ejs +8 -0
  154. package/generators/node-server/templates/server/src/common/sortable-properties.ts.ejs +43 -0
  155. package/generators/node-server/templates/server/src/common/updatable-properties.ts.ejs +14 -0
  156. package/generators/node-server/templates/server/src/config/application-dev.yml.ejs +118 -0
  157. package/generators/node-server/templates/server/src/config/application-prod.yml.ejs +97 -0
  158. package/generators/node-server/templates/server/src/config/application-secret-samples.yml.ejs +42 -0
  159. package/generators/node-server/templates/server/src/config/application-test.yml.ejs +68 -0
  160. package/generators/node-server/templates/server/src/config/application-tls-samples.yml.ejs +15 -0
  161. package/generators/node-server/templates/server/src/config/application-tls.yml.ejs +15 -0
  162. package/generators/node-server/templates/server/src/config/application.yml.ejs +149 -0
  163. package/generators/node-server/templates/server/src/config.spec.ts.ejs +81 -0
  164. package/generators/node-server/templates/server/src/config.ts.ejs +255 -0
  165. package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.controller.ts.ejs +178 -0
  166. package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.entity.ts.ejs +110 -0
  167. package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.mapper.ts.ejs +37 -0
  168. package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.module.ts.ejs +14 -0
  169. package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.service.ts.ejs +123 -0
  170. package/generators/node-server/templates/server/src/entities/_entityFileName_/dto/_entityFileName_.dto.ts.ejs +99 -0
  171. package/generators/node-server/templates/server/src/mail/mail.module.ts.ejs +49 -0
  172. package/generators/node-server/templates/server/src/mail/mail.service.spec.ts.ejs +105 -0
  173. package/generators/node-server/templates/server/src/mail/mail.service.ts.ejs +120 -0
  174. package/generators/node-server/templates/server/src/main.ts.ejs +114 -0
  175. package/generators/node-server/templates/server/src/management/api-docs.controller.ts.ejs +27 -0
  176. package/generators/node-server/templates/server/src/management/api-docs.service.ts.ejs +8 -0
  177. package/generators/node-server/templates/server/src/management/configuration.controller.ts.ejs +80 -0
  178. package/generators/node-server/templates/server/src/management/health.controller.spec.ts.ejs +79 -0
  179. package/generators/node-server/templates/server/src/management/health.controller.ts.ejs +87 -0
  180. package/generators/node-server/templates/server/src/management/health.module.ts.ejs +9 -0
  181. package/generators/node-server/templates/server/src/management/loggers.controller.ts.ejs +40 -0
  182. package/generators/node-server/templates/server/src/management/management.controller.spec.ts.ejs +26 -0
  183. package/generators/node-server/templates/server/src/management/management.controller.ts.ejs +34 -0
  184. package/generators/node-server/templates/server/src/management/management.module.ts.ejs +21 -0
  185. package/generators/node-server/templates/server/src/management/metrics.controller.ts.ejs +85 -0
  186. package/generators/node-server/templates/server/src/migrations/1570200270081-CreateTables.ts.ejs +64 -0
  187. package/generators/node-server/templates/server/src/migrations/1570200270082-CreateSessions.ts.ejs +26 -0
  188. package/generators/node-server/templates/server/src/migrations/1570200490072-SeedUsersRoles.ts.ejs +74 -0
  189. package/generators/node-server/templates/server/src/migrations/support/schema.ts.ejs +254 -0
  190. package/generators/node-server/templates/server/src/orm.config.ts.ejs +147 -0
  191. package/generators/node-server/templates/server/src/registry/consul.service.ts.ejs +70 -0
  192. package/generators/node-server/templates/server/src/registry/eureka.service.ts.ejs +129 -0
  193. package/generators/node-server/templates/server/src/swagger.ts.ejs +24 -0
  194. package/generators/node-server/templates/server/src/tls.ts.ejs +27 -0
  195. package/generators/node-server/templates/server/src/users/authority.controller.ts.ejs +85 -0
  196. package/generators/node-server/templates/server/src/users/authority.entity.ts.ejs +22 -0
  197. package/generators/node-server/templates/server/src/users/dto/authority.dto.ts.ejs +10 -0
  198. package/generators/node-server/templates/server/src/users/dto/user.dto.ts.ejs +116 -0
  199. package/generators/node-server/templates/server/src/users/public.user.controller.ts.ejs +51 -0
  200. package/generators/node-server/templates/server/src/users/user.controller.ts.ejs +191 -0
  201. package/generators/node-server/templates/server/src/users/user.entity.ts.ejs +63 -0
  202. package/generators/node-server/templates/server/src/users/user.mapper.ts.ejs +26 -0
  203. package/generators/node-server/templates/server/src/users/user.module.ts.ejs +19 -0
  204. package/generators/node-server/templates/server/src/users/user.service.ts.ejs +156 -0
  205. package/generators/node-server/templates/server/tsconfig.build.json.ejs +9 -0
  206. package/generators/node-server/templates/server/tsconfig.json.ejs +23 -0
  207. package/generators/node-server/templates/server/typeorm-migration.ts.ejs +18 -0
  208. package/generators/node-server/templates/server/vitest.config.e2e.ts.ejs +16 -0
  209. package/generators/node-server/templates/server/vitest.config.ts.ejs +16 -0
  210. package/generators/server/command.js +7 -0
  211. package/generators/server/generator.js +20 -0
  212. package/generators/server/generators/bootstrap/command.js +5 -0
  213. package/generators/server/generators/bootstrap/generator.js +15 -0
  214. package/generators/server/generators/bootstrap/index.js +2 -0
  215. package/generators/server/index.js +2 -0
  216. package/package.json +75 -4
@@ -0,0 +1,50 @@
1
+ <%_ if (authenticationType === 'oauth2') { _%>
2
+ import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
3
+ import { AuthGuard as NestAuthGuard } from '@nestjs/passport';
4
+
5
+ /**
6
+ * The authentication of the API, like JHipster Spring Boot with OAuth2: the session of the login (the clients of the
7
+ * application, their unsafe requests checked by the CsrfMiddleware), else an access token of the identity provider as
8
+ * bearer token (oauth2ResourceServer), else 401.
9
+ */
10
+ @Injectable()
11
+ export class AuthGuard extends NestAuthGuard('oauth2-bearer') {
12
+ canActivate(context: ExecutionContext): any {
13
+ const request = context.switchToHttp().getRequest();
14
+
15
+ if (request.session?.user) {
16
+ request.user = request.session.user;
17
+ return true;
18
+ }
19
+ // The authentication scheme is case insensitive, like HTTP gives it.
20
+ if (!/^bearer\s+\S/i.test(request.headers.authorization ?? '')) {
21
+ throw new UnauthorizedException();
22
+ }
23
+ return super.canActivate(context);
24
+ }
25
+ }
26
+ <%_ } else if (authenticationTypeSession) { _%>
27
+ import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
28
+
29
+ /**
30
+ * The authentication of the API, like JHipster Spring Boot with sessions: the user of the session of the login (its unsafe
31
+ * requests checked by the CsrfMiddleware), else 401.
32
+ */
33
+ @Injectable()
34
+ export class AuthGuard implements CanActivate {
35
+ canActivate(context: ExecutionContext): boolean {
36
+ const request = context.switchToHttp().getRequest();
37
+ if (!request.session?.user) {
38
+ throw new UnauthorizedException();
39
+ }
40
+ request.user = request.session.user;
41
+ return true;
42
+ }
43
+ }
44
+ <%_ } else { _%>
45
+ import { Injectable } from '@nestjs/common';
46
+ import { AuthGuard as NestAuthGuard } from '@nestjs/passport';
47
+
48
+ @Injectable()
49
+ export class AuthGuard extends NestAuthGuard('<%= authenticationType %>') {}
50
+ <%_ } _%>
@@ -0,0 +1,6 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { AuthGuard as NestAuthGuard } from '@nestjs/passport';
3
+
4
+ /** The login with the identity provider (authorization code with PKCE), on the login routes only. */
5
+ @Injectable()
6
+ export class OAuth2LoginGuard extends NestAuthGuard('oauth2') {}
@@ -0,0 +1,25 @@
1
+ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { <%= userLoginVmClass%> } from '../../users/dto/user.dto.ts';
4
+
5
+ /**
6
+ * Allows a request when its user has one of the roles of the route, the @Roles of the handler, else the ones of its
7
+ * controller class, like a @PreAuthorize of JHipster Spring Boot on a method or on its class.
8
+ */
9
+ @Injectable()
10
+ export class RolesGuard implements CanActivate {
11
+ constructor(private readonly reflector: Reflector) {}
12
+
13
+ canActivate(context: ExecutionContext): boolean {
14
+ const roles = this.reflector.getAllAndOverride<string[] | undefined>('roles', [context.getHandler(), context.getClass()]);
15
+
16
+ if (!roles) {
17
+ return true;
18
+ }
19
+
20
+ const request = context.switchToHttp().getRequest();
21
+ const user = request.user as <%= userLoginVmClass%>;
22
+
23
+ return !!user?.authorities?.some(role => roles.includes(role));
24
+ }
25
+ }
@@ -0,0 +1,8 @@
1
+ export * from './guards/auth.guard.ts';
2
+ export * from './guards/roles.guard.ts';
3
+ export * from './decorators/roles.decorator.ts';
4
+ export * from './decorators/auth-user.decorator.ts';
5
+ <%_ if (generateUserManagement) { _%>
6
+ export * from './password-util.ts';
7
+ <%_ } _%>
8
+ export * from './role-type.ts';
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { JWT_MIN_KEY_BYTES, jwtSecretKey } from './jwt.config.ts';
4
+
5
+ const configServiceOf = (secret: string): ConfigService =>
6
+ ({ getOrThrow: () => secret }) as unknown as ConfigService;
7
+
8
+ describe('JWT key', () => {
9
+ it('is the base64 decoded secret', () => {
10
+ const key = Buffer.alloc(JWT_MIN_KEY_BYTES, 1);
11
+ expect(jwtSecretKey(configServiceOf(key.toString('base64')))).toEqual(key);
12
+ });
13
+
14
+ it('refuses a secret shorter than 512 bits, like the HS512 key of JHipster Spring Boot', () => {
15
+ const secret = Buffer.alloc(JWT_MIN_KEY_BYTES - 1, 1).toString('base64');
16
+ expect(() => jwtSecretKey(configServiceOf(secret))).toThrow(/too short: 504 bits/);
17
+ });
18
+
19
+ it('refuses a missing secret', () => {
20
+ expect(() => jwtSecretKey(configServiceOf(''))).toThrow(/is not set/);
21
+ });
22
+ });
@@ -0,0 +1,29 @@
1
+ import { ConfigService } from '@nestjs/config';
2
+
3
+ /**
4
+ * The algorithm and the key of the JWTs, like JHipster Spring Boot: HS512, with the base64 decoded
5
+ * `jhipster.security.authentication.jwt.base64-secret` as the key, so the applications of a microservice architecture
6
+ * sharing that secret (a Spring Boot gateway and NodeJS microservices, or the reverse) accept each other's tokens.
7
+ */
8
+ export const JWT_ALGORITHM = 'HS512';
9
+ /** The minimum length of the key of HS512, 512 bits. */
10
+ export const JWT_MIN_KEY_BYTES = 64;
11
+
12
+ export function jwtSecretKey(configService: ConfigService): Buffer {
13
+ const secret = configService.getOrThrow<string>('jhipster.security.authentication.jwt.base64-secret');
14
+ if (!secret) {
15
+ // Like JHipster Spring Boot, the application does not start without a secret: the secret-samples profile has a
16
+ // sample for development, production sets its own.
17
+ throw new Error(
18
+ 'jhipster.security.authentication.jwt.base64-secret is not set: set the JHIPSTER_SECURITY_AUTHENTICATION_JWT_BASE64SECRET environment variable, or activate the secret-samples profile',
19
+ );
20
+ }
21
+ const key = Buffer.from(secret, 'base64');
22
+ if (key.length < JWT_MIN_KEY_BYTES) {
23
+ // Like the HS512 key of JHipster Spring Boot (its JWT encoder refuses a shorter one).
24
+ throw new Error(
25
+ `jhipster.security.authentication.jwt.base64-secret is too short: ${key.length * 8} bits, HS512 needs at least ${JWT_MIN_KEY_BYTES * 8} bits (a base64 secret of ${JWT_MIN_KEY_BYTES} bytes or more)`,
26
+ );
27
+ }
28
+ return key;
29
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { CLAIMS_NAMESPACE, extractAuthoritiesFromClaims } from './oauth2.authorities.ts';
3
+
4
+ describe('OAuth2 authorities', () => {
5
+ it('should read the groups claim', () => {
6
+ expect(extractAuthoritiesFromClaims({ groups: ['ROLE_ADMIN', 'ROLE_USER'] })).toEqual(['ROLE_ADMIN', 'ROLE_USER']);
7
+ });
8
+
9
+ it('should read the roles claim', () => {
10
+ expect(extractAuthoritiesFromClaims({ roles: ['ROLE_USER'] })).toEqual(['ROLE_USER']);
11
+ });
12
+
13
+ it('should read the namespaced roles claim', () => {
14
+ expect(extractAuthoritiesFromClaims({ [`${CLAIMS_NAMESPACE}roles`]: ['ROLE_ADMIN'] })).toEqual(['ROLE_ADMIN']);
15
+ });
16
+
17
+ it('should keep only the ROLE_ values', () => {
18
+ expect(extractAuthoritiesFromClaims({ roles: ['ROLE_USER', 'offline_access', 'uma_authorization'] })).toEqual(['ROLE_USER']);
19
+ });
20
+
21
+ it('should give no authority without roles', () => {
22
+ expect(extractAuthoritiesFromClaims({ preferred_username: 'user' })).toEqual([]);
23
+ });
24
+ });
@@ -0,0 +1,33 @@
1
+ import { <%= userLoginVmClass %> } from '../users/dto/user.dto.ts';
2
+
3
+ // Claim with the roles when the provider namespaces it, like Auth0.
4
+ export const CLAIMS_NAMESPACE = 'https://www.jhipster.tech/';
5
+
6
+ /**
7
+ * The authorities of an OIDC user, from the claims of its token, as JHipster's Spring Boot application reads them:
8
+ * the "groups" claim, else "roles", else the namespaced "roles", keeping the ROLE_ ones.
9
+ */
10
+ export function extractAuthoritiesFromClaims(claims: Record<string, unknown>): string[] {
11
+ const roles = claims.groups ?? claims.roles ?? claims[`${CLAIMS_NAMESPACE}roles`] ?? [];
12
+ return Array.isArray(roles) ? roles.filter((role): role is string => typeof role === 'string' && role.startsWith('ROLE_')) : [];
13
+ }
14
+
15
+ /**
16
+ * The user of the claims of an OIDC token (the ID token over the access token at the login, the access token of a bearer
17
+ * request), like the OIDC user of JHipster Spring Boot: the login of preferred_username (else sub) and the mail in lower
18
+ * case, the names, the language and the authorities.
19
+ */
20
+ export function oidcUserOfClaims(claims: Record<string, any>): <%= userLoginVmClass %> {
21
+ const login = String(claims.preferred_username ?? claims.sub).toLowerCase();
22
+ // Without a password, nor a mail when the token has none (the login of the user saved has its fallback).
23
+ return Object.assign(new <%= userLoginVmClass %>(), {
24
+ login,
25
+ firstName: claims.given_name,
26
+ lastName: claims.family_name,
27
+ email: claims.email ? String(claims.email).toLowerCase() : undefined,
28
+ imageUrl: claims.picture ?? '',
29
+ activated: true,
30
+ langKey: typeof claims.locale === 'string' ? claims.locale.split(/[-_]/)[0] : 'en',
31
+ authorities: extractAuthoritiesFromClaims(claims),
32
+ });
33
+ }
@@ -0,0 +1,51 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+
4
+ import type { ApplicationConfig } from '../application-config.ts';
5
+
6
+ const openIdProtocol = 'protocol/openid-connect';
7
+
8
+ /**
9
+ * The endpoints of the OpenID Connect provider (jhipster.security.oauth2.client.provider.oidc.issuer-uri), Keycloak or
10
+ * Okta: the authorization, token and logout URLs, and the callback of the application.
11
+ */
12
+ @Injectable()
13
+ export class OAuth2Config {
14
+ readonly authorizationURL: string;
15
+ readonly tokenURL: string;
16
+ readonly logoutUrl: string;
17
+ readonly callbackURL: string;
18
+ /** The keys of the issuer (JWKS), the bearer tokens are verified with. */
19
+ readonly jwksUri: string;
20
+ /** The issuer of the tokens, the configured one (the browser's), not the one the application reaches from a container. */
21
+ readonly issuer: string;
22
+ /** The audiences of the access tokens the API accepts as bearer tokens, like JHipster Spring Boot. */
23
+ readonly audience: string[];
24
+ readonly isOktaProvider: boolean;
25
+
26
+ constructor(configService: ConfigService<ApplicationConfig, true>) {
27
+ const issuerUri = configService.get('jhipster.security.oauth2.client.provider.oidc.issuer-uri', { infer: true });
28
+ // The issuer the application reaches from a container (docker compose), the browser keeps the configured one.
29
+ const dockerIssuerUri = process.env.NODE_CLIENT_PROVIDER_OIDC_ISSUER_URI || issuerUri;
30
+ // Relative, like the {baseUrl}/login/oauth2/code/oidc of JHipster Spring Boot: the strategy resolves it against the
31
+ // URL of the request (the X-Forwarded-Proto of a proxy included), the URL the browser reaches the application at.
32
+ this.callbackURL = '/login/oauth2/code/oidc';
33
+ this.isOktaProvider = issuerUri.includes('okta');
34
+ this.issuer = issuerUri;
35
+ this.audience = String(configService.get('jhipster.security.oauth2.audience', { infer: true }) ?? '')
36
+ .split(',')
37
+ .map(audience => audience.trim())
38
+ .filter(Boolean);
39
+ if (this.isOktaProvider) {
40
+ this.authorizationURL = `${issuerUri}/v1/authorize`;
41
+ this.tokenURL = `${issuerUri}/v1/token`;
42
+ this.logoutUrl = `${issuerUri}/v1/logout`;
43
+ this.jwksUri = `${dockerIssuerUri}/v1/keys`;
44
+ } else {
45
+ this.authorizationURL = `${issuerUri}/${openIdProtocol}/auth`;
46
+ this.tokenURL = `${dockerIssuerUri}/${openIdProtocol}/token`;
47
+ this.logoutUrl = `${issuerUri}/${openIdProtocol}/logout`;
48
+ this.jwksUri = `${dockerIssuerUri}/${openIdProtocol}/certs`;
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,45 @@
1
+ import { ExtractJwt, Strategy<% if (generateBuiltInUserEntity) { %>, VerifiedCallback<% } %> } from 'passport-jwt';
2
+ import { PassportStrategy } from '@nestjs/passport';
3
+ import { ConfigService } from '@nestjs/config';
4
+ import { Injectable<% if (generateBuiltInUserEntity) { %>, UnauthorizedException<% } %> } from '@nestjs/common';
5
+ import { Payload<% if (!generateBuiltInUserEntity) { %>, payloadAuthorities<% } %> } from './payload.interface.ts';
6
+ import { JWT_ALGORITHM, jwtSecretKey } from './jwt.config.ts';
7
+ <%_ if (generateBuiltInUserEntity) { _%>
8
+ import { AuthService } from './auth.service.ts';
9
+ <%_ } else { _%>
10
+ import { <%= userLoginVmClass %> } from '../users/dto/user.dto.ts';
11
+ <%_ } _%>
12
+
13
+ @Injectable()
14
+ export class JwtStrategy extends PassportStrategy(Strategy) {
15
+ <%_ if (generateBuiltInUserEntity) { _%>
16
+ constructor(private readonly authService: AuthService, configService: ConfigService) {
17
+ <%_ } else { _%>
18
+ constructor(configService: ConfigService) {
19
+ <%_ } _%>
20
+ super({
21
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
22
+ ignoreExpiration: false,
23
+ secretOrKey: jwtSecretKey(configService),
24
+ algorithms: [JWT_ALGORITHM],
25
+ });
26
+ }
27
+
28
+ <%_ if (generateBuiltInUserEntity) { _%>
29
+ async validate(payload: Payload, done: VerifiedCallback): Promise<any> {
30
+ const user = await this.authService.validateUser(payload);
31
+ if (!user) {
32
+ return done(new UnauthorizedException({ message: 'user does not exist' }), false);
33
+ }
34
+
35
+ return done(null, user);
36
+ }
37
+ <%_ } else { _%>
38
+ /**
39
+ * Without built-in User, the user is the one of the token, signed by the application issuing it (the gateway).
40
+ */
41
+ validate(payload: Payload): <%= userLoginVmClass %> {
42
+ return Object.assign(new <%= userLoginVmClass %>(), { login: payload.sub, authorities: payloadAuthorities(payload) });
43
+ }
44
+ <%_ } _%>
45
+ }
@@ -0,0 +1,32 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { PassportStrategy } from '@nestjs/passport';
3
+ import { passportJwtSecret } from 'jwks-rsa';
4
+ import { ExtractJwt, Strategy } from 'passport-jwt';
5
+
6
+ import { <%= userLoginVmClass %> } from '../users/dto/user.dto.ts';
7
+ import { oidcUserOfClaims } from './oauth2.authorities.ts';
8
+ import { OAuth2Config } from './oauth2.config.ts';
9
+
10
+ /**
11
+ * The access tokens of the identity provider sent as bearer tokens, like the oauth2ResourceServer of JHipster Spring
12
+ * Boot: signed by the keys of the issuer (its JWKS), not expired, the user and its authorities from their claims. A
13
+ * gateway relays them to its microservices, the mobile clients send them.
14
+ */
15
+ @Injectable()
16
+ export class Oauth2BearerStrategy extends PassportStrategy(Strategy, 'oauth2-bearer') {
17
+ constructor(oauth2Config: OAuth2Config) {
18
+ super({
19
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
20
+ ignoreExpiration: false,
21
+ secretOrKeyProvider: passportJwtSecret({ jwksUri: oauth2Config.jwksUri, cache: true, rateLimit: true }),
22
+ algorithms: ['RS256'],
23
+ // Issued by the identity provider for this API, like the issuer and audience validators of JHipster Spring Boot.
24
+ issuer: oauth2Config.issuer,
25
+ audience: oauth2Config.audience,
26
+ });
27
+ }
28
+
29
+ validate(claims: Record<string, any>): <%= userLoginVmClass %> {
30
+ return oidcUserOfClaims(claims);
31
+ }
32
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { Oauth2Strategy } from './passport.oauth2.strategy.ts';
3
+
4
+ const token = (claims: object): string => `${Buffer.from('{"alg":"none"}').toString('base64url')}.${Buffer.from(JSON.stringify(claims)).toString('base64url')}.`;
5
+
6
+ describe('OAuth2 login strategy', () => {
7
+ it('should build the user of the ID token of the token response, which passport-oauth2 gives to the verify', async () => {
8
+ const authService = { findUserOrSave: vi.fn() };
9
+ const configService = { get: () => 'value' };
10
+ const oauth2Config = { authorizationURL: 'http://localhost/auth', tokenURL: 'http://localhost/token', callbackURL: 'http://localhost/callback' };
11
+ const strategy: any = new Oauth2Strategy(authService as any, configService as any, oauth2Config as any);
12
+ // passport-oauth2 gives the params of the token response with a verify of 5 parameters only (4 without them).
13
+ expect(strategy._verify).toHaveLength(5);
14
+
15
+ const idToken = token({ sub: 'abc', preferred_username: 'User', email: 'User@Example.com', groups: ['ROLE_USER'] });
16
+ const user = await new Promise((resolve, reject) =>
17
+ strategy._verify(token({ sub: 'abc' }), 'refresh', { id_token: idToken }, undefined, (error: Error | null, result: unknown) =>
18
+ error ? reject(error) : resolve(result),
19
+ ),
20
+ );
21
+
22
+ expect(user).toMatchObject({ login: 'user', email: 'user@example.com', authorities: ['ROLE_USER'], idToken });
23
+ <%_ if (generateBuiltInUserEntity) { _%>
24
+ expect(authService.findUserOrSave).toHaveBeenCalledWith(expect.objectContaining({ login: 'user' }));
25
+ <%_ } _%>
26
+ });
27
+ });
@@ -0,0 +1,44 @@
1
+ import { jwtDecode } from 'jwt-decode';
2
+ import Strategy from 'passport-oauth2';
3
+ import { PassportStrategy } from '@nestjs/passport';
4
+ import { Injectable, Logger } from '@nestjs/common';
5
+ import { ConfigService } from '@nestjs/config';
6
+ import { AuthService } from './auth.service.ts';
7
+ import { <%= userLoginVmClass %> } from '../users/dto/user.dto.ts';
8
+ import { OAuth2Config } from './oauth2.config.ts';
9
+ import { oidcUserOfClaims } from './oauth2.authorities.ts';
10
+
11
+ @Injectable()
12
+ export class Oauth2Strategy extends PassportStrategy(Strategy, undefined, true) {
13
+ logger = new Logger('oauth2');
14
+
15
+ constructor(private readonly authService: AuthService, configService: ConfigService, oauth2Config: OAuth2Config) {
16
+ super({
17
+ authorizationURL: oauth2Config.authorizationURL,
18
+ tokenURL: oauth2Config.tokenURL,
19
+ clientID: `${configService.get('jhipster.security.oauth2.client.registration.oidc.client-id')}`,
20
+ clientSecret: `${configService.get('jhipster.security.oauth2.client.registration.oidc.client-secret')}`,
21
+ callbackURL: oauth2Config.callbackURL,
22
+ proxy: true,
23
+ scope: 'openid profile',
24
+ state: true,
25
+ pkce: true,
26
+ // The user comes from the tokens of the response (validate), not from a profile request.
27
+ skipUserProfile: true,
28
+ });
29
+ }
30
+
31
+ /**
32
+ * The user of the login, like the OIDC user of JHipster Spring Boot: the claims of the ID token over the ones of the
33
+ * access token (the access token of some providers, like Okta, has no login nor mail). With its 4 parameters, passport-oauth2
34
+ * gives it the params of the token response (the ID token): with 3, it gives the profile instead, undefined here.
35
+ */
36
+ <%- generateBuiltInUserEntity ? 'async ' : '' %>validate(accessToken: string, refreshToken: string, params: any, _profile: unknown): <%- generateBuiltInUserEntity ? `Promise<${userLoginVmClass} & { idToken?: string }>` : `${userLoginVmClass} & { idToken?: string }` %> {
37
+ const user = oidcUserOfClaims({ ...jwtDecode(accessToken), ...(params.id_token ? jwtDecode(params.id_token) : {}) });
38
+ <%_ if (generateBuiltInUserEntity) { _%>
39
+ await this.authService.findUserOrSave(user);
40
+ <%_ } _%>
41
+ // The ID token of the session, for the logout.
42
+ return Object.assign(user, { idToken: params.id_token });
43
+ }
44
+ }
@@ -0,0 +1,10 @@
1
+ import { compare, hash } from 'bcrypt';
2
+
3
+ // Asynchronous: bcrypt does not block the event loop of the other requests.
4
+ export const encodePassword = async (password: string, rounds: number): Promise<string> => {
5
+ return hash(password, rounds);
6
+ };
7
+
8
+ export const comparePassword = async (password: string, hash: string): Promise<boolean> => {
9
+ return compare(password, hash);
10
+ };
@@ -0,0 +1,34 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+
4
+ import type { ApplicationConfig } from '../application-config.ts';
5
+ import { comparePassword, encodePassword } from './password-util.ts';
6
+
7
+ /**
8
+ * The passwords of the users, like the PasswordEncoder of JHipster Spring Boot: bcrypt, with the rounds of
9
+ * <%= nodejsPasswordRoundsProperty %>.
10
+ */
11
+ @Injectable()
12
+ export class PasswordService {
13
+ private readonly rounds: number;
14
+
15
+ constructor(configService: ConfigService<ApplicationConfig, true>) {
16
+ this.rounds = Number(configService.get('<%= nodejsPasswordRoundsProperty %>', { infer: true }));
17
+ }
18
+
19
+ encode(password: string): Promise<string> {
20
+ return encodePassword(password, this.rounds);
21
+ }
22
+
23
+ matches(password: string, hash: string): Promise<boolean> {
24
+ return comparePassword(password, hash);
25
+ }
26
+
27
+ /** The user with its password encoded, when it has one. */
28
+ async transform<const T extends { password?: string }>(user: T): Promise<T> {
29
+ if (user.password) {
30
+ user.password = await this.encode(user.password);
31
+ }
32
+ return user;
33
+ }
34
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The claims of a JWT, like the tokens of JHipster Spring Boot: the login as the subject, and the authorities separated
3
+ * by spaces.
4
+ */
5
+ export interface Payload {
6
+ sub: string;
7
+ auth: string;
8
+ userId?: <%= databaseType === 'sql' ? 'number' : 'string' %>;
9
+ }
10
+
11
+ /** The authorities of the `auth` claim. */
12
+ export function payloadAuthorities(payload: Payload): string[] {
13
+ return payload.auth ? payload.auth.split(/[\s,]+/).filter(Boolean) : [];
14
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ export enum RoleType {
4
+ USER = 'ROLE_USER',
5
+ ADMIN = 'ROLE_ADMIN',
6
+ ANONYMOUS = 'ROLE_ANONYMOUS',
7
+ }
@@ -0,0 +1,33 @@
1
+ import { Injectable, NestMiddleware } from '@nestjs/common';
2
+ import { NextFunction, Request, Response } from 'express';
3
+ import { <%= user.dtoClass %> } from '../users/dto/user.dto.ts';
4
+ import { UserService } from '../users/user.service.ts';
5
+
6
+ /** The user a session keeps: without its secrets, which stay in the database. */
7
+ export function sessionUser(user: <%= user.dtoClass %>): Omit<<%= user.dtoClass %>, 'password' | 'activationKey' | 'resetKey'> {
8
+ const { password: _password, activationKey: _activationKey, resetKey: _resetKey, ...kept } = user;
9
+ return kept;
10
+ }
11
+
12
+ /**
13
+ * The user of a logged session, read again from the database at every request, like the user of a JWT token and the
14
+ * remember-me login of JHipster Spring Boot: a user removed or deactivated is logged out at its next request (its session
15
+ * lasting up to 31 days with remember-me), the authorities given or removed apply at once.
16
+ */
17
+ @Injectable()
18
+ export class SessionUserMiddleware implements NestMiddleware {
19
+ constructor(private readonly userService: UserService) {}
20
+
21
+ async use(req: Request, res: Response, next: NextFunction): Promise<void> {
22
+ const session = req.session as any;
23
+ if (session?.user) {
24
+ const user = await this.userService.findByFields({ where: { login: session.user.login } });
25
+ if (user?.activated) {
26
+ session.user = sessionUser(user);
27
+ } else {
28
+ await new Promise<void>(resolve => session.destroy(() => resolve()));
29
+ }
30
+ }
31
+ next();
32
+ }
33
+ }
@@ -0,0 +1,36 @@
1
+ import { Column, Entity, Index, <%_ if (databaseTypeMongodb) { _%>ObjectIdColumn<%_ } else { _%>PrimaryColumn<%_ } _%> } from 'typeorm';
2
+
3
+ /**
4
+ * A session of the server, kept in the database of the application by the SessionStore: the sessions outlive a restart
5
+ * and are shared by the instances of the application, like the HTTP sessions of a cluster of JHipster Spring Boot with
6
+ * Spring Session, and the remember-me logins outlive a restart like its persistent tokens.
7
+ */
8
+ @Entity('<%- jhiTablePrefix %>_session')
9
+ export class Session {
10
+ <%_ if (databaseTypeMongodb) { _%>
11
+ @ObjectIdColumn({ name: '_id' })
12
+ _id?: string;
13
+
14
+ /** The id of the session, the one of its cookie. */
15
+ @Column({ unique: true })
16
+ sid!: string;
17
+ <%_ } else { _%>
18
+ /** The id of the session, the one of its cookie. */
19
+ @PrimaryColumn()
20
+ sid!: string;
21
+ <%_ } _%>
22
+
23
+ /** The login of the user of the session, its sessions listed by the sessions page. */
24
+ @Index()
25
+ @Column({ nullable: true })
26
+ login?: string;
27
+
28
+ /** The end of the session, extended by its requests. */
29
+ @Index()
30
+ @Column()
31
+ expires!: Date;
32
+
33
+ /** The data of the session, as JSON. */
34
+ @Column('text')
35
+ data!: string;
36
+ }
@@ -0,0 +1,99 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { promisify } from 'node:util';
3
+ import { Session } from './session.entity.ts';
4
+ import { SessionStore } from './session.store.ts';
5
+
6
+ /** The threshold of a criterion on the expiry, a LessThan of TypeORM or a $lt of MongoDB. */
7
+ const lessThan = (criterion: any): number => new Date(criterion.value ?? criterion.$lt).getTime();
8
+
9
+ /** A repository of the sessions in memory, with the queries the store makes. */
10
+ const repository = (rows: Session[]): any => ({
11
+ findOne: ({ where }: any) => Promise.resolve(rows.find(row => row.sid === where.sid) ?? null),
12
+ find: (options?: any) => Promise.resolve(rows.filter(row => !options?.where?.login || row.login === options.where.login)),
13
+ save: (entity: Session) => {
14
+ const index = rows.findIndex(row => row.sid === entity.sid);
15
+ rows.splice(index < 0 ? rows.length : index, index < 0 ? 0 : 1, { ...entity });
16
+ return Promise.resolve(entity);
17
+ },
18
+ update: ({ sid }: any, values: Partial<Session>) => Promise.resolve(Object.assign(rows.find(row => row.sid === sid) ?? {}, values)),
19
+ delete: (criteria: any) => {
20
+ const kept = rows.filter(row => (criteria.sid ? row.sid !== criteria.sid : row.expires.getTime() >= lessThan(criteria.expires)));
21
+ rows.splice(0, rows.length, ...kept);
22
+ return Promise.resolve();
23
+ },
24
+ });
25
+
26
+ /** All the sessions of the store, by their ids. */
27
+ const allOf = (store: SessionStore): Promise<Record<string, unknown>> =>
28
+ new Promise((resolve, reject) => store.all((error, sessions) => (error ? reject(error as Error) : resolve(sessions ?? {}))));
29
+
30
+ const sessionData = (login: string | undefined, expires: Date): any => ({ cookie: { expires: expires.toISOString() }, ...(login ? { user: { login } } : {}) });
31
+
32
+ describe('SessionStore', () => {
33
+ let rows: Session[];
34
+ let store: SessionStore;
35
+ const inOneHour = new Date(Date.now() + 60 * 60 * 1000);
36
+ const anHourAgo = new Date(Date.now() - 60 * 60 * 1000);
37
+
38
+ beforeEach(() => {
39
+ rows = [];
40
+ store = new SessionStore(repository(rows));
41
+ });
42
+
43
+ it('saves a session with the login of its user and the expiry of its cookie, and gives it back', async () => {
44
+ await promisify(store.set.bind(store))('s1', sessionData('user', inOneHour));
45
+ await promisify(store.set.bind(store))('s1', { ...sessionData('user', inOneHour), series: 'a' });
46
+ expect(rows).toHaveLength(1);
47
+ expect(rows[0]).toMatchObject({ sid: 's1', login: 'user', expires: inOneHour });
48
+ expect(await promisify(store.get.bind(store))('s1')).toMatchObject({ user: { login: 'user' }, series: 'a' });
49
+ });
50
+
51
+ it('gives no session for an expired or unknown id', async () => {
52
+ rows.push({ sid: 'old', login: 'user', expires: anHourAgo, data: '{}' });
53
+ expect(await promisify(store.get.bind(store))('old')).toBeNull();
54
+ expect(await promisify(store.get.bind(store))('unknown')).toBeNull();
55
+ });
56
+
57
+ it('extends a session its request did not change', async () => {
58
+ rows.push({ sid: 's1', login: 'user', expires: anHourAgo, data: '{}' });
59
+ await promisify(store.touch.bind(store))('s1', sessionData('user', inOneHour));
60
+ expect(rows[0].expires).toEqual(inOneHour);
61
+ });
62
+
63
+ it('ends a session', async () => {
64
+ rows.push({ sid: 's1', login: 'user', expires: inOneHour, data: '{}' });
65
+ await promisify(store.destroy.bind(store))('s1');
66
+ expect(rows).toHaveLength(0);
67
+ });
68
+
69
+ it('lists the sessions not expired, of every user and of a user', async () => {
70
+ rows.push(
71
+ { sid: 's1', login: 'user', expires: inOneHour, data: '{"a":1}' },
72
+ { sid: 's2', login: 'other', expires: inOneHour, data: '{}' },
73
+ { sid: 's3', login: 'user', expires: anHourAgo, data: '{}' },
74
+ );
75
+ expect(Object.keys(await allOf(store))).toEqual(['s1', 's2']);
76
+ expect(await store.sessionsOf('user')).toEqual([{ sid: 's1', data: { a: 1 } }]);
77
+ });
78
+
79
+ it('waits for the operations in progress before the database is closed, like a touch ending a response', async () => {
80
+ let finish!: () => void;
81
+ const slow = new SessionStore({ ...repository(rows), update: () => new Promise<void>(resolve => (finish = resolve)) });
82
+ let touched = false;
83
+ slow.touch('s1', sessionData('user', inOneHour), () => (touched = true));
84
+ let closed = false;
85
+ const closing = slow.onModuleDestroy().then(() => (closed = true));
86
+ await Promise.resolve();
87
+ expect(closed).toBe(false);
88
+ finish();
89
+ await closing;
90
+ expect(touched).toBe(true);
91
+ expect(closed).toBe(true);
92
+ });
93
+
94
+ it('removes the expired sessions', async () => {
95
+ rows.push({ sid: 's1', login: 'user', expires: inOneHour, data: '{}' }, { sid: 's2', login: 'user', expires: anHourAgo, data: '{}' });
96
+ await store.prune();
97
+ expect(rows.map(row => row.sid)).toEqual(['s1']);
98
+ });
99
+ });