@furystack/rest-service 12.0.0 โ 12.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/esm/actions/index.d.ts +1 -0
- package/esm/actions/index.d.ts.map +1 -1
- package/esm/actions/index.js +1 -0
- package/esm/actions/index.js.map +1 -1
- package/esm/actions/login.d.ts +7 -3
- package/esm/actions/login.d.ts.map +1 -1
- package/esm/actions/login.js +11 -5
- package/esm/actions/login.js.map +1 -1
- package/esm/actions/password-login-action.d.ts +24 -0
- package/esm/actions/password-login-action.d.ts.map +1 -0
- package/esm/actions/password-login-action.js +31 -0
- package/esm/actions/password-login-action.js.map +1 -0
- package/esm/actions/password-login-action.spec.d.ts +2 -0
- package/esm/actions/password-login-action.spec.d.ts.map +1 -0
- package/esm/actions/password-login-action.spec.js +105 -0
- package/esm/actions/password-login-action.spec.js.map +1 -0
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- package/esm/login-response-strategy.d.ts +28 -0
- package/esm/login-response-strategy.d.ts.map +1 -0
- package/esm/login-response-strategy.js +28 -0
- package/esm/login-response-strategy.js.map +1 -0
- package/esm/login-response-strategy.spec.d.ts +2 -0
- package/esm/login-response-strategy.spec.d.ts.map +1 -0
- package/esm/login-response-strategy.spec.js +78 -0
- package/esm/login-response-strategy.spec.js.map +1 -0
- package/package.json +7 -7
- package/src/actions/index.ts +1 -0
- package/src/actions/login.ts +12 -6
- package/src/actions/password-login-action.spec.ts +122 -0
- package/src/actions/password-login-action.ts +35 -0
- package/src/http-authentication-settings.ts +30 -30
- package/src/http-user-context.spec.ts +462 -462
- package/src/http-user-context.ts +164 -164
- package/src/index.ts +1 -0
- package/src/login-response-strategy.spec.ts +90 -0
- package/src/login-response-strategy.ts +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [12.1.0] - 2026-02-26
|
|
4
|
+
|
|
5
|
+
### ๐ง Chores
|
|
6
|
+
|
|
7
|
+
- Normalized line endings in `http-user-context.ts`, `http-authentication-settings.ts`, and related spec files
|
|
8
|
+
|
|
9
|
+
### โจ Features
|
|
10
|
+
|
|
11
|
+
### `LoginResponseStrategy<TResult>` type
|
|
12
|
+
|
|
13
|
+
New pluggable type that decouples login actions from session/token creation. A strategy turns an authenticated `User` into an `ActionResult<TResult>` โ the generic parameter flows through to the action's return type for full type inference.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import type { LoginResponseStrategy } from '@furystack/rest-service'
|
|
17
|
+
|
|
18
|
+
type LoginResponseStrategy<TResult> = {
|
|
19
|
+
createLoginResponse: (user: User, injector: Injector) => Promise<ActionResult<TResult>>
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### `createCookieLoginStrategy(injector)`
|
|
24
|
+
|
|
25
|
+
Factory that creates a cookie-based `LoginResponseStrategy<User>`. On login it generates a random session ID, persists it in the session DataSet, and returns the user with a `Set-Cookie` header.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createCookieLoginStrategy } from '@furystack/rest-service'
|
|
29
|
+
|
|
30
|
+
const cookieStrategy = createCookieLoginStrategy(injector)
|
|
31
|
+
// cookieStrategy.createLoginResponse(user, injector) โ ActionResult<User> with Set-Cookie header
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### `createPasswordLoginAction(strategy)`
|
|
35
|
+
|
|
36
|
+
Factory that creates a password-based login `RequestAction`. Authenticates via `HttpUserContext.authenticateUser()` then delegates session/token creation to the provided strategy. Includes timing-attack mitigation on failure.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { createPasswordLoginAction, createCookieLoginStrategy } from '@furystack/rest-service'
|
|
40
|
+
|
|
41
|
+
const cookieStrategy = createCookieLoginStrategy(injector)
|
|
42
|
+
const loginAction = createPasswordLoginAction(cookieStrategy)
|
|
43
|
+
// loginAction: RequestAction<{ result: User; body: { username: string; password: string } }>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### ๐งช Tests
|
|
47
|
+
|
|
48
|
+
- Added `login-response-strategy.spec.ts` โ tests cookie strategy session creation, Set-Cookie headers, session persistence, and session ID uniqueness
|
|
49
|
+
- Added `password-login-action.spec.ts` โ tests strategy delegation, user forwarding, auth failure handling, and custom strategy result types
|
|
50
|
+
|
|
51
|
+
### โฌ๏ธ Dependencies
|
|
52
|
+
|
|
53
|
+
- Bumped `@types/node` from ^25.3.0 to ^25.3.1
|
|
54
|
+
|
|
3
55
|
## [12.0.0] - 2026-02-26
|
|
4
56
|
|
|
5
57
|
### โจ Features
|
package/esm/actions/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,4BAA4B,CAAA"}
|
package/esm/actions/index.js
CHANGED
package/esm/actions/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,4BAA4B,CAAA"}
|
package/esm/actions/login.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { User } from '@furystack/core';
|
|
2
2
|
import type { RequestAction } from '../request-action-implementation.js';
|
|
3
3
|
/**
|
|
4
|
-
* Action that logs in the current user
|
|
5
|
-
* Should be called with a JSON
|
|
6
|
-
* Returns the current user instance
|
|
4
|
+
* Action that logs in the current user.
|
|
5
|
+
* Should be called with a JSON POST body with `username` and `password` fields.
|
|
6
|
+
* Returns the current user instance.
|
|
7
|
+
*
|
|
8
|
+
* @deprecated Use `createPasswordLoginAction(createCookieLoginStrategy(injector))` instead.
|
|
9
|
+
* This static action resolves services from the request-scoped injector on
|
|
10
|
+
* every call; the factory approach captures them once at setup time.
|
|
7
11
|
*/
|
|
8
12
|
export declare const LoginAction: RequestAction<{
|
|
9
13
|
result: User;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/actions/login.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/actions/login.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAK3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAA;AAGxE;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC;IACtC,MAAM,EAAE,IAAI,CAAA;IACZ,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAC7C,CAWA,CAAA"}
|
package/esm/actions/login.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { HttpUserContext } from '../http-user-context.js';
|
|
2
1
|
import { RequestError } from '@furystack/rest';
|
|
2
|
+
import { sleepAsync } from '@furystack/utils';
|
|
3
|
+
import { HttpUserContext } from '../http-user-context.js';
|
|
3
4
|
import { JsonResult } from '../request-action-implementation.js';
|
|
4
5
|
/**
|
|
5
|
-
* Action that logs in the current user
|
|
6
|
-
* Should be called with a JSON
|
|
7
|
-
* Returns the current user instance
|
|
6
|
+
* Action that logs in the current user.
|
|
7
|
+
* Should be called with a JSON POST body with `username` and `password` fields.
|
|
8
|
+
* Returns the current user instance.
|
|
9
|
+
*
|
|
10
|
+
* @deprecated Use `createPasswordLoginAction(createCookieLoginStrategy(injector))` instead.
|
|
11
|
+
* This static action resolves services from the request-scoped injector on
|
|
12
|
+
* every call; the factory approach captures them once at setup time.
|
|
8
13
|
*/
|
|
9
14
|
export const LoginAction = async ({ injector, getBody, response }) => {
|
|
10
15
|
const userContext = injector.getInstance(HttpUserContext);
|
|
@@ -14,7 +19,8 @@ export const LoginAction = async ({ injector, getBody, response }) => {
|
|
|
14
19
|
await userContext.cookieLogin(user, response);
|
|
15
20
|
return JsonResult(user, 200);
|
|
16
21
|
}
|
|
17
|
-
catch
|
|
22
|
+
catch {
|
|
23
|
+
await sleepAsync(Math.random() * 1000);
|
|
18
24
|
throw new RequestError('Login Failed', 400);
|
|
19
25
|
}
|
|
20
26
|
};
|
package/esm/actions/login.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/actions/login.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/actions/login.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAEzD,OAAO,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAA;AAEhE;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,WAAW,GAGnB,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE;IAC7C,MAAM,WAAW,GAAG,QAAQ,CAAC,WAAW,CAAC,eAAe,CAAC,CAAA;IACzD,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAA;IAC5B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;QAC7E,MAAM,WAAW,CAAC,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QAC7C,OAAO,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAA;QACtC,MAAM,IAAI,YAAY,CAAC,cAAc,EAAE,GAAG,CAAC,CAAA;IAC7C,CAAC;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LoginResponseStrategy } from '../login-response-strategy.js';
|
|
2
|
+
import type { RequestAction } from '../request-action-implementation.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a login {@link RequestAction} that authenticates a user by
|
|
5
|
+
* username + password, then delegates session/token creation to the
|
|
6
|
+
* provided {@link LoginResponseStrategy}.
|
|
7
|
+
*
|
|
8
|
+
* The return type is inferred from the strategy:
|
|
9
|
+
* - Cookie strategy -> `ActionResult<User>`
|
|
10
|
+
* - JWT strategy -> `ActionResult<{ accessToken: string; refreshToken: string }>`
|
|
11
|
+
*
|
|
12
|
+
* A random delay (0-1 s) is added on failure to mitigate timing attacks.
|
|
13
|
+
*
|
|
14
|
+
* @param strategy The login response strategy that produces the session/token result
|
|
15
|
+
* @returns A `RequestAction` that can be wired into a REST API route
|
|
16
|
+
*/
|
|
17
|
+
export declare const createPasswordLoginAction: <TResult>(strategy: LoginResponseStrategy<TResult>) => RequestAction<{
|
|
18
|
+
result: TResult;
|
|
19
|
+
body: {
|
|
20
|
+
username: string;
|
|
21
|
+
password: string;
|
|
22
|
+
};
|
|
23
|
+
}>;
|
|
24
|
+
//# sourceMappingURL=password-login-action.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password-login-action.d.ts","sourceRoot":"","sources":["../../src/actions/password-login-action.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAA;AAExE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,EAC/C,UAAU,qBAAqB,CAAC,OAAO,CAAC,KACvC,aAAa,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAWjF,CAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { RequestError } from '@furystack/rest';
|
|
2
|
+
import { sleepAsync } from '@furystack/utils';
|
|
3
|
+
import { HttpUserContext } from '../http-user-context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a login {@link RequestAction} that authenticates a user by
|
|
6
|
+
* username + password, then delegates session/token creation to the
|
|
7
|
+
* provided {@link LoginResponseStrategy}.
|
|
8
|
+
*
|
|
9
|
+
* The return type is inferred from the strategy:
|
|
10
|
+
* - Cookie strategy -> `ActionResult<User>`
|
|
11
|
+
* - JWT strategy -> `ActionResult<{ accessToken: string; refreshToken: string }>`
|
|
12
|
+
*
|
|
13
|
+
* A random delay (0-1 s) is added on failure to mitigate timing attacks.
|
|
14
|
+
*
|
|
15
|
+
* @param strategy The login response strategy that produces the session/token result
|
|
16
|
+
* @returns A `RequestAction` that can be wired into a REST API route
|
|
17
|
+
*/
|
|
18
|
+
export const createPasswordLoginAction = (strategy) => {
|
|
19
|
+
return async ({ injector, getBody }) => {
|
|
20
|
+
const body = await getBody();
|
|
21
|
+
try {
|
|
22
|
+
const user = await injector.getInstance(HttpUserContext).authenticateUser(body.username, body.password);
|
|
23
|
+
return strategy.createLoginResponse(user, injector);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
await sleepAsync(Math.random() * 1000);
|
|
27
|
+
throw new RequestError('Login Failed', 400);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=password-login-action.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password-login-action.js","sourceRoot":"","sources":["../../src/actions/password-login-action.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAIzD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,QAAwC,EAC0C,EAAE;IACpF,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE;QACrC,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAA;QAC5B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;YACvG,OAAO,QAAQ,CAAC,mBAAmB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QACrD,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAA;YACtC,MAAM,IAAI,YAAY,CAAC,cAAc,EAAE,GAAG,CAAC,CAAA;QAC7C,CAAC;IACH,CAAC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password-login-action.spec.d.ts","sourceRoot":"","sources":["../../src/actions/password-login-action.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { InMemoryStore, StoreManager, User, addStore } from '@furystack/core';
|
|
2
|
+
import { Injector } from '@furystack/inject';
|
|
3
|
+
import { getRepository } from '@furystack/repository';
|
|
4
|
+
import { PasswordAuthenticator, PasswordCredential, PasswordResetToken, usePasswordPolicy } from '@furystack/security';
|
|
5
|
+
import { usingAsync } from '@furystack/utils';
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { useHttpAuthentication } from '../helpers.js';
|
|
8
|
+
import { DefaultSession } from '../models/default-session.js';
|
|
9
|
+
import { JsonResult } from '../request-action-implementation.js';
|
|
10
|
+
import { createPasswordLoginAction } from './password-login-action.js';
|
|
11
|
+
const setupInjector = async (i, username, password) => {
|
|
12
|
+
addStore(i, new InMemoryStore({ model: User, primaryKey: 'username' }))
|
|
13
|
+
.addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
|
|
14
|
+
.addStore(new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
|
|
15
|
+
.addStore(new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }));
|
|
16
|
+
const repo = getRepository(i);
|
|
17
|
+
repo.createDataSet(User, 'username');
|
|
18
|
+
repo.createDataSet(DefaultSession, 'sessionId');
|
|
19
|
+
repo.createDataSet(PasswordCredential, 'userName');
|
|
20
|
+
repo.createDataSet(PasswordResetToken, 'token');
|
|
21
|
+
usePasswordPolicy(i);
|
|
22
|
+
useHttpAuthentication(i);
|
|
23
|
+
const sm = i.getInstance(StoreManager);
|
|
24
|
+
const pw = i.getInstance(PasswordAuthenticator);
|
|
25
|
+
const cred = await pw.hasher.createCredential(username, password);
|
|
26
|
+
await sm.getStoreFor(PasswordCredential, 'userName').add(cred);
|
|
27
|
+
await sm.getStoreFor(User, 'username').add({ username, roles: ['admin'] });
|
|
28
|
+
};
|
|
29
|
+
const mockStrategy = {
|
|
30
|
+
createLoginResponse: vi.fn(async (user) => JsonResult(user, 200)),
|
|
31
|
+
};
|
|
32
|
+
describe('createPasswordLoginAction', () => {
|
|
33
|
+
const request = {};
|
|
34
|
+
const response = {};
|
|
35
|
+
it('Should delegate to the strategy on successful authentication', async () => {
|
|
36
|
+
await usingAsync(new Injector(), async (i) => {
|
|
37
|
+
await setupInjector(i, 'testuser', 'testpass');
|
|
38
|
+
const action = createPasswordLoginAction(mockStrategy);
|
|
39
|
+
const result = await action({
|
|
40
|
+
injector: i,
|
|
41
|
+
request,
|
|
42
|
+
response,
|
|
43
|
+
getBody: () => Promise.resolve({ username: 'testuser', password: 'testpass' }),
|
|
44
|
+
});
|
|
45
|
+
expect(mockStrategy.createLoginResponse).toHaveBeenCalled();
|
|
46
|
+
expect(result.chunk.username).toBe('testuser');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it('Should pass the correct user to the strategy', async () => {
|
|
50
|
+
await usingAsync(new Injector(), async (i) => {
|
|
51
|
+
await setupInjector(i, 'testuser', 'testpass');
|
|
52
|
+
const strategyFn = vi.fn(async (user) => JsonResult(user, 200));
|
|
53
|
+
const strategy = { createLoginResponse: strategyFn };
|
|
54
|
+
const action = createPasswordLoginAction(strategy);
|
|
55
|
+
await action({
|
|
56
|
+
injector: i,
|
|
57
|
+
request,
|
|
58
|
+
response,
|
|
59
|
+
getBody: () => Promise.resolve({ username: 'testuser', password: 'testpass' }),
|
|
60
|
+
});
|
|
61
|
+
expect(strategyFn).toHaveBeenCalledWith(expect.objectContaining({ username: 'testuser', roles: ['admin'] }), i);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
it('Should throw RequestError on invalid credentials', async () => {
|
|
65
|
+
await usingAsync(new Injector(), async (i) => {
|
|
66
|
+
await setupInjector(i, 'testuser', 'testpass');
|
|
67
|
+
const action = createPasswordLoginAction(mockStrategy);
|
|
68
|
+
await expect(action({
|
|
69
|
+
injector: i,
|
|
70
|
+
request,
|
|
71
|
+
response,
|
|
72
|
+
getBody: () => Promise.resolve({ username: 'testuser', password: 'wrongpass' }),
|
|
73
|
+
})).rejects.toThrow('Login Failed');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it('Should throw RequestError for nonexistent user', async () => {
|
|
77
|
+
await usingAsync(new Injector(), async (i) => {
|
|
78
|
+
await setupInjector(i, 'testuser', 'testpass');
|
|
79
|
+
const action = createPasswordLoginAction(mockStrategy);
|
|
80
|
+
await expect(action({
|
|
81
|
+
injector: i,
|
|
82
|
+
request,
|
|
83
|
+
response,
|
|
84
|
+
getBody: () => Promise.resolve({ username: 'nobody', password: 'nopass' }),
|
|
85
|
+
})).rejects.toThrow('Login Failed');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
it('Should work with a custom result type from strategy', async () => {
|
|
89
|
+
await usingAsync(new Injector(), async (i) => {
|
|
90
|
+
await setupInjector(i, 'testuser', 'testpass');
|
|
91
|
+
const tokenStrategy = {
|
|
92
|
+
createLoginResponse: async () => JsonResult({ accessToken: 'tok123' }, 200),
|
|
93
|
+
};
|
|
94
|
+
const action = createPasswordLoginAction(tokenStrategy);
|
|
95
|
+
const result = await action({
|
|
96
|
+
injector: i,
|
|
97
|
+
request,
|
|
98
|
+
response,
|
|
99
|
+
getBody: () => Promise.resolve({ username: 'testuser', password: 'testpass' }),
|
|
100
|
+
});
|
|
101
|
+
expect(result.chunk.accessToken).toBe('tok123');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
//# sourceMappingURL=password-login-action.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password-login-action.spec.js","sourceRoot":"","sources":["../../src/actions/password-login-action.spec.ts"],"names":[],"mappings":"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"}
|
package/esm/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './get-schema-from-api.js';
|
|
|
9
9
|
export * from './helpers.js';
|
|
10
10
|
export * from './http-authentication-settings.js';
|
|
11
11
|
export * from './http-user-context.js';
|
|
12
|
+
export * from './login-response-strategy.js';
|
|
12
13
|
export * from './mime-types.js';
|
|
13
14
|
export * from './models/index.js';
|
|
14
15
|
export * from './proxy-manager.js';
|
package/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,qCAAqC,CAAA;AACnD,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,cAAc,CAAA;AAC5B,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,oCAAoC,CAAA;AAClD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,qBAAqB,CAAA;AACnC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,qCAAqC,CAAA;AACnD,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,cAAc,CAAA;AAC5B,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,oCAAoC,CAAA;AAClD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,qBAAqB,CAAA;AACnC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,eAAe,CAAA"}
|
package/esm/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from './get-schema-from-api.js';
|
|
|
9
9
|
export * from './helpers.js';
|
|
10
10
|
export * from './http-authentication-settings.js';
|
|
11
11
|
export * from './http-user-context.js';
|
|
12
|
+
export * from './login-response-strategy.js';
|
|
12
13
|
export * from './mime-types.js';
|
|
13
14
|
export * from './models/index.js';
|
|
14
15
|
export * from './proxy-manager.js';
|
package/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,qCAAqC,CAAA;AACnD,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,cAAc,CAAA;AAC5B,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,oCAAoC,CAAA;AAClD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,qBAAqB,CAAA;AACnC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,qCAAqC,CAAA;AACnD,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,cAAc,CAAA;AAC5B,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,oCAAoC,CAAA;AAClD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,qBAAqB,CAAA;AACnC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,eAAe,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { User } from '@furystack/core';
|
|
2
|
+
import type { Injector } from '@furystack/inject';
|
|
3
|
+
import type { ActionResult } from './request-action-implementation.js';
|
|
4
|
+
/**
|
|
5
|
+
* A pluggable strategy that turns an authenticated {@link User} into an
|
|
6
|
+
* {@link ActionResult} containing the session/token data for the client.
|
|
7
|
+
*
|
|
8
|
+
* Pass a concrete strategy to action factories like
|
|
9
|
+
* {@link createPasswordLoginAction} or `createGoogleLoginAction` to
|
|
10
|
+
* decouple the authentication mechanism from the session/token mechanism.
|
|
11
|
+
*
|
|
12
|
+
* @typeParam TResult The shape of the response body (e.g. `User` for cookies,
|
|
13
|
+
* `{ accessToken: string; refreshToken: string }` for JWT)
|
|
14
|
+
*/
|
|
15
|
+
export type LoginResponseStrategy<TResult> = {
|
|
16
|
+
createLoginResponse: (user: User, injector: Injector) => Promise<ActionResult<TResult>>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Creates a cookie-based {@link LoginResponseStrategy}.
|
|
20
|
+
*
|
|
21
|
+
* On each login it generates a random session ID, persists it in the session
|
|
22
|
+
* DataSet, and returns the user with a `Set-Cookie` header.
|
|
23
|
+
*
|
|
24
|
+
* @param injector The root injector (must have {@link HttpAuthenticationSettings} configured)
|
|
25
|
+
* @returns A strategy that returns `ActionResult<User>`
|
|
26
|
+
*/
|
|
27
|
+
export declare const createCookieLoginStrategy: (injector: Injector) => LoginResponseStrategy<User>;
|
|
28
|
+
//# sourceMappingURL=login-response-strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login-response-strategy.d.ts","sourceRoot":"","sources":["../src/login-response-strategy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAE3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAIjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AAGtE;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,CAAC,OAAO,IAAI;IAC3C,mBAAmB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAA;CACxF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAI,UAAU,QAAQ,KAAG,qBAAqB,CAAC,IAAI,CAcxF,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useSystemIdentityContext } from '@furystack/core';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
import { HttpAuthenticationSettings } from './http-authentication-settings.js';
|
|
4
|
+
import { JsonResult } from './request-action-implementation.js';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a cookie-based {@link LoginResponseStrategy}.
|
|
7
|
+
*
|
|
8
|
+
* On each login it generates a random session ID, persists it in the session
|
|
9
|
+
* DataSet, and returns the user with a `Set-Cookie` header.
|
|
10
|
+
*
|
|
11
|
+
* @param injector The root injector (must have {@link HttpAuthenticationSettings} configured)
|
|
12
|
+
* @returns A strategy that returns `ActionResult<User>`
|
|
13
|
+
*/
|
|
14
|
+
export const createCookieLoginStrategy = (injector) => {
|
|
15
|
+
const settings = injector.getInstance(HttpAuthenticationSettings);
|
|
16
|
+
const systemInjector = useSystemIdentityContext({ injector, username: 'CookieLoginStrategy' });
|
|
17
|
+
return {
|
|
18
|
+
createLoginResponse: async (user) => {
|
|
19
|
+
const sessionDataSet = settings.getSessionDataSet(systemInjector);
|
|
20
|
+
const sessionId = randomBytes(32).toString('hex');
|
|
21
|
+
await sessionDataSet.add(systemInjector, { sessionId, username: user.username });
|
|
22
|
+
return JsonResult(user, 200, {
|
|
23
|
+
'Set-Cookie': `${settings.cookieName}=${sessionId}; Path=/; HttpOnly`,
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=login-response-strategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login-response-strategy.js","sourceRoot":"","sources":["../src/login-response-strategy.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAA;AAE1D,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AAEpC,OAAO,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAA;AAE9E,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAiB/D;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,QAAkB,EAA+B,EAAE;IAC3F,MAAM,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC,0BAA0B,CAAC,CAAA;IACjE,MAAM,cAAc,GAAG,wBAAwB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,qBAAqB,EAAE,CAAC,CAAA;IAE9F,OAAO;QACL,mBAAmB,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YAClC,MAAM,cAAc,GAAG,QAAQ,CAAC,iBAAiB,CAAC,cAAc,CAAC,CAAA;YACjE,MAAM,SAAS,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;YACjD,MAAM,cAAc,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAA;YAChF,OAAO,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE;gBAC3B,YAAY,EAAE,GAAG,QAAQ,CAAC,UAAU,IAAI,SAAS,oBAAoB;aACtE,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login-response-strategy.spec.d.ts","sourceRoot":"","sources":["../src/login-response-strategy.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { InMemoryStore, User, addStore } from '@furystack/core';
|
|
2
|
+
import { Injector } from '@furystack/inject';
|
|
3
|
+
import { getRepository } from '@furystack/repository';
|
|
4
|
+
import { PasswordCredential, PasswordResetToken, usePasswordPolicy } from '@furystack/security';
|
|
5
|
+
import { usingAsync } from '@furystack/utils';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { useHttpAuthentication } from './helpers.js';
|
|
8
|
+
import { createCookieLoginStrategy } from './login-response-strategy.js';
|
|
9
|
+
import { DefaultSession } from './models/default-session.js';
|
|
10
|
+
const setupInjector = (i) => {
|
|
11
|
+
addStore(i, new InMemoryStore({ model: User, primaryKey: 'username' }))
|
|
12
|
+
.addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
|
|
13
|
+
.addStore(new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
|
|
14
|
+
.addStore(new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }));
|
|
15
|
+
const repo = getRepository(i);
|
|
16
|
+
repo.createDataSet(User, 'username');
|
|
17
|
+
repo.createDataSet(DefaultSession, 'sessionId');
|
|
18
|
+
repo.createDataSet(PasswordCredential, 'userName');
|
|
19
|
+
repo.createDataSet(PasswordResetToken, 'token');
|
|
20
|
+
usePasswordPolicy(i);
|
|
21
|
+
useHttpAuthentication(i);
|
|
22
|
+
};
|
|
23
|
+
describe('createCookieLoginStrategy', () => {
|
|
24
|
+
const testUser = { username: 'testuser', roles: ['admin'] };
|
|
25
|
+
it('Should return the user as the response body', async () => {
|
|
26
|
+
await usingAsync(new Injector(), async (i) => {
|
|
27
|
+
setupInjector(i);
|
|
28
|
+
const strategy = createCookieLoginStrategy(i);
|
|
29
|
+
const result = await strategy.createLoginResponse(testUser, i);
|
|
30
|
+
expect(result.chunk).toEqual(testUser);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it('Should return status code 200', async () => {
|
|
34
|
+
await usingAsync(new Injector(), async (i) => {
|
|
35
|
+
setupInjector(i);
|
|
36
|
+
const strategy = createCookieLoginStrategy(i);
|
|
37
|
+
const result = await strategy.createLoginResponse(testUser, i);
|
|
38
|
+
expect(result.statusCode).toBe(200);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it('Should include a Set-Cookie header with the session ID', async () => {
|
|
42
|
+
await usingAsync(new Injector(), async (i) => {
|
|
43
|
+
setupInjector(i);
|
|
44
|
+
const strategy = createCookieLoginStrategy(i);
|
|
45
|
+
const result = await strategy.createLoginResponse(testUser, i);
|
|
46
|
+
const setCookie = result.headers['Set-Cookie'];
|
|
47
|
+
expect(setCookie).toBeDefined();
|
|
48
|
+
expect(setCookie).toContain('fss=');
|
|
49
|
+
expect(setCookie).toContain('Path=/');
|
|
50
|
+
expect(setCookie).toContain('HttpOnly');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it('Should persist the session in the DataSet', async () => {
|
|
54
|
+
await usingAsync(new Injector(), async (i) => {
|
|
55
|
+
setupInjector(i);
|
|
56
|
+
const strategy = createCookieLoginStrategy(i);
|
|
57
|
+
await strategy.createLoginResponse(testUser, i);
|
|
58
|
+
const repo = getRepository(i);
|
|
59
|
+
const sessionDataSet = repo.getDataSetFor(DefaultSession, 'sessionId');
|
|
60
|
+
const sessions = await sessionDataSet.find(i, { filter: { username: { $eq: 'testuser' } } });
|
|
61
|
+
expect(sessions).toHaveLength(1);
|
|
62
|
+
expect(sessions[0].username).toBe('testuser');
|
|
63
|
+
expect(sessions[0].sessionId).toBeTruthy();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it('Should create unique session IDs for each call', async () => {
|
|
67
|
+
await usingAsync(new Injector(), async (i) => {
|
|
68
|
+
setupInjector(i);
|
|
69
|
+
const strategy = createCookieLoginStrategy(i);
|
|
70
|
+
const result1 = await strategy.createLoginResponse(testUser, i);
|
|
71
|
+
const result2 = await strategy.createLoginResponse(testUser, i);
|
|
72
|
+
const sessionId1 = result1.headers['Set-Cookie']?.split('=')[1]?.split(';')[0];
|
|
73
|
+
const sessionId2 = result2.headers['Set-Cookie']?.split('=')[1]?.split(';')[0];
|
|
74
|
+
expect(sessionId1).not.toBe(sessionId2);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
//# sourceMappingURL=login-response-strategy.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login-response-strategy.spec.js","sourceRoot":"","sources":["../src/login-response-strategy.spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/rest-service",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.1.0",
|
|
4
4
|
"description": "REST API service implementation for FuryStack",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -39,19 +39,19 @@
|
|
|
39
39
|
},
|
|
40
40
|
"homepage": "https://github.com/furystack/furystack",
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@furystack/core": "^15.2.
|
|
42
|
+
"@furystack/core": "^15.2.2",
|
|
43
43
|
"@furystack/inject": "^12.0.30",
|
|
44
|
-
"@furystack/repository": "^10.1.
|
|
45
|
-
"@furystack/rest": "^8.0.
|
|
46
|
-
"@furystack/security": "^7.0.
|
|
44
|
+
"@furystack/repository": "^10.1.3",
|
|
45
|
+
"@furystack/rest": "^8.0.40",
|
|
46
|
+
"@furystack/security": "^7.0.1",
|
|
47
47
|
"@furystack/utils": "^8.1.10",
|
|
48
48
|
"ajv": "^8.18.0",
|
|
49
49
|
"ajv-formats": "^3.0.1",
|
|
50
50
|
"path-to-regexp": "^8.3.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@furystack/rest-client-fetch": "^8.0.
|
|
54
|
-
"@types/node": "^25.3.
|
|
53
|
+
"@furystack/rest-client-fetch": "^8.0.40",
|
|
54
|
+
"@types/node": "^25.3.1",
|
|
55
55
|
"@types/ws": "^8.18.1",
|
|
56
56
|
"typescript": "^5.9.3",
|
|
57
57
|
"vitest": "^4.0.18",
|
package/src/actions/index.ts
CHANGED
package/src/actions/login.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { HttpUserContext } from '../http-user-context.js'
|
|
2
1
|
import type { User } from '@furystack/core'
|
|
3
2
|
import { RequestError } from '@furystack/rest'
|
|
3
|
+
import { sleepAsync } from '@furystack/utils'
|
|
4
|
+
|
|
5
|
+
import { HttpUserContext } from '../http-user-context.js'
|
|
4
6
|
import type { RequestAction } from '../request-action-implementation.js'
|
|
5
7
|
import { JsonResult } from '../request-action-implementation.js'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
|
-
* Action that logs in the current user
|
|
9
|
-
* Should be called with a JSON
|
|
10
|
-
* Returns the current user instance
|
|
10
|
+
* Action that logs in the current user.
|
|
11
|
+
* Should be called with a JSON POST body with `username` and `password` fields.
|
|
12
|
+
* Returns the current user instance.
|
|
13
|
+
*
|
|
14
|
+
* @deprecated Use `createPasswordLoginAction(createCookieLoginStrategy(injector))` instead.
|
|
15
|
+
* This static action resolves services from the request-scoped injector on
|
|
16
|
+
* every call; the factory approach captures them once at setup time.
|
|
11
17
|
*/
|
|
12
|
-
|
|
13
18
|
export const LoginAction: RequestAction<{
|
|
14
19
|
result: User
|
|
15
20
|
body: { username: string; password: string }
|
|
@@ -20,7 +25,8 @@ export const LoginAction: RequestAction<{
|
|
|
20
25
|
const user = await userContext.authenticateUser(body.username, body.password)
|
|
21
26
|
await userContext.cookieLogin(user, response)
|
|
22
27
|
return JsonResult(user, 200)
|
|
23
|
-
} catch
|
|
28
|
+
} catch {
|
|
29
|
+
await sleepAsync(Math.random() * 1000)
|
|
24
30
|
throw new RequestError('Login Failed', 400)
|
|
25
31
|
}
|
|
26
32
|
}
|