@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.
- package/LICENSE +339 -0
- package/README.md +219 -0
- package/dist/actions/error-action.d.ts +14 -0
- package/dist/actions/error-action.d.ts.map +1 -0
- package/dist/actions/error-action.js +29 -0
- package/dist/actions/error-action.js.map +1 -0
- package/dist/actions/error-action.spec.d.ts +2 -0
- package/dist/actions/error-action.spec.d.ts.map +1 -0
- package/dist/actions/error-action.spec.js +51 -0
- package/dist/actions/error-action.spec.js.map +1 -0
- package/dist/actions/get-current-user.d.ts +11 -0
- package/dist/actions/get-current-user.d.ts.map +1 -0
- package/dist/actions/get-current-user.js +15 -0
- package/dist/actions/get-current-user.js.map +1 -0
- package/dist/actions/get-current-user.spec.d.ts +2 -0
- package/dist/actions/get-current-user.spec.d.ts.map +1 -0
- package/dist/actions/get-current-user.spec.js +20 -0
- package/dist/actions/get-current-user.spec.js.map +1 -0
- package/dist/actions/index.d.ts +7 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/is-authenticated.d.ts +14 -0
- package/dist/actions/is-authenticated.d.ts.map +1 -0
- package/dist/actions/is-authenticated.js +17 -0
- package/dist/actions/is-authenticated.js.map +1 -0
- package/dist/actions/is-authenticated.spec.d.ts +2 -0
- package/dist/actions/is-authenticated.spec.d.ts.map +1 -0
- package/dist/actions/is-authenticated.spec.js +19 -0
- package/dist/actions/is-authenticated.spec.js.map +1 -0
- package/dist/actions/login-action.spec.d.ts +2 -0
- package/dist/actions/login-action.spec.d.ts.map +1 -0
- package/dist/actions/login-action.spec.js +35 -0
- package/dist/actions/login-action.spec.js.map +1 -0
- package/dist/actions/login.d.ts +16 -0
- package/dist/actions/login.d.ts.map +1 -0
- package/dist/actions/login.js +26 -0
- package/dist/actions/login.js.map +1 -0
- package/dist/actions/logout-action.spec.d.ts +2 -0
- package/dist/actions/logout-action.spec.d.ts.map +1 -0
- package/dist/actions/logout-action.spec.js +23 -0
- package/dist/actions/logout-action.spec.js.map +1 -0
- package/dist/actions/logout.d.ts +14 -0
- package/dist/actions/logout.d.ts.map +1 -0
- package/dist/actions/logout.js +20 -0
- package/dist/actions/logout.js.map +1 -0
- package/dist/actions/not-found-action.d.ts +10 -0
- package/dist/actions/not-found-action.d.ts.map +1 -0
- package/dist/actions/not-found-action.js +14 -0
- package/dist/actions/not-found-action.js.map +1 -0
- package/dist/actions/not-found-action.spec.d.ts +2 -0
- package/dist/actions/not-found-action.spec.d.ts.map +1 -0
- package/dist/actions/not-found-action.spec.js +17 -0
- package/dist/actions/not-found-action.spec.js.map +1 -0
- package/dist/add-cors-header.spec.d.ts +2 -0
- package/dist/add-cors-header.spec.d.ts.map +1 -0
- package/dist/add-cors-header.spec.js +99 -0
- package/dist/add-cors-header.spec.js.map +1 -0
- package/dist/api-manager.d.ts +61 -0
- package/dist/api-manager.d.ts.map +1 -0
- package/dist/api-manager.js +144 -0
- package/dist/api-manager.js.map +1 -0
- package/dist/authenticate.d.ts +5 -0
- package/dist/authenticate.d.ts.map +1 -0
- package/dist/authenticate.js +20 -0
- package/dist/authenticate.js.map +1 -0
- package/dist/authenticate.spec.d.ts +2 -0
- package/dist/authenticate.spec.d.ts.map +1 -0
- package/dist/authenticate.spec.js +59 -0
- package/dist/authenticate.spec.js.map +1 -0
- package/dist/authorize.d.ts +5 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +22 -0
- package/dist/authorize.js.map +1 -0
- package/dist/authorize.spec.d.ts +2 -0
- package/dist/authorize.spec.d.ts.map +1 -0
- package/dist/authorize.spec.js +55 -0
- package/dist/authorize.spec.js.map +1 -0
- package/dist/endpoint-generators/create-delete-endpoint.d.ts +17 -0
- package/dist/endpoint-generators/create-delete-endpoint.d.ts.map +1 -0
- package/dist/endpoint-generators/create-delete-endpoint.js +24 -0
- package/dist/endpoint-generators/create-delete-endpoint.js.map +1 -0
- package/dist/endpoint-generators/create-delete-endpoint.spec.d.ts +2 -0
- package/dist/endpoint-generators/create-delete-endpoint.spec.d.ts.map +1 -0
- package/dist/endpoint-generators/create-delete-endpoint.spec.js +33 -0
- package/dist/endpoint-generators/create-delete-endpoint.spec.js.map +1 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.d.ts +17 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.js +26 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.js.map +1 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.spec.d.ts +2 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.spec.d.ts.map +1 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.spec.js +143 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.d.ts +17 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.js +29 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.js.map +1 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.spec.d.ts +2 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.spec.d.ts.map +1 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.spec.js +74 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -0
- package/dist/endpoint-generators/create-patch-endpoint.d.ts +18 -0
- package/dist/endpoint-generators/create-patch-endpoint.d.ts.map +1 -0
- package/dist/endpoint-generators/create-patch-endpoint.js +26 -0
- package/dist/endpoint-generators/create-patch-endpoint.js.map +1 -0
- package/dist/endpoint-generators/create-patch-endpoint.spec.d.ts +2 -0
- package/dist/endpoint-generators/create-patch-endpoint.spec.d.ts.map +1 -0
- package/dist/endpoint-generators/create-patch-endpoint.spec.js +36 -0
- package/dist/endpoint-generators/create-patch-endpoint.spec.js.map +1 -0
- package/dist/endpoint-generators/create-post-endpoint.d.ts +18 -0
- package/dist/endpoint-generators/create-post-endpoint.d.ts.map +1 -0
- package/dist/endpoint-generators/create-post-endpoint.js +29 -0
- package/dist/endpoint-generators/create-post-endpoint.js.map +1 -0
- package/dist/endpoint-generators/create-post-endpoint.spec.d.ts +2 -0
- package/dist/endpoint-generators/create-post-endpoint.spec.d.ts.map +1 -0
- package/dist/endpoint-generators/create-post-endpoint.spec.js +34 -0
- package/dist/endpoint-generators/create-post-endpoint.spec.js.map +1 -0
- package/dist/endpoint-generators/index.d.ts +6 -0
- package/dist/endpoint-generators/index.d.ts.map +1 -0
- package/dist/endpoint-generators/index.js +9 -0
- package/dist/endpoint-generators/index.js.map +1 -0
- package/dist/endpoint-generators/utils.d.ts +9 -0
- package/dist/endpoint-generators/utils.d.ts.map +1 -0
- package/dist/endpoint-generators/utils.js +27 -0
- package/dist/endpoint-generators/utils.js.map +1 -0
- package/dist/http-authentication-settings.d.ts +17 -0
- package/dist/http-authentication-settings.d.ts.map +1 -0
- package/dist/http-authentication-settings.js +26 -0
- package/dist/http-authentication-settings.js.map +1 -0
- package/dist/http-user-context.d.ts +54 -0
- package/dist/http-user-context.d.ts.map +1 -0
- package/dist/http-user-context.js +153 -0
- package/dist/http-user-context.js.map +1 -0
- package/dist/http-user-context.spec.d.ts +4 -0
- package/dist/http-user-context.spec.d.ts.map +1 -0
- package/dist/http-user-context.spec.js +267 -0
- package/dist/http-user-context.spec.js.map +1 -0
- package/dist/incoming-message-extensions.d.ts +8 -0
- package/dist/incoming-message-extensions.d.ts.map +1 -0
- package/dist/incoming-message-extensions.js +14 -0
- package/dist/incoming-message-extensions.js.map +1 -0
- package/dist/incoming-message-extensions.spec.d.ts +2 -0
- package/dist/incoming-message-extensions.spec.d.ts.map +1 -0
- package/dist/incoming-message-extensions.spec.js +39 -0
- package/dist/incoming-message-extensions.spec.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/injector-extensions.d.ts +21 -0
- package/dist/injector-extensions.d.ts.map +1 -0
- package/dist/injector-extensions.js +14 -0
- package/dist/injector-extensions.js.map +1 -0
- package/dist/injector-extensions.spec.d.ts +2 -0
- package/dist/injector-extensions.spec.d.ts.map +1 -0
- package/dist/injector-extensions.spec.js +19 -0
- package/dist/injector-extensions.spec.js.map +1 -0
- package/dist/models/cors-options.d.ts +22 -0
- package/dist/models/cors-options.d.ts.map +1 -0
- package/dist/models/cors-options.js +3 -0
- package/dist/models/cors-options.js.map +1 -0
- package/dist/models/default-session.d.ts +14 -0
- package/dist/models/default-session.d.ts.map +1 -0
- package/dist/models/default-session.js +10 -0
- package/dist/models/default-session.js.map +1 -0
- package/dist/request-action-implementation.d.ts +54 -0
- package/dist/request-action-implementation.d.ts.map +1 -0
- package/dist/request-action-implementation.js +42 -0
- package/dist/request-action-implementation.js.map +1 -0
- package/dist/rest-service.integration.spec.d.ts +2 -0
- package/dist/rest-service.integration.spec.d.ts.map +1 -0
- package/dist/rest-service.integration.spec.js +129 -0
- package/dist/rest-service.integration.spec.js.map +1 -0
- package/dist/rest.integration.test.d.ts +58 -0
- package/dist/rest.integration.test.d.ts.map +1 -0
- package/dist/rest.integration.test.js +94 -0
- package/dist/rest.integration.test.js.map +1 -0
- package/dist/schema-validator/index.d.ts +3 -0
- package/dist/schema-validator/index.d.ts.map +1 -0
- package/dist/schema-validator/index.js +6 -0
- package/dist/schema-validator/index.js.map +1 -0
- package/dist/schema-validator/schema-validation-error.d.ts +10 -0
- package/dist/schema-validator/schema-validation-error.d.ts.map +1 -0
- package/dist/schema-validator/schema-validation-error.js +15 -0
- package/dist/schema-validator/schema-validation-error.js.map +1 -0
- package/dist/schema-validator/schema-validator.d.ts +20 -0
- package/dist/schema-validator/schema-validator.d.ts.map +1 -0
- package/dist/schema-validator/schema-validator.js +36 -0
- package/dist/schema-validator/schema-validator.js.map +1 -0
- package/dist/schema-validator/schema-validator.test.d.ts +2 -0
- package/dist/schema-validator/schema-validator.test.d.ts.map +1 -0
- package/dist/schema-validator/schema-validator.test.js +62 -0
- package/dist/schema-validator/schema-validator.test.js.map +1 -0
- package/dist/schema-validator/validate-examples.d.ts +37 -0
- package/dist/schema-validator/validate-examples.d.ts.map +1 -0
- package/dist/schema-validator/validate-examples.js +29 -0
- package/dist/schema-validator/validate-examples.js.map +1 -0
- package/dist/server-manager.d.ts +30 -0
- package/dist/server-manager.d.ts.map +1 -0
- package/dist/server-manager.js +71 -0
- package/dist/server-manager.js.map +1 -0
- package/dist/server-response-extensions.d.ts +21 -0
- package/dist/server-response-extensions.d.ts.map +1 -0
- package/dist/server-response-extensions.js +15 -0
- package/dist/server-response-extensions.js.map +1 -0
- package/dist/server-response-extensions.spec.d.ts +2 -0
- package/dist/server-response-extensions.spec.d.ts.map +1 -0
- package/dist/server-response-extensions.spec.js +49 -0
- package/dist/server-response-extensions.spec.js.map +1 -0
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +66 -0
- package/dist/utils.js.map +1 -0
- package/dist/validate.d.ts +18 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.integration.schema.d.ts +69 -0
- package/dist/validate.integration.schema.d.ts.map +1 -0
- package/dist/validate.integration.schema.js +3 -0
- package/dist/validate.integration.schema.js.map +1 -0
- package/dist/validate.integration.spec.d.ts +13 -0
- package/dist/validate.integration.spec.d.ts.map +1 -0
- package/dist/validate.integration.spec.js +223 -0
- package/dist/validate.integration.spec.js.map +1 -0
- package/dist/validate.integration.spec.schema.json +749 -0
- package/dist/validate.js +49 -0
- package/dist/validate.js.map +1 -0
- package/package.json +56 -0
- package/src/actions/error-action.spec.ts +54 -0
- package/src/actions/error-action.ts +34 -0
- package/src/actions/get-current-user.spec.ts +23 -0
- package/src/actions/get-current-user.ts +15 -0
- package/src/actions/index.ts +6 -0
- package/src/actions/is-authenticated.spec.ts +18 -0
- package/src/actions/is-authenticated.ts +13 -0
- package/src/actions/login-action.spec.ts +41 -0
- package/src/actions/login.ts +26 -0
- package/src/actions/logout-action.spec.ts +27 -0
- package/src/actions/logout.ts +16 -0
- package/src/actions/not-found-action.spec.ts +17 -0
- package/src/actions/not-found-action.ts +13 -0
- package/src/add-cors-header.spec.ts +133 -0
- package/src/api-manager.ts +222 -0
- package/src/authenticate.spec.ts +78 -0
- package/src/authenticate.ts +22 -0
- package/src/authorize.spec.ts +69 -0
- package/src/authorize.ts +19 -0
- package/src/endpoint-generators/create-delete-endpoint.spec.ts +34 -0
- package/src/endpoint-generators/create-delete-endpoint.ts +25 -0
- package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +164 -0
- package/src/endpoint-generators/create-get-collection-endpoint.ts +28 -0
- package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +75 -0
- package/src/endpoint-generators/create-get-entity-endpoint.ts +29 -0
- package/src/endpoint-generators/create-patch-endpoint.spec.ts +36 -0
- package/src/endpoint-generators/create-patch-endpoint.ts +27 -0
- package/src/endpoint-generators/create-post-endpoint.spec.ts +32 -0
- package/src/endpoint-generators/create-post-endpoint.ts +30 -0
- package/src/endpoint-generators/index.ts +5 -0
- package/src/endpoint-generators/utils.ts +34 -0
- package/src/http-authentication-settings.ts +23 -0
- package/src/http-user-context.spec.ts +299 -0
- package/src/http-user-context.ts +160 -0
- package/src/incoming-message-extensions.spec.ts +41 -0
- package/src/incoming-message-extensions.ts +19 -0
- package/src/index.ts +16 -0
- package/src/injector-extensions.spec.ts +19 -0
- package/src/injector-extensions.ts +35 -0
- package/src/models/cors-options.ts +21 -0
- package/src/models/default-session.ts +14 -0
- package/src/request-action-implementation.ts +70 -0
- package/src/rest-service.integration.spec.ts +166 -0
- package/src/rest.integration.test.ts +112 -0
- package/src/schema-validator/index.ts +2 -0
- package/src/schema-validator/schema-validation-error.ts +11 -0
- package/src/schema-validator/schema-validator.test.ts +72 -0
- package/src/schema-validator/schema-validator.ts +31 -0
- package/src/schema-validator/validate-examples.ts +38 -0
- package/src/server-manager.ts +88 -0
- package/src/server-response-extensions.spec.ts +53 -0
- package/src/server-response-extensions.ts +30 -0
- package/src/utils.ts +65 -0
- package/src/validate.integration.schema.ts +50 -0
- package/src/validate.integration.spec.schema.json +779 -0
- package/src/validate.integration.spec.ts +218 -0
- package/src/validate.ts +60 -0
|
@@ -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
|
+
}
|