@furystack/rest-service 4.0.19

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 (285) hide show
  1. package/LICENSE +339 -0
  2. package/README.md +219 -0
  3. package/dist/actions/error-action.d.ts +14 -0
  4. package/dist/actions/error-action.d.ts.map +1 -0
  5. package/dist/actions/error-action.js +29 -0
  6. package/dist/actions/error-action.js.map +1 -0
  7. package/dist/actions/error-action.spec.d.ts +2 -0
  8. package/dist/actions/error-action.spec.d.ts.map +1 -0
  9. package/dist/actions/error-action.spec.js +51 -0
  10. package/dist/actions/error-action.spec.js.map +1 -0
  11. package/dist/actions/get-current-user.d.ts +11 -0
  12. package/dist/actions/get-current-user.d.ts.map +1 -0
  13. package/dist/actions/get-current-user.js +15 -0
  14. package/dist/actions/get-current-user.js.map +1 -0
  15. package/dist/actions/get-current-user.spec.d.ts +2 -0
  16. package/dist/actions/get-current-user.spec.d.ts.map +1 -0
  17. package/dist/actions/get-current-user.spec.js +20 -0
  18. package/dist/actions/get-current-user.spec.js.map +1 -0
  19. package/dist/actions/index.d.ts +7 -0
  20. package/dist/actions/index.d.ts.map +1 -0
  21. package/dist/actions/index.js +10 -0
  22. package/dist/actions/index.js.map +1 -0
  23. package/dist/actions/is-authenticated.d.ts +14 -0
  24. package/dist/actions/is-authenticated.d.ts.map +1 -0
  25. package/dist/actions/is-authenticated.js +17 -0
  26. package/dist/actions/is-authenticated.js.map +1 -0
  27. package/dist/actions/is-authenticated.spec.d.ts +2 -0
  28. package/dist/actions/is-authenticated.spec.d.ts.map +1 -0
  29. package/dist/actions/is-authenticated.spec.js +19 -0
  30. package/dist/actions/is-authenticated.spec.js.map +1 -0
  31. package/dist/actions/login-action.spec.d.ts +2 -0
  32. package/dist/actions/login-action.spec.d.ts.map +1 -0
  33. package/dist/actions/login-action.spec.js +35 -0
  34. package/dist/actions/login-action.spec.js.map +1 -0
  35. package/dist/actions/login.d.ts +16 -0
  36. package/dist/actions/login.d.ts.map +1 -0
  37. package/dist/actions/login.js +26 -0
  38. package/dist/actions/login.js.map +1 -0
  39. package/dist/actions/logout-action.spec.d.ts +2 -0
  40. package/dist/actions/logout-action.spec.d.ts.map +1 -0
  41. package/dist/actions/logout-action.spec.js +23 -0
  42. package/dist/actions/logout-action.spec.js.map +1 -0
  43. package/dist/actions/logout.d.ts +14 -0
  44. package/dist/actions/logout.d.ts.map +1 -0
  45. package/dist/actions/logout.js +20 -0
  46. package/dist/actions/logout.js.map +1 -0
  47. package/dist/actions/not-found-action.d.ts +10 -0
  48. package/dist/actions/not-found-action.d.ts.map +1 -0
  49. package/dist/actions/not-found-action.js +14 -0
  50. package/dist/actions/not-found-action.js.map +1 -0
  51. package/dist/actions/not-found-action.spec.d.ts +2 -0
  52. package/dist/actions/not-found-action.spec.d.ts.map +1 -0
  53. package/dist/actions/not-found-action.spec.js +17 -0
  54. package/dist/actions/not-found-action.spec.js.map +1 -0
  55. package/dist/add-cors-header.spec.d.ts +2 -0
  56. package/dist/add-cors-header.spec.d.ts.map +1 -0
  57. package/dist/add-cors-header.spec.js +99 -0
  58. package/dist/add-cors-header.spec.js.map +1 -0
  59. package/dist/api-manager.d.ts +61 -0
  60. package/dist/api-manager.d.ts.map +1 -0
  61. package/dist/api-manager.js +144 -0
  62. package/dist/api-manager.js.map +1 -0
  63. package/dist/authenticate.d.ts +5 -0
  64. package/dist/authenticate.d.ts.map +1 -0
  65. package/dist/authenticate.js +20 -0
  66. package/dist/authenticate.js.map +1 -0
  67. package/dist/authenticate.spec.d.ts +2 -0
  68. package/dist/authenticate.spec.d.ts.map +1 -0
  69. package/dist/authenticate.spec.js +59 -0
  70. package/dist/authenticate.spec.js.map +1 -0
  71. package/dist/authorize.d.ts +5 -0
  72. package/dist/authorize.d.ts.map +1 -0
  73. package/dist/authorize.js +22 -0
  74. package/dist/authorize.js.map +1 -0
  75. package/dist/authorize.spec.d.ts +2 -0
  76. package/dist/authorize.spec.d.ts.map +1 -0
  77. package/dist/authorize.spec.js +55 -0
  78. package/dist/authorize.spec.js.map +1 -0
  79. package/dist/endpoint-generators/create-delete-endpoint.d.ts +17 -0
  80. package/dist/endpoint-generators/create-delete-endpoint.d.ts.map +1 -0
  81. package/dist/endpoint-generators/create-delete-endpoint.js +24 -0
  82. package/dist/endpoint-generators/create-delete-endpoint.js.map +1 -0
  83. package/dist/endpoint-generators/create-delete-endpoint.spec.d.ts +2 -0
  84. package/dist/endpoint-generators/create-delete-endpoint.spec.d.ts.map +1 -0
  85. package/dist/endpoint-generators/create-delete-endpoint.spec.js +33 -0
  86. package/dist/endpoint-generators/create-delete-endpoint.spec.js.map +1 -0
  87. package/dist/endpoint-generators/create-get-collection-endpoint.d.ts +17 -0
  88. package/dist/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -0
  89. package/dist/endpoint-generators/create-get-collection-endpoint.js +26 -0
  90. package/dist/endpoint-generators/create-get-collection-endpoint.js.map +1 -0
  91. package/dist/endpoint-generators/create-get-collection-endpoint.spec.d.ts +2 -0
  92. package/dist/endpoint-generators/create-get-collection-endpoint.spec.d.ts.map +1 -0
  93. package/dist/endpoint-generators/create-get-collection-endpoint.spec.js +143 -0
  94. package/dist/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -0
  95. package/dist/endpoint-generators/create-get-entity-endpoint.d.ts +17 -0
  96. package/dist/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -0
  97. package/dist/endpoint-generators/create-get-entity-endpoint.js +29 -0
  98. package/dist/endpoint-generators/create-get-entity-endpoint.js.map +1 -0
  99. package/dist/endpoint-generators/create-get-entity-endpoint.spec.d.ts +2 -0
  100. package/dist/endpoint-generators/create-get-entity-endpoint.spec.d.ts.map +1 -0
  101. package/dist/endpoint-generators/create-get-entity-endpoint.spec.js +74 -0
  102. package/dist/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -0
  103. package/dist/endpoint-generators/create-patch-endpoint.d.ts +18 -0
  104. package/dist/endpoint-generators/create-patch-endpoint.d.ts.map +1 -0
  105. package/dist/endpoint-generators/create-patch-endpoint.js +26 -0
  106. package/dist/endpoint-generators/create-patch-endpoint.js.map +1 -0
  107. package/dist/endpoint-generators/create-patch-endpoint.spec.d.ts +2 -0
  108. package/dist/endpoint-generators/create-patch-endpoint.spec.d.ts.map +1 -0
  109. package/dist/endpoint-generators/create-patch-endpoint.spec.js +36 -0
  110. package/dist/endpoint-generators/create-patch-endpoint.spec.js.map +1 -0
  111. package/dist/endpoint-generators/create-post-endpoint.d.ts +18 -0
  112. package/dist/endpoint-generators/create-post-endpoint.d.ts.map +1 -0
  113. package/dist/endpoint-generators/create-post-endpoint.js +29 -0
  114. package/dist/endpoint-generators/create-post-endpoint.js.map +1 -0
  115. package/dist/endpoint-generators/create-post-endpoint.spec.d.ts +2 -0
  116. package/dist/endpoint-generators/create-post-endpoint.spec.d.ts.map +1 -0
  117. package/dist/endpoint-generators/create-post-endpoint.spec.js +34 -0
  118. package/dist/endpoint-generators/create-post-endpoint.spec.js.map +1 -0
  119. package/dist/endpoint-generators/index.d.ts +6 -0
  120. package/dist/endpoint-generators/index.d.ts.map +1 -0
  121. package/dist/endpoint-generators/index.js +9 -0
  122. package/dist/endpoint-generators/index.js.map +1 -0
  123. package/dist/endpoint-generators/utils.d.ts +9 -0
  124. package/dist/endpoint-generators/utils.d.ts.map +1 -0
  125. package/dist/endpoint-generators/utils.js +27 -0
  126. package/dist/endpoint-generators/utils.js.map +1 -0
  127. package/dist/http-authentication-settings.d.ts +17 -0
  128. package/dist/http-authentication-settings.d.ts.map +1 -0
  129. package/dist/http-authentication-settings.js +26 -0
  130. package/dist/http-authentication-settings.js.map +1 -0
  131. package/dist/http-user-context.d.ts +54 -0
  132. package/dist/http-user-context.d.ts.map +1 -0
  133. package/dist/http-user-context.js +153 -0
  134. package/dist/http-user-context.js.map +1 -0
  135. package/dist/http-user-context.spec.d.ts +4 -0
  136. package/dist/http-user-context.spec.d.ts.map +1 -0
  137. package/dist/http-user-context.spec.js +267 -0
  138. package/dist/http-user-context.spec.js.map +1 -0
  139. package/dist/incoming-message-extensions.d.ts +8 -0
  140. package/dist/incoming-message-extensions.d.ts.map +1 -0
  141. package/dist/incoming-message-extensions.js +14 -0
  142. package/dist/incoming-message-extensions.js.map +1 -0
  143. package/dist/incoming-message-extensions.spec.d.ts +2 -0
  144. package/dist/incoming-message-extensions.spec.d.ts.map +1 -0
  145. package/dist/incoming-message-extensions.spec.js +39 -0
  146. package/dist/incoming-message-extensions.spec.js.map +1 -0
  147. package/dist/index.d.ts +17 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +20 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/injector-extensions.d.ts +21 -0
  152. package/dist/injector-extensions.d.ts.map +1 -0
  153. package/dist/injector-extensions.js +14 -0
  154. package/dist/injector-extensions.js.map +1 -0
  155. package/dist/injector-extensions.spec.d.ts +2 -0
  156. package/dist/injector-extensions.spec.d.ts.map +1 -0
  157. package/dist/injector-extensions.spec.js +19 -0
  158. package/dist/injector-extensions.spec.js.map +1 -0
  159. package/dist/models/cors-options.d.ts +22 -0
  160. package/dist/models/cors-options.d.ts.map +1 -0
  161. package/dist/models/cors-options.js +3 -0
  162. package/dist/models/cors-options.js.map +1 -0
  163. package/dist/models/default-session.d.ts +14 -0
  164. package/dist/models/default-session.d.ts.map +1 -0
  165. package/dist/models/default-session.js +10 -0
  166. package/dist/models/default-session.js.map +1 -0
  167. package/dist/request-action-implementation.d.ts +54 -0
  168. package/dist/request-action-implementation.d.ts.map +1 -0
  169. package/dist/request-action-implementation.js +42 -0
  170. package/dist/request-action-implementation.js.map +1 -0
  171. package/dist/rest-service.integration.spec.d.ts +2 -0
  172. package/dist/rest-service.integration.spec.d.ts.map +1 -0
  173. package/dist/rest-service.integration.spec.js +129 -0
  174. package/dist/rest-service.integration.spec.js.map +1 -0
  175. package/dist/rest.integration.test.d.ts +58 -0
  176. package/dist/rest.integration.test.d.ts.map +1 -0
  177. package/dist/rest.integration.test.js +94 -0
  178. package/dist/rest.integration.test.js.map +1 -0
  179. package/dist/schema-validator/index.d.ts +3 -0
  180. package/dist/schema-validator/index.d.ts.map +1 -0
  181. package/dist/schema-validator/index.js +6 -0
  182. package/dist/schema-validator/index.js.map +1 -0
  183. package/dist/schema-validator/schema-validation-error.d.ts +10 -0
  184. package/dist/schema-validator/schema-validation-error.d.ts.map +1 -0
  185. package/dist/schema-validator/schema-validation-error.js +15 -0
  186. package/dist/schema-validator/schema-validation-error.js.map +1 -0
  187. package/dist/schema-validator/schema-validator.d.ts +20 -0
  188. package/dist/schema-validator/schema-validator.d.ts.map +1 -0
  189. package/dist/schema-validator/schema-validator.js +36 -0
  190. package/dist/schema-validator/schema-validator.js.map +1 -0
  191. package/dist/schema-validator/schema-validator.test.d.ts +2 -0
  192. package/dist/schema-validator/schema-validator.test.d.ts.map +1 -0
  193. package/dist/schema-validator/schema-validator.test.js +62 -0
  194. package/dist/schema-validator/schema-validator.test.js.map +1 -0
  195. package/dist/schema-validator/validate-examples.d.ts +37 -0
  196. package/dist/schema-validator/validate-examples.d.ts.map +1 -0
  197. package/dist/schema-validator/validate-examples.js +29 -0
  198. package/dist/schema-validator/validate-examples.js.map +1 -0
  199. package/dist/server-manager.d.ts +30 -0
  200. package/dist/server-manager.d.ts.map +1 -0
  201. package/dist/server-manager.js +71 -0
  202. package/dist/server-manager.js.map +1 -0
  203. package/dist/server-response-extensions.d.ts +21 -0
  204. package/dist/server-response-extensions.d.ts.map +1 -0
  205. package/dist/server-response-extensions.js +15 -0
  206. package/dist/server-response-extensions.js.map +1 -0
  207. package/dist/server-response-extensions.spec.d.ts +2 -0
  208. package/dist/server-response-extensions.spec.d.ts.map +1 -0
  209. package/dist/server-response-extensions.spec.js +49 -0
  210. package/dist/server-response-extensions.spec.js.map +1 -0
  211. package/dist/utils.d.ts +24 -0
  212. package/dist/utils.d.ts.map +1 -0
  213. package/dist/utils.js +66 -0
  214. package/dist/utils.js.map +1 -0
  215. package/dist/validate.d.ts +18 -0
  216. package/dist/validate.d.ts.map +1 -0
  217. package/dist/validate.integration.schema.d.ts +69 -0
  218. package/dist/validate.integration.schema.d.ts.map +1 -0
  219. package/dist/validate.integration.schema.js +3 -0
  220. package/dist/validate.integration.schema.js.map +1 -0
  221. package/dist/validate.integration.spec.d.ts +13 -0
  222. package/dist/validate.integration.spec.d.ts.map +1 -0
  223. package/dist/validate.integration.spec.js +223 -0
  224. package/dist/validate.integration.spec.js.map +1 -0
  225. package/dist/validate.integration.spec.schema.json +749 -0
  226. package/dist/validate.js +49 -0
  227. package/dist/validate.js.map +1 -0
  228. package/package.json +56 -0
  229. package/src/actions/error-action.spec.ts +54 -0
  230. package/src/actions/error-action.ts +34 -0
  231. package/src/actions/get-current-user.spec.ts +23 -0
  232. package/src/actions/get-current-user.ts +15 -0
  233. package/src/actions/index.ts +6 -0
  234. package/src/actions/is-authenticated.spec.ts +18 -0
  235. package/src/actions/is-authenticated.ts +13 -0
  236. package/src/actions/login-action.spec.ts +41 -0
  237. package/src/actions/login.ts +26 -0
  238. package/src/actions/logout-action.spec.ts +27 -0
  239. package/src/actions/logout.ts +16 -0
  240. package/src/actions/not-found-action.spec.ts +17 -0
  241. package/src/actions/not-found-action.ts +13 -0
  242. package/src/add-cors-header.spec.ts +133 -0
  243. package/src/api-manager.ts +222 -0
  244. package/src/authenticate.spec.ts +78 -0
  245. package/src/authenticate.ts +22 -0
  246. package/src/authorize.spec.ts +69 -0
  247. package/src/authorize.ts +19 -0
  248. package/src/endpoint-generators/create-delete-endpoint.spec.ts +34 -0
  249. package/src/endpoint-generators/create-delete-endpoint.ts +25 -0
  250. package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +164 -0
  251. package/src/endpoint-generators/create-get-collection-endpoint.ts +28 -0
  252. package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +75 -0
  253. package/src/endpoint-generators/create-get-entity-endpoint.ts +29 -0
  254. package/src/endpoint-generators/create-patch-endpoint.spec.ts +36 -0
  255. package/src/endpoint-generators/create-patch-endpoint.ts +27 -0
  256. package/src/endpoint-generators/create-post-endpoint.spec.ts +32 -0
  257. package/src/endpoint-generators/create-post-endpoint.ts +30 -0
  258. package/src/endpoint-generators/index.ts +5 -0
  259. package/src/endpoint-generators/utils.ts +34 -0
  260. package/src/http-authentication-settings.ts +23 -0
  261. package/src/http-user-context.spec.ts +299 -0
  262. package/src/http-user-context.ts +160 -0
  263. package/src/incoming-message-extensions.spec.ts +41 -0
  264. package/src/incoming-message-extensions.ts +19 -0
  265. package/src/index.ts +16 -0
  266. package/src/injector-extensions.spec.ts +19 -0
  267. package/src/injector-extensions.ts +35 -0
  268. package/src/models/cors-options.ts +21 -0
  269. package/src/models/default-session.ts +14 -0
  270. package/src/request-action-implementation.ts +70 -0
  271. package/src/rest-service.integration.spec.ts +166 -0
  272. package/src/rest.integration.test.ts +112 -0
  273. package/src/schema-validator/index.ts +2 -0
  274. package/src/schema-validator/schema-validation-error.ts +11 -0
  275. package/src/schema-validator/schema-validator.test.ts +72 -0
  276. package/src/schema-validator/schema-validator.ts +31 -0
  277. package/src/schema-validator/validate-examples.ts +38 -0
  278. package/src/server-manager.ts +88 -0
  279. package/src/server-response-extensions.spec.ts +53 -0
  280. package/src/server-response-extensions.ts +30 -0
  281. package/src/utils.ts +65 -0
  282. package/src/validate.integration.schema.ts +50 -0
  283. package/src/validate.integration.spec.schema.json +779 -0
  284. package/src/validate.integration.spec.ts +218 -0
  285. package/src/validate.ts +60 -0
@@ -0,0 +1,5 @@
1
+ export * from './create-delete-endpoint'
2
+ export * from './create-get-collection-endpoint'
3
+ export * from './create-get-entity-endpoint'
4
+ export * from './create-patch-endpoint'
5
+ export * from './create-post-endpoint'
@@ -0,0 +1,34 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { InMemoryStore, User } from '@furystack/core'
3
+ import { DefaultSession } from '../models/default-session'
4
+ import '@furystack/repository'
5
+ import '../injector-extensions'
6
+
7
+ export class MockClass {
8
+ id!: string
9
+ value!: string
10
+ }
11
+
12
+ export const setupContext = (i: Injector) => {
13
+ i.setupStores((b) =>
14
+ b
15
+ .addStore(
16
+ new InMemoryStore({
17
+ model: MockClass,
18
+ primaryKey: 'id',
19
+ }),
20
+ )
21
+ .addStore(
22
+ new InMemoryStore({
23
+ model: User,
24
+ primaryKey: 'username',
25
+ }),
26
+ )
27
+ .addStore(
28
+ new InMemoryStore({
29
+ model: DefaultSession,
30
+ primaryKey: 'sessionId',
31
+ }),
32
+ ),
33
+ ).setupRepository((r) => r.createDataSet(MockClass, 'id'))
34
+ }
@@ -0,0 +1,23 @@
1
+ import { PhysicalStore, User, StoreManager } from '@furystack/core'
2
+ import { Constructable, Injectable } from '@furystack/inject'
3
+ import { sha256 } from 'hash.js'
4
+ import { DefaultSession } from './models/default-session'
5
+
6
+ /**
7
+ * Authentication settings object for FuryStack HTTP Api
8
+ */
9
+ @Injectable({ lifetime: 'singleton' })
10
+ export class HttpAuthenticationSettings<TUser extends User, TSession extends DefaultSession> {
11
+ public model: Constructable<TUser> = User as Constructable<TUser>
12
+
13
+ public getUserStore: (storeManager: StoreManager) => PhysicalStore<TUser & { password: string }, keyof TUser> = (
14
+ sm,
15
+ ) => sm.getStoreFor<TUser & { password: string }, keyof TUser>(User as any, 'username')
16
+
17
+ public getSessionStore: (storeManager: StoreManager) => PhysicalStore<TSession, keyof TSession> = (sm) =>
18
+ sm.getStoreFor(DefaultSession, 'sessionId') as unknown as PhysicalStore<TSession, keyof TSession>
19
+
20
+ public cookieName = 'fss'
21
+ public hashMethod: (plain: string) => string = (plain) => sha256().update(plain).digest('hex')
22
+ public enableBasicAuth = true
23
+ }
@@ -0,0 +1,299 @@
1
+ import { IncomingMessage, ServerResponse } from 'http'
2
+ import { usingAsync } from '@furystack/utils'
3
+ import { Injector } from '@furystack/inject'
4
+ import { User, StoreManager, InMemoryStore } from '@furystack/core'
5
+ import { DefaultSession } from './models/default-session'
6
+ import { HttpUserContext } from './http-user-context'
7
+ import './injector-extensions'
8
+
9
+ export const prepareInjector = async (i: Injector) => {
10
+ i.setupStores((sm) =>
11
+ sm
12
+ .addStore(new InMemoryStore({ model: User, primaryKey: 'username' }))
13
+ .addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' })),
14
+ )
15
+
16
+ i.useHttpAuthentication()
17
+ // await i.getInstance(ServerManager).getOrCreate({ port: 19999 })
18
+ }
19
+
20
+ describe('HttpUserContext', () => {
21
+ const request = { headers: {} } as IncomingMessage
22
+ const response = {} as any as ServerResponse
23
+
24
+ const testUser: User = { username: 'testUser', roles: ['grantedRole1', 'grantedRole2'] }
25
+
26
+ it('Should be constructed with the extension method', async () => {
27
+ await usingAsync(new Injector(), async (i) => {
28
+ await prepareInjector(i)
29
+ const ctx = i.getInstance(HttpUserContext)
30
+ expect(ctx).toBeInstanceOf(HttpUserContext)
31
+ })
32
+ })
33
+
34
+ describe('isAuthenticated', () => {
35
+ it('Should return true for authenticated users', async () => {
36
+ await usingAsync(new Injector(), async (i) => {
37
+ await prepareInjector(i)
38
+ const ctx = i.getInstance(HttpUserContext)
39
+ ctx.getCurrentUser = jest.fn(async () => testUser)
40
+ const value = await ctx.isAuthenticated(request)
41
+ expect(value).toBe(true)
42
+ expect(ctx.getCurrentUser).toBeCalled()
43
+ })
44
+ })
45
+
46
+ it('Should return false for unauthenticated users', async () => {
47
+ await usingAsync(new Injector(), async (i) => {
48
+ await prepareInjector(i)
49
+ const ctx = i.getInstance(HttpUserContext)
50
+ ctx.getCurrentUser = jest.fn(async () => {
51
+ throw Error(':(')
52
+ })
53
+ await expect(ctx.isAuthenticated(request)).resolves.toEqual(false)
54
+ expect(ctx.getCurrentUser).toBeCalled()
55
+ })
56
+ })
57
+ })
58
+
59
+ describe('isAuthorized', () => {
60
+ it('Should return true if all roles are authorized', async () => {
61
+ await usingAsync(new Injector(), async (i) => {
62
+ await prepareInjector(i)
63
+ const ctx = i.getInstance(HttpUserContext)
64
+ ctx.getCurrentUser = jest.fn(async () => testUser)
65
+ const value = await ctx.isAuthorized(request, 'grantedRole1', 'grantedRole2')
66
+ expect(value).toBe(true)
67
+ expect(ctx.getCurrentUser).toBeCalled()
68
+ })
69
+ })
70
+
71
+ it('Should return false if not all roles are authorized', async () => {
72
+ await usingAsync(new Injector(), async (i) => {
73
+ await prepareInjector(i)
74
+ const ctx = i.getInstance(HttpUserContext)
75
+ ctx.getCurrentUser = jest.fn(async () => testUser)
76
+ const value = await ctx.isAuthorized(request, 'grantedRole1', 'nonGrantedRole2')
77
+ expect(value).toBe(false)
78
+ expect(ctx.getCurrentUser).toBeCalled()
79
+ })
80
+ })
81
+ })
82
+
83
+ describe('authenticateUser', () => {
84
+ it('Should fail when the store is empty', async () => {
85
+ await usingAsync(new Injector(), async (i) => {
86
+ await prepareInjector(i)
87
+ const ctx = i.getInstance(HttpUserContext)
88
+ await expect(ctx.authenticateUser('user', 'password')).rejects.toThrow('')
89
+ })
90
+ })
91
+
92
+ it('Should fail when the password not equals', async () => {
93
+ await usingAsync(new Injector(), async (i) => {
94
+ await prepareInjector(i)
95
+ const ctx = i.getInstance(HttpUserContext)
96
+ ctx.authentication
97
+ .getUserStore(i.getInstance(StoreManager))
98
+ .add({ username: 'user', password: ctx.authentication.hashMethod('pass123'), roles: [] })
99
+ await expect(ctx.authenticateUser('user', 'pass321')).rejects.toThrow('')
100
+ })
101
+ })
102
+
103
+ it('Should fail when the username not equals', async () => {
104
+ await usingAsync(new Injector(), async (i) => {
105
+ await prepareInjector(i)
106
+ const ctx = i.getInstance(HttpUserContext)
107
+ ctx.authentication
108
+ .getUserStore(i.getInstance(StoreManager))
109
+ .add({ username: 'otherUser', password: ctx.authentication.hashMethod('pass123'), roles: [] })
110
+ expect(ctx.authenticateUser('user', 'pass123')).rejects.toThrow('')
111
+ })
112
+ })
113
+
114
+ it('Should fail when password not provided', async () => {
115
+ await usingAsync(new Injector(), async (i) => {
116
+ await prepareInjector(i)
117
+ const ctx = i.getInstance(HttpUserContext)
118
+ ctx.authentication
119
+ .getUserStore(i.getInstance(StoreManager))
120
+ .add({ username: 'otherUser', password: ctx.authentication.hashMethod('pass123'), roles: [] })
121
+ await expect(ctx.authenticateUser('user', '')).rejects.toThrow('')
122
+ })
123
+ })
124
+
125
+ it('Should return the user without the password hash when the username and password matches', async () => {
126
+ await usingAsync(new Injector(), async (i) => {
127
+ await prepareInjector(i)
128
+ const ctx = i.getInstance(HttpUserContext)
129
+ const store = ctx.authentication.getUserStore(i.getInstance(StoreManager))
130
+ const loginUser = { username: 'user', roles: [] }
131
+ store.add({ ...loginUser, password: ctx.authentication.hashMethod('pass123') })
132
+ const value = await ctx.authenticateUser('user', 'pass123')
133
+ expect(value).toEqual(loginUser)
134
+ })
135
+ })
136
+ })
137
+
138
+ describe('getSessionIdFromRequest', () => {
139
+ it('Should return null if no headers present', async () => {
140
+ await usingAsync(new Injector(), async (i) => {
141
+ await prepareInjector(i)
142
+ const ctx = i.getInstance(HttpUserContext)
143
+ const sid = ctx.getSessionIdFromRequest(request)
144
+ expect(sid).toBeNull()
145
+ })
146
+ })
147
+
148
+ it('Should return null if no session ID cookie present', async () => {
149
+ await usingAsync(new Injector(), async (i) => {
150
+ await prepareInjector(i)
151
+ const requestWithCookie = { ...request, cookie: 'a=2;b=3;c=4;' } as unknown as IncomingMessage
152
+ const ctx = i.getInstance(HttpUserContext)
153
+ const sid = ctx.getSessionIdFromRequest(requestWithCookie)
154
+ expect(sid).toBeNull()
155
+ })
156
+ })
157
+ it('Should return the Session ID value if session ID cookie present', async () => {
158
+ await usingAsync(new Injector(), async (i) => {
159
+ await prepareInjector(i)
160
+ const ctx = i.getInstance(HttpUserContext)
161
+ const requestWithAuthCookie = {
162
+ ...request,
163
+ headers: { cookie: `a=2;b=3;${ctx.authentication.cookieName}=666;c=4;` },
164
+ } as unknown as IncomingMessage
165
+
166
+ const sid = ctx.getSessionIdFromRequest(requestWithAuthCookie)
167
+ expect(sid).toBe('666')
168
+ })
169
+ })
170
+ })
171
+
172
+ describe('authenticateRequest', () => {
173
+ it('Should try to authenticate with Basic, if enabled', async () => {
174
+ await usingAsync(new Injector(), async (i) => {
175
+ await prepareInjector(i)
176
+ const ctx = i.getInstance(HttpUserContext)
177
+ ctx.authenticateUser = jest.fn(async () => testUser)
178
+ const result = await ctx.authenticateRequest({
179
+ headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
180
+ } as IncomingMessage)
181
+ expect(ctx.authenticateUser).toBeCalledWith('testuser', 'password')
182
+ expect(result).toBe(testUser)
183
+ })
184
+ })
185
+
186
+ it('Should NOT try to authenticate with Basic, if disabled', async () => {
187
+ await usingAsync(new Injector(), async (i) => {
188
+ await prepareInjector(i)
189
+ const ctx = i.getInstance(HttpUserContext)
190
+ ctx.authentication.enableBasicAuth = false
191
+ ctx.authenticateUser = jest.fn(async () => testUser)
192
+ await expect(
193
+ ctx.authenticateRequest({
194
+ headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
195
+ } as IncomingMessage),
196
+ ).rejects.toThrow('')
197
+ expect(ctx.authenticateUser).not.toBeCalled()
198
+ })
199
+ })
200
+
201
+ it('Should fail with no session in the store', async () => {
202
+ await usingAsync(new Injector(), async (i) => {
203
+ await prepareInjector(i)
204
+ const ctx = i.getInstance(HttpUserContext)
205
+ await expect(
206
+ ctx.authenticateRequest({
207
+ headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
208
+ } as IncomingMessage),
209
+ ).rejects.toThrow('')
210
+ })
211
+ })
212
+
213
+ it('Should fail with valid session Id but no user', async () => {
214
+ await usingAsync(new Injector(), async (i) => {
215
+ await prepareInjector(i)
216
+ const ctx = i.getInstance(HttpUserContext)
217
+ ctx.authentication
218
+ .getSessionStore(i.getInstance(StoreManager))
219
+ .add({ sessionId: '666', username: testUser.username })
220
+ await expect(
221
+ ctx.authenticateRequest({
222
+ headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
223
+ } as IncomingMessage),
224
+ ).rejects.toThrow('')
225
+ })
226
+ })
227
+
228
+ it('Should authenticate with cookie, if the session IDs matches', async () => {
229
+ await usingAsync(new Injector(), async (i) => {
230
+ await prepareInjector(i)
231
+ const ctx = i.getInstance(HttpUserContext)
232
+ ctx.authentication
233
+ .getSessionStore(i.getInstance(StoreManager))
234
+ .add({ sessionId: '666', username: testUser.username })
235
+
236
+ ctx.authentication.getUserStore(i.getInstance(StoreManager)).add({ ...testUser, password: '' })
237
+
238
+ const result = await ctx.authenticateRequest({
239
+ headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
240
+ } as IncomingMessage)
241
+
242
+ expect(result).toEqual(testUser)
243
+ })
244
+ })
245
+ })
246
+
247
+ describe('getCurrentUser', () => {
248
+ it('Should return the current user from authenticateRequest() once per request', async () => {
249
+ await usingAsync(new Injector(), async (i) => {
250
+ await prepareInjector(i)
251
+ const ctx = i.getInstance(HttpUserContext)
252
+ ctx.authenticateRequest = jest.fn(async () => testUser)
253
+ const result = await ctx.getCurrentUser(request)
254
+ const result2 = await ctx.getCurrentUser(request)
255
+ expect(ctx.authenticateRequest).toBeCalledTimes(1)
256
+ expect(result).toBe(testUser)
257
+ expect(result2).toBe(testUser)
258
+ })
259
+ })
260
+ })
261
+
262
+ describe('cookieLogin', () => {
263
+ it('Should return the current user from authenticateRequest() once per request', async () => {
264
+ await usingAsync(new Injector(), async (i) => {
265
+ await prepareInjector(i)
266
+ const ctx = i.getInstance(HttpUserContext)
267
+ const setHeader = jest.fn()
268
+ ctx.getSessionStore().add = jest.fn(async () => {
269
+ return {} as any
270
+ })
271
+ const authResult = await ctx.cookieLogin(testUser, { setHeader } as any)
272
+ expect(authResult).toBe(testUser)
273
+ expect(setHeader).toBeCalled()
274
+ expect(ctx.getSessionStore().add).toBeCalled()
275
+ })
276
+ })
277
+ })
278
+
279
+ describe('cookieLogout', () => {
280
+ it('Should invalidate the current session id cookie', async () => {
281
+ await usingAsync(new Injector(), async (i) => {
282
+ await prepareInjector(i)
283
+ const ctx = i.getInstance(HttpUserContext)
284
+ const setHeader = jest.fn()
285
+ ctx.getSessionStore().add = jest.fn(async () => {
286
+ return {} as any
287
+ })
288
+ ctx.authenticateRequest = jest.fn(async () => testUser)
289
+ ctx.getSessionStore().remove = jest.fn(async () => undefined)
290
+ ctx.getSessionIdFromRequest = () => 'example-session-id'
291
+ response.setHeader = jest.fn(() => response)
292
+ await ctx.cookieLogin(testUser, { setHeader } as any)
293
+ await ctx.cookieLogout(request, response)
294
+ expect(response.setHeader).toBeCalledWith('Set-Cookie', 'fss=; Path=/; HttpOnly')
295
+ expect(ctx.getSessionStore().remove).toBeCalled()
296
+ })
297
+ })
298
+ })
299
+ })
@@ -0,0 +1,160 @@
1
+ import { IncomingMessage, ServerResponse } from 'http'
2
+ import { User, StoreManager } from '@furystack/core'
3
+ import { Injectable } from '@furystack/inject'
4
+ import { v1 } from 'uuid'
5
+ import { HttpAuthenticationSettings } from './http-authentication-settings'
6
+ import { DefaultSession } from 'models/default-session'
7
+
8
+ /**
9
+ * Injectable UserContext for FuryStack HTTP Api
10
+ */
11
+ @Injectable({ lifetime: 'scoped' })
12
+ export class HttpUserContext {
13
+ public getUserStore = () => this.authentication.getUserStore(this.storeManager)
14
+
15
+ public getSessionStore = () => this.authentication.getSessionStore(this.storeManager)
16
+
17
+ private user?: User
18
+
19
+ /**
20
+ * @param request The request to be authenticated
21
+ * @returns if the current user is authenticated
22
+ */
23
+ public async isAuthenticated(request: IncomingMessage) {
24
+ try {
25
+ const currentUser = await this.getCurrentUser(request)
26
+ return currentUser !== null
27
+ } catch (error) {
28
+ return false
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Returns if the current user can be authorized with ALL of the specified roles
34
+ *
35
+ * @param request The request to be authenticated
36
+ * @param roles The list of roles to authorize
37
+ * @returns a boolean value that indicates if the user is authenticated
38
+ */
39
+ public async isAuthorized(request: IncomingMessage, ...roles: string[]): Promise<boolean> {
40
+ const currentUser = await this.getCurrentUser(request)
41
+ for (const role of roles) {
42
+ if (!currentUser || !currentUser.roles.some((c) => c === role)) {
43
+ return false
44
+ }
45
+ }
46
+ return true
47
+ }
48
+
49
+ /**
50
+ * Checks if the system contains a user with the provided name and password, throws an error otherwise
51
+ *
52
+ * @param userName The username
53
+ * @param password The password
54
+ * @returns the authenticated User
55
+ */
56
+ public async authenticateUser(userName: string, password: string) {
57
+ const match =
58
+ (password &&
59
+ password.length &&
60
+ (await this.getUserStore().find({
61
+ filter: {
62
+ username: { $eq: userName },
63
+ password: { $eq: this.authentication.hashMethod(password) },
64
+ },
65
+ }))) ||
66
+ []
67
+ if (match.length === 1) {
68
+ const { password: pw, ...user } = match[0]
69
+ return user
70
+ }
71
+ throw Error('Failed to authenticate.')
72
+ }
73
+
74
+ public async getCurrentUser(request: IncomingMessage) {
75
+ if (!this.user) {
76
+ this.user = await this.authenticateRequest(request)
77
+ return this.user
78
+ }
79
+ return this.user
80
+ }
81
+
82
+ public getSessionIdFromRequest(request: IncomingMessage): string | null {
83
+ if (request.headers.cookie) {
84
+ const cookies = request.headers.cookie
85
+ .toString()
86
+ .split(';')
87
+ .filter((val) => val.length > 0)
88
+ .map((val) => {
89
+ const [name, value] = val.split('=')
90
+ return { name: name.trim(), value: value.trim() }
91
+ })
92
+ const sessionCookie = cookies.find((c) => c.name === this.authentication.cookieName)
93
+ if (sessionCookie) {
94
+ return sessionCookie.value
95
+ }
96
+ }
97
+ return null
98
+ }
99
+
100
+ public async authenticateRequest(request: IncomingMessage): Promise<User> {
101
+ // Basic auth
102
+ if (this.authentication.enableBasicAuth && request.headers.authorization) {
103
+ const authData = Buffer.from(request.headers.authorization.toString().split(' ')[1], 'base64')
104
+ const [userName, password] = authData.toString().split(':')
105
+ return await this.authenticateUser(userName, password)
106
+ }
107
+
108
+ // Cookie auth
109
+ const sessionId = this.getSessionIdFromRequest(request)
110
+ if (sessionId) {
111
+ const [session] = await this.getSessionStore().find({ filter: { sessionId: { $eq: sessionId } }, top: 2 })
112
+ if (session) {
113
+ const userResult = await this.getUserStore().find({
114
+ filter: {
115
+ username: { $eq: session.username },
116
+ },
117
+ top: 2,
118
+ })
119
+ if (userResult.length === 1) {
120
+ const { password, ...user } = userResult[0]
121
+ return user
122
+ }
123
+ throw Error('Inconsistent session result')
124
+ }
125
+ }
126
+
127
+ throw Error('Failed to authenticate request')
128
+ }
129
+
130
+ /**
131
+ * Creates and sets up a cookie-based session for the provided user
132
+ *
133
+ * @param user The user to create a session for
134
+ * @param serverResponse A serverResponse to set the cookie
135
+ * @returns the current User
136
+ */
137
+ public async cookieLogin(user: User, serverResponse: ServerResponse): Promise<User> {
138
+ const sessionId = v1()
139
+ await this.getSessionStore().add({ sessionId, username: user.username })
140
+ serverResponse.setHeader('Set-Cookie', `${this.authentication.cookieName}=${sessionId}; Path=/; HttpOnly`)
141
+ this.user = user
142
+ return user
143
+ }
144
+
145
+ public async cookieLogout(request: IncomingMessage, response: ServerResponse) {
146
+ const sessionId = this.getSessionIdFromRequest(request)
147
+ response.setHeader('Set-Cookie', `${this.authentication.cookieName}=; Path=/; HttpOnly`)
148
+ this.user = undefined
149
+ if (sessionId) {
150
+ const sessionStore = this.getSessionStore()
151
+ const sessions = await sessionStore.find({ filter: { sessionId: { $eq: sessionId } } })
152
+ await this.getSessionStore().remove(...sessions.map((s) => s[sessionStore.primaryKey]))
153
+ }
154
+ }
155
+
156
+ constructor(
157
+ public readonly authentication: HttpAuthenticationSettings<User, DefaultSession>,
158
+ private readonly storeManager: StoreManager,
159
+ ) {}
160
+ }