From d68f8dc4ff3deb374e3c3bed809727198c8f3da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Poullain?= Date: Thu, 26 May 2022 11:35:24 +0200 Subject: [PATCH 1/9] Ctx.session: undefined -> null --- .../specifying-the-store-locally.feature.ts | 10 +++++----- .../session-tokens/using-cookies.feature.ts | 6 +++--- .../using-the-authorization-header.feature.ts | 20 +++++++++---------- packages/core/src/core/http/context.spec.ts | 2 +- packages/core/src/core/http/context.ts | 6 ++++-- .../src/sessions/use-sessions.hook.spec.ts | 8 ++++---- .../architecture/websocket-context.spec.ts | 2 +- .../src/architecture/websocket-context.ts | 6 ++++-- .../src/socketio-controller.service.spec.ts | 4 ++-- packages/typeorm/package-lock.json | 2 +- 10 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/specifying-the-store-locally.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/specifying-the-store-locally.feature.ts index 7acc36b26d..51b0fd6d89 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/specifying-the-store-locally.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/specifying-the-store-locally.feature.ts @@ -39,7 +39,7 @@ describe('Feature: Specifying the store locally', () => { it('Example: Usage with Redis store', async () => { - let session: Session|undefined; + let session: Session|null = null; /* ======================= DOCUMENTATION BEGIN ======================= */ @@ -81,21 +81,21 @@ describe('Feature: Specifying the store locally', () => { const app = await createApp(AppController, { serviceManager: services }); store = services.get(RedisStore); - strictEqual(session, undefined); + strictEqual(session, null); await request(app) .get('/api/products') .expect(200) .expect([]); - strictEqual(session, undefined); + strictEqual(session, null); const response = await request(app) .post('/api/login') .send({}) .expect(200); - strictEqual(session, undefined); + strictEqual(session, null); const token: undefined|string = response.body.token; if (token === undefined) { @@ -108,7 +108,7 @@ describe('Feature: Specifying the store locally', () => { .expect(200) .expect([]); - notStrictEqual(session, undefined); + notStrictEqual(session, null); strictEqual((session as unknown as Session).getToken(), token); }); diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/using-cookies.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/using-cookies.feature.ts index ef37743c2f..a9c4220bc1 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/using-cookies.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/using-cookies.feature.ts @@ -37,7 +37,7 @@ describe('Feature: Using cookies', () => { it('Example: Simple usage with cookies', async () => { - let session: Session|undefined; + let session: Session|null = null; /* ======================= DOCUMENTATION BEGIN ======================= */ @@ -83,7 +83,7 @@ describe('Feature: Using cookies', () => { const app = await createApp(AppController); - strictEqual(session, undefined); + strictEqual(session, null); const response = await request(app) .get('/api/products') @@ -91,7 +91,7 @@ describe('Feature: Using cookies', () => { const token = readCookie(response.get('Set-Cookie'), cookieName).value; - notStrictEqual(session, undefined); + notStrictEqual(session, null); strictEqual((session as unknown as Session).getToken(), token); const response2 = await request(app) diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/using-the-authorization-header.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/using-the-authorization-header.feature.ts index edb2dc97cd..de1f7f11bb 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/using-the-authorization-header.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/using-the-authorization-header.feature.ts @@ -36,7 +36,7 @@ describe('Feature: Using the Authorization header', () => { it('Example: Simple usage with optional bearer tokens', async () => { - let session: Session|undefined; + let session: Session|null = null; /* ======================= DOCUMENTATION BEGIN ======================= */ @@ -85,21 +85,21 @@ describe('Feature: Using the Authorization header', () => { const app = await createApp(AppController); - strictEqual(session, undefined); + strictEqual(session, null); await request(app) .get('/api/products') .expect(200) .expect([]); - strictEqual(session, undefined); + strictEqual(session, null); const response = await request(app) .post('/api/login') .send({}) .expect(200); - strictEqual(session, undefined); + strictEqual(session, null); const token: undefined|string = response.body.token; if (token === undefined) { @@ -112,14 +112,14 @@ describe('Feature: Using the Authorization header', () => { .expect(200) .expect([]); - notStrictEqual(session, undefined); + notStrictEqual(session, null); strictEqual((session as unknown as Session).getToken(), token); }); it('Example: Usage with required bearer tokens', async () => { - let session: Session|undefined; + let session: Session|null = null; /* ======================= DOCUMENTATION BEGIN ======================= */ @@ -168,7 +168,7 @@ describe('Feature: Using the Authorization header', () => { const app = await createApp(AppController); - strictEqual(session, undefined); + strictEqual(session, null); await request(app) .get('/api/products') @@ -178,14 +178,14 @@ describe('Feature: Using the Authorization header', () => { description: 'Authorization header not found.' }); - strictEqual(session, undefined); + strictEqual(session, null); const response = await request(app) .post('/api/login') .send({}) .expect(200); - strictEqual(session, undefined); + strictEqual(session, null); const token: undefined|string = response.body.token; if (token === undefined) { @@ -198,7 +198,7 @@ describe('Feature: Using the Authorization header', () => { .expect(200) .expect([]); - notStrictEqual(session, undefined); + notStrictEqual(session, null); strictEqual((session as unknown as Session).getToken(), token); }); diff --git a/packages/core/src/core/http/context.spec.ts b/packages/core/src/core/http/context.spec.ts index 8260cf05af..1a4950d9e9 100644 --- a/packages/core/src/core/http/context.spec.ts +++ b/packages/core/src/core/http/context.spec.ts @@ -12,7 +12,7 @@ describe('Context', () => { strictEqual(actual.request, request); deepStrictEqual(actual.state, {}); strictEqual(actual.user, undefined); - strictEqual(actual.session, undefined); + strictEqual(actual.session, null); }); }); diff --git a/packages/core/src/core/http/context.ts b/packages/core/src/core/http/context.ts index e469a63c92..f128c00466 100644 --- a/packages/core/src/core/http/context.ts +++ b/packages/core/src/core/http/context.ts @@ -108,8 +108,8 @@ interface Request extends IncomingMessage { * @class Context * @template User */ -export class Context { - state: ContextState = {} as ContextState; +export class Context { + state: ContextState; user: User; session: ContextSession; request: Request; @@ -121,5 +121,7 @@ export class Context { context('given options.create is false or undefined', async () => { - it('should let ctx.session equal undefined.', async () => { + it('should let ctx.session equal null.', async () => { await hook(ctx, services); - strictEqual(ctx.session, undefined); + strictEqual(ctx.session, null); }); }); @@ -306,10 +306,10 @@ describe('UseSessions', () => { beforeEach(() => hook = getHookFunction(UseSessions({ store: Store, cookie: true, create: false }))); - it('should let ctx.session equal undefined.', async () => { + it('should let ctx.session equal null.', async () => { await hook(ctx, services); - strictEqual(ctx.session, undefined); + strictEqual(ctx.session, null); }); }); diff --git a/packages/socket.io/src/architecture/websocket-context.spec.ts b/packages/socket.io/src/architecture/websocket-context.spec.ts index 27adcf7bfe..3de890baa4 100644 --- a/packages/socket.io/src/architecture/websocket-context.spec.ts +++ b/packages/socket.io/src/architecture/websocket-context.spec.ts @@ -18,7 +18,7 @@ describe('WebsocketContext', () => { strictEqual(actual.socket, socket); deepStrictEqual(actual.state, {}); strictEqual(actual.user, undefined); - strictEqual(actual.session, undefined); + strictEqual(actual.session, null); }); }); diff --git a/packages/socket.io/src/architecture/websocket-context.ts b/packages/socket.io/src/architecture/websocket-context.ts index a356d00f20..d56e33c0cc 100644 --- a/packages/socket.io/src/architecture/websocket-context.ts +++ b/packages/socket.io/src/architecture/websocket-context.ts @@ -17,8 +17,8 @@ import { Session } from '@foal/core'; * @template ContextSession * @template ContextState */ -export class WebsocketContext { - state: ContextState = {} as ContextState; +export class WebsocketContext { + state: ContextState; user: User; session: ContextSession; socket: Socket; @@ -32,5 +32,7 @@ export class WebsocketContext { strictEqual(actualContext?.eventName, ''); strictEqual(actualContext?.payload, undefined); - strictEqual(actualContext?.session, undefined); + strictEqual(actualContext?.session, null); notStrictEqual(actualContext?.socket, undefined); notDeepStrictEqual(actualContext?.socket, {}); deepStrictEqual(actualContext?.state, {}); @@ -161,7 +161,7 @@ describe('SocketIOController', () => { deepStrictEqual(actualContext?.payload, payload); deepStrictEqual(actualPayload, payload); - strictEqual(actualContext?.session, undefined); + strictEqual(actualContext?.session, null); notStrictEqual(actualContext?.socket, undefined); notDeepStrictEqual(actualContext?.socket, {}); deepStrictEqual(actualContext?.state, {}); diff --git a/packages/typeorm/package-lock.json b/packages/typeorm/package-lock.json index 43afff5186..16a09f87ef 100644 --- a/packages/typeorm/package-lock.json +++ b/packages/typeorm/package-lock.json @@ -317,7 +317,7 @@ "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true }, "anymatch": { From 3afd6ee4794ba57eb5b9605dbfa76132b8607360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Poullain?= Date: Thu, 26 May 2022 15:04:58 +0200 Subject: [PATCH 2/9] Remove console.log --- packages/mongodb/src/mongodb-store.service.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/mongodb/src/mongodb-store.service.ts b/packages/mongodb/src/mongodb-store.service.ts index f6e54f6b9f..c40ce8ebb5 100644 --- a/packages/mongodb/src/mongodb-store.service.ts +++ b/packages/mongodb/src/mongodb-store.service.ts @@ -23,21 +23,16 @@ export class MongoDBStore extends SessionStore { } async boot() { - console.log('[Debug CI] [MongoDBStore.boot] beginning'); if (!this.mongoDBClient) { const mongoDBURI = Config.getOrThrow( 'settings.mongodb.uri', 'string', 'You must provide the URI of your database when using MongoDBStore.' ); - console.log('[Debug CI] [MongoDBStore.boot] before connect'); this.mongoDBClient = await MongoClient.connect(mongoDBURI); - console.log('[Debug CI] [MongoDBStore.boot] after connect'); } - console.log('[Debug CI] [MongoDBStore.boot] middle'); this.collection = this.mongoDBClient.db().collection('sessions'); this.collection.createIndex({ sessionID: 1 }, { unique: true }); - console.log('[Debug CI] [MongoDBStore.boot] end'); } async save(state: SessionState, maxInactivity: number): Promise { From 6720e60cf04c4cb1cf9baab4d54ce3546dafa4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Poullain?= Date: Thu, 26 May 2022 16:40:21 +0200 Subject: [PATCH 3/9] Ctx.user: undefined -> null --- docs/docs/architecture/controllers.md | 4 +- docs/docs/architecture/hooks.md | 2 +- .../administrators-and-roles.md | 4 +- .../groups-and-permissions.md | 4 +- .../session-tokens.md | 14 +++--- docs/docs/databases/using-another-orm.md | 5 +-- .../10-auth-with-react.md | 2 +- .../11-sign-up.md | 2 +- .../12-file-upload.md | 2 +- .../8-authentication.md | 6 +-- .../9-authenticated-api.md | 2 +- .../mocking-services-in-hooks.feature.ts | 2 +- ...ling-access-with-administrators.feature.ts | 2 +- ...olling-access-with-static-roles.feature.ts | 2 +- ...thentication-and-access-control.feature.ts | 18 ++++---- .../modifying-session-timeouts.feature.ts | 2 +- .../saving-and-reading-content.feature.ts | 4 +- ...ng-a-custom-fetch-user-function.feature.ts | 5 +-- .../common/hooks/user-required.hook.spec.ts | 27 ------------ .../src/common/hooks/user-required.hook.ts | 2 +- packages/core/src/core/hooks.ts | 8 ++-- packages/core/src/core/http/context.spec.ts | 2 +- packages/core/src/core/http/context.ts | 10 +++-- .../core/src/sessions/fetch-user.interface.ts | 4 +- .../src/sessions/use-sessions.hook.spec.ts | 44 +++++++++---------- .../src/app/controllers/profile.controller.ts | 4 +- packages/jwt/src/jwt.hook.spec.ts | 20 ++++----- .../architecture/websocket-context.spec.ts | 2 +- .../src/architecture/websocket-context.ts | 12 +++-- .../src/socketio-controller.service.spec.ts | 4 +- .../src/utils/fetch-mongodb-user.util.spec.ts | 12 +++-- .../src/utils/fetch-mongodb-user.util.ts | 4 +- .../fetch-user-with-permissions.util.spec.ts | 22 ++++++---- .../utils/fetch-user-with-permissions.util.ts | 4 +- .../typeorm/src/utils/fetch-user.util.spec.ts | 16 ++++--- packages/typeorm/src/utils/fetch-user.util.ts | 2 +- 36 files changed, 134 insertions(+), 147 deletions(-) diff --git a/docs/docs/architecture/controllers.md b/docs/docs/architecture/controllers.md index 576de805b5..e28824bbf1 100644 --- a/docs/docs/architecture/controllers.md +++ b/docs/docs/architecture/controllers.md @@ -101,8 +101,8 @@ It has four properties: | --- | --- | --- | | `request` | `Request` | Gives information about the HTTP request. | | `state` | object | Object which can be used to forward data accross several hooks (see [Hooks](./hooks.md)). | -| `user` | `any\|undefined` | The current user (see [Authentication](../authentication-and-access-control/quick-start.md)). | -| `session`| `Session\|undefined` | The session object if you use sessions. | +| `user` | `any\|null` | The current user (see [Authentication](../authentication-and-access-control/quick-start.md)). | +| `session`| `Session\|null` | The session object if you use sessions. | ### HTTP Requests diff --git a/docs/docs/architecture/hooks.md b/docs/docs/architecture/hooks.md index e27c0a9c7b..aa08850d4c 100644 --- a/docs/docs/architecture/hooks.md +++ b/docs/docs/architecture/hooks.md @@ -395,7 +395,7 @@ export class UserService { }; getUser(key: string) { - return this.users[key]; + return this.users[key] ?? null; } } diff --git a/docs/docs/authentication-and-access-control/administrators-and-roles.md b/docs/docs/authentication-and-access-control/administrators-and-roles.md index 7d9f915a96..09e201e5a9 100644 --- a/docs/docs/authentication-and-access-control/administrators-and-roles.md +++ b/docs/docs/authentication-and-access-control/administrators-and-roles.md @@ -33,7 +33,7 @@ import { Context, Hook, HttpResponseForbidden, HttpResponseUnauthorized } from ' import { User } from '../entities'; export function AdminRequired() { - return Hook((ctx: Context) => { + return Hook((ctx: Context) => { if (!ctx.user) { return new HttpResponseUnauthorized(); } @@ -88,7 +88,7 @@ import { Context, Hook, HttpResponseForbidden, HttpResponseUnauthorized } from ' import { User } from '../entities'; export function RoleRequired(role: string) { - return Hook((ctx: Context) => { + return Hook((ctx: Context) => { if (!ctx.user) { return new HttpResponseUnauthorized(); } diff --git a/docs/docs/authentication-and-access-control/groups-and-permissions.md b/docs/docs/authentication-and-access-control/groups-and-permissions.md index 60dbb184b4..6861da8f60 100644 --- a/docs/docs/authentication-and-access-control/groups-and-permissions.md +++ b/docs/docs/authentication-and-access-control/groups-and-permissions.md @@ -340,7 +340,7 @@ export class ProductController { | Context | Response | | --- | --- | -| `ctx.user` is undefined | 401 - UNAUTHORIZED | +| `ctx.user` is null | 401 - UNAUTHORIZED | | `ctx.user.hasPerm('perm')` is false | 403 - FORBIDDEN | ```typescript @@ -349,7 +349,7 @@ export class ProductController { | Context | Response | | --- | --- | -| `ctx.user` is undefined | Redirects to `/login` (302 - FOUND) | +| `ctx.user` is null | Redirects to `/login` (302 - FOUND) | | `ctx.user.hasPerm('perm')` is false | 403 - FORBIDDEN | *Example* diff --git a/docs/docs/authentication-and-access-control/session-tokens.md b/docs/docs/authentication-and-access-control/session-tokens.md index 2c0acfe7a9..6c72e6de2e 100644 --- a/docs/docs/authentication-and-access-control/session-tokens.md +++ b/docs/docs/authentication-and-access-control/session-tokens.md @@ -398,9 +398,9 @@ export class ApiController { } @Get('/products') - readProducts(ctx: Context) { + readProducts(ctx: Context) { // If the ctx.session is defined and the session is attached to a user - // then ctx.user is an instance of User. Otherwise it is undefined. + // then ctx.user is an instance of User. Otherwise it is null. return new HttpResponseOK([]); } @@ -493,7 +493,7 @@ In the following example, the `user` cookie is empty if no user is logged in or *Server-side code* ```typescript -function userToJSON(user: User|undefined) { +function userToJSON(user: User|null) { if (!user) { return 'null'; } @@ -507,13 +507,13 @@ function userToJSON(user: User|undefined) { @UseSessions({ cookie: true, user: fetchUser(User), - userCookie: (ctx, services) => userToJSON(ctx.user) + userCookie: (ctx, services) => userToJSON(ctx.user as User|null) }) export class ApiController { @Get('/products') @UserRequired() - async readProducts(ctx: Context) { + async readProducts(ctx: Context) { const products = await Product.find({ owner: ctx.user }); return new HttpResponseOK(products); } @@ -899,10 +899,10 @@ interface SessionState { The function `fetchUser` from the package `@foal/typeorm` takes an `@Entity()` class as parameter and returns a function with this signature: ```typescript -type FetchUser = (id: string|number, services: ServiceManager) => Promise +type FetchUser = (id: string|number, services: ServiceManager) => Promise ``` -If the ID matches a user, then an instance of the class is returned. Otherwise, the function returns `undefined`. +If the ID matches a user, then an instance of the class is returned. Otherwise, the function returns `null`. If needed you can implement your own `fetchUser` function with this exact signature. diff --git a/docs/docs/databases/using-another-orm.md b/docs/docs/databases/using-another-orm.md index 88e35bee81..454214b8fb 100644 --- a/docs/docs/databases/using-another-orm.md +++ b/docs/docs/databases/using-another-orm.md @@ -29,7 +29,7 @@ To do so, you will have to remove TypeORM and all its utilities and implement so If you wish to use the `user` option of `@JWTRequired` or `@UseSessions` to set the `ctx.user` property, then you will need to implement your own `fetchUser` function. -This utility returns a function that takes an `id` as parameter which might be a `string` or a `number` and returns a promise. The promise value must be `undefined` is no user matches the given `id` and the *user object* otherwise. +This utility returns a function that takes an `id` as parameter which might be a `string` or a `number` and returns a promise. The promise value must be `null` is no user matches the given `id` and the *user object* otherwise. *Example* ```typescript @@ -41,9 +41,6 @@ export function fetchUser(userModel: any): FetchUser { throw new Error('The user ID must be a number.'); } const user = await userModel.findOne({ id }); - if (user === null) { - return undefined; - } return user; }; } diff --git a/docs/docs/tutorials/real-world-example-with-react/10-auth-with-react.md b/docs/docs/tutorials/real-world-example-with-react/10-auth-with-react.md index 9b6c375d8e..12eeb0bc60 100644 --- a/docs/docs/tutorials/real-world-example-with-react/10-auth-with-react.md +++ b/docs/docs/tutorials/real-world-example-with-react/10-auth-with-react.md @@ -53,7 +53,7 @@ import { Context } from '@foal/core'; @UseSessions({ cookie: true, user: fetchUser(User), - userCookie: (ctx: Context) => ctx.user ? JSON.stringify({ id: ctx.user.id, name: ctx.user.name }) : '', + userCookie: (ctx: Context) => ctx.user ? JSON.stringify({ id: ctx.user.id, name: ctx.user.name }) : '', }) ``` diff --git a/docs/docs/tutorials/real-world-example-with-react/11-sign-up.md b/docs/docs/tutorials/real-world-example-with-react/11-sign-up.md index d45e3f313e..e34f2efc6d 100644 --- a/docs/docs/tutorials/real-world-example-with-react/11-sign-up.md +++ b/docs/docs/tutorials/real-world-example-with-react/11-sign-up.md @@ -28,7 +28,7 @@ export class AuthController { @Post('/signup') @ValidateBody(credentialsSchema) - async signup(ctx: Context) { + async signup(ctx: Context) { const email = ctx.request.body.email; const password = ctx.request.body.password; diff --git a/docs/docs/tutorials/real-world-example-with-react/12-file-upload.md b/docs/docs/tutorials/real-world-example-with-react/12-file-upload.md index d03b218c46..62dde28b50 100644 --- a/docs/docs/tutorials/real-world-example-with-react/12-file-upload.md +++ b/docs/docs/tutorials/real-world-example-with-react/12-file-upload.md @@ -59,7 +59,7 @@ export class ProfileController { @Get('/avatar') @ValidateQueryParam('userId', { type: 'number' }, { required: false }) - async readProfileImage(ctx: Context) { + async readProfileImage(ctx: Context) { let user = ctx.user; const userId: number|undefined = ctx.request.query.userId; diff --git a/docs/docs/tutorials/real-world-example-with-react/8-authentication.md b/docs/docs/tutorials/real-world-example-with-react/8-authentication.md index 825b1e894b..6f1a563fa0 100644 --- a/docs/docs/tutorials/real-world-example-with-react/8-authentication.md +++ b/docs/docs/tutorials/real-world-example-with-react/8-authentication.md @@ -76,7 +76,7 @@ export class AuthController { @Post('/login') @ValidateBody(credentialsSchema) - async login(ctx: Context) { + async login(ctx: Context) { const email = ctx.request.body.email; const password = ctx.request.body.password; @@ -99,7 +99,7 @@ export class AuthController { } @Post('/logout') - async logout(ctx: Context) { + async logout(ctx: Context) { await ctx.session.destroy(); return new HttpResponseNoContent(); } @@ -110,5 +110,5 @@ export class AuthController { The `login` method first checks that the user exists and that the credentials provided are correct. If so, it associates the user with the current session. -On subsequent requests, the *UseSessions* hook will retrieve the user's ID from the session and set the `ctx.user` property accordingly. If the user has not previously logged in, then `ctx.user` will be `undefined`. If they have, then `ctx.user` will be an instance of `User`. This is made possible by the `user` option we provided to the hook earlier. It is actually the function that takes the user ID as parameter and returns the value to assign to `ctx.user`. +On subsequent requests, the *UseSessions* hook will retrieve the user's ID from the session and set the `ctx.user` property accordingly. If the user has not previously logged in, then `ctx.user` will be `null`. If they have, then `ctx.user` will be an instance of `User`. This is made possible by the `user` option we provided to the hook earlier. It is actually the function that takes the user ID as parameter and returns the value to assign to `ctx.user`. diff --git a/docs/docs/tutorials/real-world-example-with-react/9-authenticated-api.md b/docs/docs/tutorials/real-world-example-with-react/9-authenticated-api.md index 6ef54e0d1a..205e437783 100644 --- a/docs/docs/tutorials/real-world-example-with-react/9-authenticated-api.md +++ b/docs/docs/tutorials/real-world-example-with-react/9-authenticated-api.md @@ -62,6 +62,6 @@ export class StoriesController { } ``` -When sending a request to these endpoints, the `@UserRequired` hook will return a 401 error if `ctx.user` is not defined (i.e. if the user has not logged in first). But if it is, the controller method will be executed. +When sending a request to these endpoints, the `@UserRequired` hook will return a 401 error if `ctx.user` is not null (i.e. if the user has not logged in first). But if it is, the controller method will be executed. Go to [http://localhost:3001/swagger](http://localhost:3001/swagger) and check that the controller is working as expected. You can, for example, first try to create a story without being connected and then log in and try again. \ No newline at end of file diff --git a/packages/acceptance-tests/src/docs/architecture/hooks/mocking-services-in-hooks.feature.ts b/packages/acceptance-tests/src/docs/architecture/hooks/mocking-services-in-hooks.feature.ts index d7445330cb..5e0ce74541 100644 --- a/packages/acceptance-tests/src/docs/architecture/hooks/mocking-services-in-hooks.feature.ts +++ b/packages/acceptance-tests/src/docs/architecture/hooks/mocking-services-in-hooks.feature.ts @@ -18,7 +18,7 @@ describe('Feature: Mocking services in hooks', () => { }; getUser(key: string) { - return this.users[key]; + return this.users[key] ?? null; } } diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/administrators-and-roles/controlling-access-with-administrators.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/administrators-and-roles/controlling-access-with-administrators.feature.ts index 307c34fe79..fdaa5bcd76 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/administrators-and-roles/controlling-access-with-administrators.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/administrators-and-roles/controlling-access-with-administrators.feature.ts @@ -41,7 +41,7 @@ describe('Feature: Controlling access with administrators', () => { // hooks/admin-required.hook.ts function AdminRequired() { - return Hook((ctx: Context) => { + return Hook((ctx: Context) => { if (!ctx.user) { return new HttpResponseUnauthorized(); } diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/administrators-and-roles/controlling-access-with-static-roles.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/administrators-and-roles/controlling-access-with-static-roles.feature.ts index 1b9f2f8184..ddef12b964 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/administrators-and-roles/controlling-access-with-static-roles.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/administrators-and-roles/controlling-access-with-static-roles.feature.ts @@ -41,7 +41,7 @@ describe('Feature: Controlling access with static roles', () => { // hooks/role-required.hook.ts function RoleRequired(role: string) { - return Hook((ctx: Context) => { + return Hook((ctx: Context) => { if (!ctx.user) { return new HttpResponseUnauthorized(); } diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/adding-authentication-and-access-control.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/adding-authentication-and-access-control.feature.ts index d5416e43de..dd330d3937 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/adding-authentication-and-access-control.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/adding-authentication-and-access-control.feature.ts @@ -38,7 +38,7 @@ describe('Feature: Adding authentication and access control', () => { it('Example: Simple authentication', async () => { - let user: User|undefined; + let user: User|null = null; @Entity() class User extends BaseEntity { @@ -77,7 +77,7 @@ describe('Feature: Adding authentication and access control', () => { } @Get('/products') - readProducts(ctx: Context) { + readProducts(ctx: Context) { // If the ctx.session is defined and the session is attached to a user // then ctx.user is an instance of User. Otherwise it is undefined. // Not in the documentation @@ -104,21 +104,21 @@ describe('Feature: Adding authentication and access control', () => { const user2 = new User(); await user2.save(); - strictEqual(user, undefined); + strictEqual(user, null); await request(app) .get('/api/products') .expect(200) .expect([]); - strictEqual(user, undefined); + strictEqual(user, null); const response = await request(app) .post(`/api/login?id=${user2.id}`) .send({}) .expect(200); - strictEqual(user, undefined); + strictEqual(user, null); const token: undefined|string = response.body.token; if (token === undefined) { @@ -131,7 +131,7 @@ describe('Feature: Adding authentication and access control', () => { .expect(200) .expect([]); - notStrictEqual(user, undefined); + notStrictEqual(user, null); strictEqual((user as unknown as User).id, user2.id); }); @@ -288,7 +288,7 @@ describe('Feature: Adding authentication and access control', () => { /* ======================= DOCUMENTATION BEGIN ======================= */ - function userToJSON(user: User|undefined) { + function userToJSON(user: User|null) { if (!user) { return 'null'; } @@ -302,13 +302,13 @@ describe('Feature: Adding authentication and access control', () => { @UseSessions({ cookie: true, user: fetchUser(User), - userCookie: (ctx, services) => userToJSON(ctx.user) + userCookie: (ctx, services) => userToJSON(ctx.user as User|null) }) class ApiController { @Get('/products') @UserRequired() - async readProducts(ctx: Context) { + async readProducts(ctx: Context) { const products = await Product.find({ owner: ctx.user }); return new HttpResponseOK(products); } diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts index 0d05ad785c..c336cc12c8 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts @@ -41,7 +41,7 @@ describe('Feature: Modifying session timeouts', () => { } @Get('/') - index(ctx: Context) { + index(ctx: Context) { return new HttpResponseOK(); } diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/saving-and-reading-content.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/saving-and-reading-content.feature.ts index 8c7fd1c371..006b7a284f 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/saving-and-reading-content.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/saving-and-reading-content.feature.ts @@ -104,7 +104,7 @@ describe('Feature: Saving and reading content', () => { } @Post('/add-flash-content') - addFlashContent(ctx: Context) { + addFlashContent(ctx: Context) { /* ======================= DOCUMENTATION BEGIN ======================= */ ctx.session.set('error', 'Incorrect email or password', { flash: true }); /* ======================= DOCUMENTATION END ========================= */ @@ -113,7 +113,7 @@ describe('Feature: Saving and reading content', () => { } @Get('/read-flash-content') - readFlashContent(ctx: Context) { + readFlashContent(ctx: Context) { return new HttpResponseOK( ctx.session.get('error', 'No error') ); diff --git a/packages/acceptance-tests/src/docs/databases/using-another-orm/creating-a-custom-fetch-user-function.feature.ts b/packages/acceptance-tests/src/docs/databases/using-another-orm/creating-a-custom-fetch-user-function.feature.ts index 4f00d9c8a4..0d998fd83c 100644 --- a/packages/acceptance-tests/src/docs/databases/using-another-orm/creating-a-custom-fetch-user-function.feature.ts +++ b/packages/acceptance-tests/src/docs/databases/using-another-orm/creating-a-custom-fetch-user-function.feature.ts @@ -16,9 +16,6 @@ describe('Feature: Creating a custom fetch user function.', () => { throw new Error('The user ID must be a number.'); } const user = await userModel.findOne({ id }); - if (user === null) { - return undefined; - } return user; }; } @@ -41,7 +38,7 @@ describe('Feature: Creating a custom fetch user function.', () => { 'The user ID must be a number.' ); deepStrictEqual(await fetchUser(User)(1, services), { id: 1 }) - strictEqual(await fetchUser(User)(2, services), undefined) + strictEqual(await fetchUser(User)(2, services), null) }); diff --git a/packages/core/src/common/hooks/user-required.hook.spec.ts b/packages/core/src/common/hooks/user-required.hook.spec.ts index a9ad584bdb..1da136b197 100644 --- a/packages/core/src/common/hooks/user-required.hook.spec.ts +++ b/packages/core/src/common/hooks/user-required.hook.spec.ts @@ -29,19 +29,6 @@ describe('UserRequired', () => { beforeEach(() => hook = getHookFunction(UserRequired())); - context('given Context.user is undefined', () => { - - beforeEach(() => ctx.user = undefined); - - it('should return an HttpResponseUnauthorized instance.', () => { - const response = hook(ctx, services); - if (!isHttpResponseUnauthorized(response)) { - throw new Error('The hook should have returned an HttpResponseUnauthorized instance.'); - } - }); - - }); - context('given Context.user is null', () => { beforeEach(() => ctx.user = null); @@ -74,20 +61,6 @@ describe('UserRequired', () => { beforeEach(() => hook = getHookFunction(UserRequired({ redirectTo: path }))); - context('given Context.user is undefined', () => { - - beforeEach(() => ctx.user = undefined); - - it('should return an HttpResponseRedirect instance.', () => { - const response = hook(ctx, services); - if (!isHttpResponseRedirect(response)) { - throw new Error('The hook should have returned an HttpResponseRedirect instance.'); - } - strictEqual(response.path, path); - }); - - }); - context('given Context.user is null', () => { beforeEach(() => ctx.user = null); diff --git a/packages/core/src/common/hooks/user-required.hook.ts b/packages/core/src/common/hooks/user-required.hook.ts index 88e9179a59..a61d9371b7 100644 --- a/packages/core/src/common/hooks/user-required.hook.ts +++ b/packages/core/src/common/hooks/user-required.hook.ts @@ -2,7 +2,7 @@ import { ApiResponse, Context, Hook, HookDecorator, HttpResponseRedirect, HttpRe export function UserRequired(options: { redirectTo?: string, openapi?: boolean } = {}): HookDecorator { function hook(ctx: Context) { - if (ctx.user === undefined || ctx.user === null) { + if (!ctx.user) { if (options.redirectTo) { return new HttpResponseRedirect(options.redirectTo); } diff --git a/packages/core/src/core/hooks.ts b/packages/core/src/core/hooks.ts index 34d36ee59e..571ee87f3d 100644 --- a/packages/core/src/core/hooks.ts +++ b/packages/core/src/core/hooks.ts @@ -20,7 +20,7 @@ export type HookPostFunction = (response: HttpResponse) => void | Promise; * * @export */ -export type HookFunction = (ctx: Context, services: ServiceManager) => +export type HookFunction = (ctx: C, services: ServiceManager) => void | HttpResponse | HookPostFunction | Promise ; /** @@ -37,12 +37,12 @@ export type HookDecorator = (target: any, propertyKey?: string) => any; * @param {HookFunction[]} hookFunction - The function from which the hook should be created. * @returns {HookDecorator} - The hook decorator. */ -export function Hook( - hookFunction: HookFunction, openApiDecorators: OpenApiDecorator[] = [], options: { openapi?: boolean } = {} +export function Hook( + hookFunction: HookFunction, openApiDecorators: OpenApiDecorator[] = [], options: { openapi?: boolean } = {} ): HookDecorator { return (target: any, propertyKey?: string) => { // Note that propertyKey can be undefined as it's an optional parameter in getMetadata. - const hooks: HookFunction[] = Reflect.getOwnMetadata('hooks', target, propertyKey as string) || []; + const hooks: HookFunction[] = Reflect.getOwnMetadata('hooks', target, propertyKey as string) || []; hooks.unshift(hookFunction); Reflect.defineMetadata('hooks', hooks, target, propertyKey as string); diff --git a/packages/core/src/core/http/context.spec.ts b/packages/core/src/core/http/context.spec.ts index 1a4950d9e9..d1ab4693ac 100644 --- a/packages/core/src/core/http/context.spec.ts +++ b/packages/core/src/core/http/context.spec.ts @@ -11,7 +11,7 @@ describe('Context', () => { const actual = new Context(request); strictEqual(actual.request, request); deepStrictEqual(actual.state, {}); - strictEqual(actual.user, undefined); + strictEqual(actual.user, null); strictEqual(actual.session, null); }); diff --git a/packages/core/src/core/http/context.ts b/packages/core/src/core/http/context.ts index f128c00466..17b58d7e31 100644 --- a/packages/core/src/core/http/context.ts +++ b/packages/core/src/core/http/context.ts @@ -108,11 +108,12 @@ interface Request extends IncomingMessage { * @class Context * @template User */ -export class Context { - state: ContextState; +export class Context { + request: Request; + user: User; session: ContextSession; - request: Request; + state: ContextState; /** * Creates an instance of Context. @@ -121,7 +122,8 @@ export class Context Promise; \ No newline at end of file +export type FetchUser = (id: string|number, services: ServiceManager) => Promise; \ No newline at end of file diff --git a/packages/core/src/sessions/use-sessions.hook.spec.ts b/packages/core/src/sessions/use-sessions.hook.spec.ts index d00a9d1f24..b274979c6a 100644 --- a/packages/core/src/sessions/use-sessions.hook.spec.ts +++ b/packages/core/src/sessions/use-sessions.hook.spec.ts @@ -158,10 +158,10 @@ describe('UseSessions', () => { strictEqual(isHttpResponse(response), false); }); - it('should let ctx.user equal undefined.', async () => { + it('should let ctx.user equal null.', async () => { await hook(ctx, services); - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); }); context('given options.create is false or undefined', async () => { @@ -284,10 +284,10 @@ describe('UseSessions', () => { strictEqual(isHttpResponse(response), false); }); - it('should let ctx.user equal undefined.', async () => { + it('should let ctx.user equal null.', async () => { await hook(ctx, services); - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); }); context('given options.create is not defined', async () => { @@ -700,11 +700,11 @@ describe('UseSessions', () => { beforeEach(() => ctx = createContext({ Authorization: `Bearer ${anonymousSessionID}`})); - it('with the undefined value.', async () => { + it('with the null value.', async () => { const response = await hook(ctx, services); strictEqual(isHttpResponse(response), false); - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); }); }); @@ -715,11 +715,11 @@ describe('UseSessions', () => { context('given options.user is not defined', () => { - it('with the undefined value.', async () => { + it('with the null value.', async () => { const response = await hook(ctx, services); strictEqual(isHttpResponse(response), false); - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); }); }); @@ -732,7 +732,7 @@ describe('UseSessions', () => { beforeEach(() => { const fetchUser: FetchUser = async (id, services) => { actualServices = services; - return id === userId ? user : undefined + return id === userId ? user : null }; hook = getHookFunction(UseSessions({ store: Store, user: fetchUser })); }); @@ -750,16 +750,16 @@ describe('UseSessions', () => { strictEqual(ctx.user, user); }); - context('given the function options.user returns undefined (session invalid)', () => { + context('given the function options.user returns null (session invalid)', () => { - const fetchUser: FetchUser = async id => undefined; + const fetchUser: FetchUser = async id => null; beforeEach(() => hook = getHookFunction(UseSessions({ store: Store, user: fetchUser }))); - it('with the undefined value and should destroy the session.', async () => { + it('with the null value and should destroy the session.', async () => { await hook(ctx, services); - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); // tslint:disable-next-line strictEqual(ctx.session?.isDestroyed, true); }); @@ -767,7 +767,7 @@ describe('UseSessions', () => { context('given options.cookie is false or not defined', () => { it( - 'with the undefined value and should not remove a session cookie in the response ' + 'with the null value and should not remove a session cookie in the response ' + '(it can belongs to another application).', async () => { const response = await hook(ctx, services); @@ -775,7 +775,7 @@ describe('UseSessions', () => { throw new Error('The hook should have returned an HttpResponse instance.'); } - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); deepStrictEqual(response.getCookies(), {}); } @@ -800,13 +800,13 @@ describe('UseSessions', () => { } }); - it('with the undefined value and should remove the session cookie.', async () => { + it('with the null value and should remove the session cookie.', async () => { const response = await hook(ctx, services); if (!isHttpResponse(response)) { throw new Error('The hook should have returned an HttpResponse instance.'); } - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); const { value, options } = response.getCookie(SESSION_DEFAULT_COOKIE_NAME); strictEqual(value, ''); @@ -830,7 +830,7 @@ describe('UseSessions', () => { throw new Error('The hook should have returned an HttpResponse instance.'); } - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); const { value, options } = response.getCookie(SESSION_USER_COOKIE_NAME); strictEqual(value, ''); @@ -847,7 +847,7 @@ describe('UseSessions', () => { throw new Error('The hook should have returned an HttpResponse instance.'); } - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); const { value } = response.getCookie(SESSION_USER_COOKIE_NAME); strictEqual(value, undefined); @@ -859,10 +859,10 @@ describe('UseSessions', () => { context('given options.redirectTo is not defined', () => { - it('with the undefined value and should return an HttpResponseUnauthorized object.', async () => { + it('with the null value and should return an HttpResponseUnauthorized object.', async () => { const response = await hook(ctx, services); - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); if (!isHttpResponseUnauthorized(response)) { throw new Error('response should be instance of HttpResponseUnauthorized'); @@ -888,7 +888,7 @@ describe('UseSessions', () => { it('with the null value and should return an HttpResponseRedirect object.', async () => { const response = await hook(ctx, services); - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); if (!isHttpResponseRedirect(response)) { throw new Error('response should be instance of HttpResponseRedirect'); diff --git a/packages/examples/src/app/controllers/profile.controller.ts b/packages/examples/src/app/controllers/profile.controller.ts index 67c0dd3b8e..fc66e833a4 100644 --- a/packages/examples/src/app/controllers/profile.controller.ts +++ b/packages/examples/src/app/controllers/profile.controller.ts @@ -18,7 +18,7 @@ export class ProfileController { disk: Disk; @Post('/image') - @Hook(async ctx => { ctx.user = await getRepository(User).findOne({ email: 'john@foalts.org' }); }) + @Hook(async ctx => { ctx.user = await getRepository(User).findOne({ email: 'john@foalts.org' }) ?? null; }) @ValidateMultipartFormDataBody({ files: { profile: { required: true, saveTo: 'images/profiles' } @@ -41,7 +41,7 @@ export class ProfileController { } @Get('/image') - @Hook(async ctx => { ctx.user = await getRepository(User).findOne({ email: 'john@foalts.org' }); }) + @Hook(async ctx => { ctx.user = await getRepository(User).findOne({ email: 'john@foalts.org' }) ?? null; }) async downloadProfilePicture(ctx: Context) { const { profile } = ctx.user; diff --git a/packages/jwt/src/jwt.hook.spec.ts b/packages/jwt/src/jwt.hook.spec.ts index 65fc346781..0525ec9420 100644 --- a/packages/jwt/src/jwt.hook.spec.ts +++ b/packages/jwt/src/jwt.hook.spec.ts @@ -96,7 +96,7 @@ export function testSuite(JWT: typeof JWTOptional|typeof JWTRequired, required: const fetchUser: FetchUser = async (id, services) => { actualServices = services; - return id === '1' ? user : undefined; + return id === '1' ? user : null; }; let ctx: Context; @@ -151,10 +151,10 @@ export function testSuite(JWT: typeof JWTOptional|typeof JWTRequired, required: } else { - it('should let ctx.user equal undefined if the Authorization header does not exist.', async () => { + it('should let ctx.user equal null if the Authorization header does not exist.', async () => { const response = await hook(ctx, services); strictEqual(response, undefined); - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); }); } @@ -195,12 +195,12 @@ export function testSuite(JWT: typeof JWTOptional|typeof JWTRequired, required: } else { - it('should let ctx.user equal undefined if the cookie does not exist.', async () => { + it('should let ctx.user equal null if the cookie does not exist.', async () => { const hook = getHookFunction(JWT({ cookie: true })); const response = await hook(ctx, services); strictEqual(response, undefined); - strictEqual(ctx.user, undefined); + strictEqual(ctx.user, null); }); } @@ -765,7 +765,7 @@ export function testSuite(JWT: typeof JWTOptional|typeof JWTRequired, required: await hook(ctx, services); - notStrictEqual(ctx.user, undefined); + notStrictEqual(ctx.user, null); strictEqual((ctx.user as any).foo, 'bar'); }); @@ -787,7 +787,7 @@ export function testSuite(JWT: typeof JWTOptional|typeof JWTRequired, required: await hook(ctx, services); - notStrictEqual(ctx.user, undefined); + notStrictEqual(ctx.user, null); strictEqual((ctx.user as any).foo, 'bar'); }); @@ -802,7 +802,7 @@ export function testSuite(JWT: typeof JWTOptional|typeof JWTRequired, required: await hook(ctx, services); - notStrictEqual(ctx.user, undefined); + notStrictEqual(ctx.user, null); strictEqual((ctx.user as any).foo, 'bar'); }); @@ -814,7 +814,7 @@ export function testSuite(JWT: typeof JWTOptional|typeof JWTRequired, required: await hook(ctx, services); - notStrictEqual(ctx.user, undefined); + notStrictEqual(ctx.user, null); strictEqual((ctx.user as any).foo, 'bar'); }); @@ -828,7 +828,7 @@ export function testSuite(JWT: typeof JWTOptional|typeof JWTRequired, required: await hook(ctx, services); - notStrictEqual(ctx.user, undefined); + notStrictEqual(ctx.user, null); strictEqual((ctx.user as any).foo, 'bar'); }); diff --git a/packages/socket.io/src/architecture/websocket-context.spec.ts b/packages/socket.io/src/architecture/websocket-context.spec.ts index 3de890baa4..3875f57c85 100644 --- a/packages/socket.io/src/architecture/websocket-context.spec.ts +++ b/packages/socket.io/src/architecture/websocket-context.spec.ts @@ -17,7 +17,7 @@ describe('WebsocketContext', () => { strictEqual(actual.payload, payload); strictEqual(actual.socket, socket); deepStrictEqual(actual.state, {}); - strictEqual(actual.user, undefined); + strictEqual(actual.user, null); strictEqual(actual.session, null); }); diff --git a/packages/socket.io/src/architecture/websocket-context.ts b/packages/socket.io/src/architecture/websocket-context.ts index d56e33c0cc..822f1c9b18 100644 --- a/packages/socket.io/src/architecture/websocket-context.ts +++ b/packages/socket.io/src/architecture/websocket-context.ts @@ -17,11 +17,13 @@ import { Session } from '@foal/core'; * @template ContextSession * @template ContextState */ -export class WebsocketContext { - state: ContextState; +export class WebsocketContext { + socket: Socket; + user: User; session: ContextSession; - socket: Socket; + state: ContextState; + /** * Creates an instance of WebsocketContext. @@ -32,7 +34,9 @@ export class WebsocketContext { notStrictEqual(actualContext?.socket, undefined); notDeepStrictEqual(actualContext?.socket, {}); deepStrictEqual(actualContext?.state, {}); - strictEqual(actualContext?.user, undefined); + strictEqual(actualContext?.user, null); }); it('and should emit a connection error if SocketIOController.onConnection throws or rejects an error.', async () => { @@ -165,7 +165,7 @@ describe('SocketIOController', () => { notStrictEqual(actualContext?.socket, undefined); notDeepStrictEqual(actualContext?.socket, {}); deepStrictEqual(actualContext?.state, {}); - strictEqual(actualContext?.user, undefined); + strictEqual(actualContext?.user, null); }); it('and should return an ok response if the controller method returns a WebsocketResponse (with no payload).', async () => { diff --git a/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts b/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts index 997051af46..ae64320e81 100644 --- a/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts +++ b/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts @@ -65,19 +65,23 @@ describe('fetchMongoDBUser', () => { it('should return the user fetched from the database (id).', async () => { const actual = await fetchMongoDBUser(User)(user.id.toString(), new ServiceManager()); - notStrictEqual(actual, undefined); + if (actual === null) { + throw new Error('The user should not be null.'); + } strictEqual(user.id.equals(actual.id), true); }); it('should return the user fetched from the database (_id).', async () => { const actual = await fetchMongoDBUser(User2)(user2._id.toString(), new ServiceManager()); - notStrictEqual(actual, undefined); + if (actual === null) { + throw new Error('The user should not be null.'); + } strictEqual(user2._id.equals(actual._id), true); }); - it('should return undefined if no user is found in the database (string).', async () => { + it('should return null if no user is found in the database (string).', async () => { const actual = await fetchMongoDBUser(User)('5c584690ba14b143235f195d', new ServiceManager()); - strictEqual(actual, undefined); + strictEqual(actual, null); }); }); diff --git a/packages/typeorm/src/utils/fetch-mongodb-user.util.ts b/packages/typeorm/src/utils/fetch-mongodb-user.util.ts index 0c8dc45524..58249c3b90 100644 --- a/packages/typeorm/src/utils/fetch-mongodb-user.util.ts +++ b/packages/typeorm/src/utils/fetch-mongodb-user.util.ts @@ -18,10 +18,10 @@ import { getMongoRepository, ObjectID } from 'typeorm'; * @returns {FetchUser} The returned function expecting an id. */ export function fetchMongoDBUser(userEntityClass: Class<{ id: ObjectID }|{ _id: ObjectID }>): FetchUser { - return (id: number|string) => { + return async (id: number|string) => { if (typeof id === 'number') { throw new Error('Unexpected type for MongoDB user ID: number.'); } - return getMongoRepository(userEntityClass).findOne(id); + return await getMongoRepository(userEntityClass).findOne(id) ?? null; }; } diff --git a/packages/typeorm/src/utils/fetch-user-with-permissions.util.spec.ts b/packages/typeorm/src/utils/fetch-user-with-permissions.util.spec.ts index d4e6ec2dd0..64659c9381 100644 --- a/packages/typeorm/src/utils/fetch-user-with-permissions.util.spec.ts +++ b/packages/typeorm/src/utils/fetch-user-with-permissions.util.spec.ts @@ -82,20 +82,26 @@ function testSuite(type: 'mysql' | 'mariadb' | 'postgres' | 'sqlite' | 'better-s it('should return the user fetched from the database (id: number).', async () => { const actual = await fetchUserWithPermissions(User)(user.id, new ServiceManager()); - notStrictEqual(actual, undefined); - strictEqual((actual as User).id, user.id); + if (actual === null) { + throw new Error('The user should not be null.'); + } + strictEqual(actual.id, user.id); }); it('should return the user fetched from the database (id: string).', async () => { const actual = await fetchUserWithPermissions(User)(user.id.toString(), new ServiceManager()); - notStrictEqual(actual, undefined); - strictEqual((actual as User).id, user.id); + if (actual === null) { + throw new Error('The user should not be null.'); + } + strictEqual(actual.id, user.id); }); it('should return the user fetched from the database with their groups and permissions.', async () => { const actual = await fetchUserWithPermissions(User)(user.id, new ServiceManager()); - notStrictEqual(actual, undefined); - strictEqual((actual as User).id, user.id); + if (actual === null) { + throw new Error('The user should not be null.'); + } + strictEqual(actual.id, user.id); ok(Array.isArray(actual.userPermissions), 'userPermissions is not an array'); strictEqual(actual.userPermissions.length, 1); @@ -110,9 +116,9 @@ function testSuite(type: 'mysql' | 'mariadb' | 'postgres' | 'sqlite' | 'better-s strictEqual(actual.groups[0].permissions[0].codeName, 'permission1'); }); - it('should return undefined if no user is found in the database.', async () => { + it('should return null if no user is found in the database.', async () => { const actual = await fetchUserWithPermissions(User)(56, new ServiceManager()); - strictEqual(actual, undefined); + strictEqual(actual, null); }); }); diff --git a/packages/typeorm/src/utils/fetch-user-with-permissions.util.ts b/packages/typeorm/src/utils/fetch-user-with-permissions.util.ts index 624bdd2b95..910acb07d4 100644 --- a/packages/typeorm/src/utils/fetch-user-with-permissions.util.ts +++ b/packages/typeorm/src/utils/fetch-user-with-permissions.util.ts @@ -18,8 +18,8 @@ import { getRepository } from 'typeorm'; * @returns {FetchUser} The returned function expecting an id. */ export function fetchUserWithPermissions(userEntityClass: Class<{ id: number|string }>): FetchUser { - return (id: number|string) => getRepository(userEntityClass).findOne( + return async (id: number|string) => await getRepository(userEntityClass).findOne( { id }, { relations: [ 'userPermissions', 'groups', 'groups.permissions' ] } - ); + ) ?? null; } diff --git a/packages/typeorm/src/utils/fetch-user.util.spec.ts b/packages/typeorm/src/utils/fetch-user.util.spec.ts index 6793712bbe..37657f217f 100644 --- a/packages/typeorm/src/utils/fetch-user.util.spec.ts +++ b/packages/typeorm/src/utils/fetch-user.util.spec.ts @@ -71,19 +71,23 @@ function testSuite(type: 'mysql'|'mariadb'|'postgres'|'sqlite'|'better-sqlite3') it('should return the user fetched from the database (id: number).', async () => { const actual = await fetchUser(User)(user.id, new ServiceManager()); - notStrictEqual(actual, undefined); - strictEqual((actual as User).id, user.id); + if (actual === null) { + throw new Error('The user should not be null.'); + } + strictEqual(actual.id, user.id); }); it('should return the user fetched from the database (id: string).', async () => { const actual = await fetchUser(User)(user.id.toString(), new ServiceManager()); - notStrictEqual(actual, undefined); - strictEqual((actual as User).id, user.id); + if (actual === null) { + throw new Error('The user should not be null.'); + } + strictEqual(actual.id, user.id); }); - it('should return undefined if no user is found in the database.', async () => { + it('should return null if no user is found in the database.', async () => { const actual = await fetchUser(User)(56, new ServiceManager()); - strictEqual(actual, undefined); + strictEqual(actual, null); }); }); diff --git a/packages/typeorm/src/utils/fetch-user.util.ts b/packages/typeorm/src/utils/fetch-user.util.ts index 0475315c63..01da233058 100644 --- a/packages/typeorm/src/utils/fetch-user.util.ts +++ b/packages/typeorm/src/utils/fetch-user.util.ts @@ -17,5 +17,5 @@ import { getRepository } from 'typeorm'; * @returns {FetchUser} The returned function expecting an id. */ export function fetchUser(userEntityClass: Class<{ id: number|string }>): FetchUser { - return (id: number|string) => getRepository(userEntityClass).findOne({ id }); + return async (id: number|string) => await getRepository(userEntityClass).findOne({ id }) ?? null; } From d8728d53267c889a8c183f44fec1d9ddb29382e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Poullain?= Date: Thu, 26 May 2022 17:04:11 +0200 Subject: [PATCH 4/9] Remove session generic type from Contexts --- docs/docs/architecture/hooks.md | 2 +- .../quick-start.md | 46 +++++++++---------- .../session-tokens.md | 10 ++-- .../social-auth.md | 5 +- docs/docs/security/csrf-protection.md | 5 +- .../11-sign-up.md | 6 +-- .../15-social-auth.md | 6 +-- .../8-authentication.md | 10 ++-- .../src/additional/session.config.spec.ts | 5 +- ...in-a-stateful-spa-using-cookies.feature.ts | 17 ++++--- ...-stateful-ssr-app-using-cookies.feature.ts | 25 +++++----- .../modifying-session-timeouts.feature.ts | 2 +- .../regenerating-the-session-id.feature.ts | 7 ++- .../saving-and-reading-content.feature.ts | 17 ++++--- ...using-social-auth-with-sessions.feature.ts | 9 ++-- .../csrf/regular-web-app.stateful.spec.ts | 5 +- packages/core/src/core/http/context.ts | 7 +-- packages/examples/src/app/app.controller.ts | 6 +-- .../src/app/controllers/auth.controller.ts | 17 ++++--- .../src/architecture/websocket-context.ts | 6 +-- 20 files changed, 102 insertions(+), 111 deletions(-) diff --git a/docs/docs/architecture/hooks.md b/docs/docs/architecture/hooks.md index aa08850d4c..5a11f118fd 100644 --- a/docs/docs/architecture/hooks.md +++ b/docs/docs/architecture/hooks.md @@ -494,7 +494,7 @@ interface State { export class ApiController { // ... - readOrgName(ctx: Context) { + readOrgName(ctx: Context) { // ... } } diff --git a/docs/docs/authentication-and-access-control/quick-start.md b/docs/docs/authentication-and-access-control/quick-start.md index d4355c8ec7..fb025b9a6b 100644 --- a/docs/docs/authentication-and-access-control/quick-start.md +++ b/docs/docs/authentication-and-access-control/quick-start.md @@ -140,7 +140,7 @@ export class AppController implements IAppController { *src/app/controllers/auth.controller.ts* ```typescript -import { Context, hashPassword, HttpResponseOK, HttpResponseUnauthorized, Post, Session, ValidateBody, verifyPassword } from '@foal/core'; +import { Context, hashPassword, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core'; import { User } from '../entities'; @@ -158,21 +158,21 @@ export class AuthController { @Post('/signup') @ValidateBody(credentialsSchema) - async signup(ctx: Context) { + async signup(ctx: Context) { const user = new User(); user.email = ctx.request.body.email; user.password = await hashPassword(ctx.request.body.password); await user.save(); - ctx.session.setUser(user); - await ctx.session.regenerateID(); + ctx.session!.setUser(user); + await ctx.session!.regenerateID(); return new HttpResponseOK(); } @Post('/login') @ValidateBody(credentialsSchema) - async login(ctx: Context) { + async login(ctx: Context) { const user = await User.findOne({ email: ctx.request.body.email }); if (!user) { @@ -183,15 +183,15 @@ export class AuthController { return new HttpResponseUnauthorized(); } - ctx.session.setUser(user); - await ctx.session.regenerateID(); + ctx.session!.setUser(user); + await ctx.session!.regenerateID(); return new HttpResponseOK(); } @Post('/logout') - async logout(ctx: Context) { - await ctx.session.destroy(); + async logout(ctx: Context) { + await ctx.session!.destroy(); return new HttpResponseOK(); } @@ -719,7 +719,7 @@ npm run migrations *src/app/app.controller.ts* ```typescript -import { Context, controller, dependency, Get, IAppController, render, Session, Store, UserRequired, UseSessions } from '@foal/core'; +import { Context, controller, dependency, Get, IAppController, render, Store, UserRequired, UseSessions } from '@foal/core'; import { fetchUser } from '@foal/typeorm'; import { createConnection } from 'typeorm'; @@ -751,9 +751,9 @@ export class AppController implements IAppController { } @Get('/login') - login(ctx: Context) { + login(ctx: Context) { return render('./templates/login.html', { - errorMessage: ctx.session.get('errorMessage', '') + errorMessage: ctx.session!.get('errorMessage', '') }); } @@ -762,7 +762,7 @@ export class AppController implements IAppController { *src/app/controllers/auth.controller.ts* ```typescript -import { Context, hashPassword, HttpResponseRedirect, Post, Session, ValidateBody, verifyPassword } from '@foal/core'; +import { Context, hashPassword, HttpResponseRedirect, Post, ValidateBody, verifyPassword } from '@foal/core'; import { User } from '../entities'; @@ -780,42 +780,42 @@ export class AuthController { @Post('/signup') @ValidateBody(credentialsSchema) - async signup(ctx: Context) { + async signup(ctx: Context) { const user = new User(); user.email = ctx.request.body.email; user.password = await hashPassword(ctx.request.body.password); await user.save(); - ctx.session.setUser(user); - await ctx.session.regenerateID(); + ctx.session!.setUser(user); + await ctx.session!.regenerateID(); return new HttpResponseRedirect('/'); } @Post('/login') @ValidateBody(credentialsSchema) - async login(ctx: Context) { + async login(ctx: Context) { const user = await User.findOne({ email: ctx.request.body.email }); if (!user) { - ctx.session.set('errorMessage', 'Unknown email.', { flash: true }); + ctx.session!.set('errorMessage', 'Unknown email.', { flash: true }); return new HttpResponseRedirect('/login'); } if (!await verifyPassword(ctx.request.body.password, user.password)) { - ctx.session.set('errorMessage', 'Invalid password.', { flash: true }); + ctx.session!.set('errorMessage', 'Invalid password.', { flash: true }); return new HttpResponseRedirect('/login'); } - ctx.session.setUser(user); - await ctx.session.regenerateID(); + ctx.session!.setUser(user); + await ctx.session!.regenerateID(); return new HttpResponseRedirect('/'); } @Post('/logout') - async logout(ctx: Context) { - await ctx.session.destroy(); + async logout(ctx: Context) { + await ctx.session!.destroy(); return new HttpResponseRedirect('/login'); } diff --git a/docs/docs/authentication-and-access-control/session-tokens.md b/docs/docs/authentication-and-access-control/session-tokens.md index 6c72e6de2e..6b6860da20 100644 --- a/docs/docs/authentication-and-access-control/session-tokens.md +++ b/docs/docs/authentication-and-access-control/session-tokens.md @@ -537,20 +537,20 @@ const user = JSON.parse(decodeURIComponent(/* cookie value */)); You can access and modify the session content with the `set` and `get` methods. ```typescript -import { Context, HttpResponseNoContent, Post, Session, UseSessions } from '@foal/core'; +import { Context, HttpResponseNoContent, Post, UseSessions } from '@foal/core'; @UseSessions(/* ... */) export class ApiController { @Post('/subscribe') - subscribe(ctx: Context) { - const plan = ctx.session.get('plan', 'free'); + subscribe(ctx: Context) { + const plan = ctx.session!.get('plan', 'free'); // ... } @Post('/choose-premium-plan') - choosePremimumPlan(ctx: Context) { - ctx.session.set('plan', 'premium'); + choosePremimumPlan(ctx: Context) { + ctx.session!.set('plan', 'premium'); return new HttpResponseNoContent(); } } diff --git a/docs/docs/authentication-and-access-control/social-auth.md b/docs/docs/authentication-and-access-control/social-auth.md index 4b57d3032e..40e2c9b074 100644 --- a/docs/docs/authentication-and-access-control/social-auth.md +++ b/docs/docs/authentication-and-access-control/social-auth.md @@ -194,7 +194,6 @@ import { dependency, Get, HttpResponseRedirect, - Session, Store, UseSessions, } from '@foal/core'; @@ -218,7 +217,7 @@ export class AuthController { @UseSessions({ cookie: true, }) - async handleGoogleRedirection(ctx: Context) { + async handleGoogleRedirection(ctx: Context) { const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx); if (!userInfo.email) { @@ -234,7 +233,7 @@ export class AuthController { await user.save(); } - ctx.session.setUser(user); + ctx.session!.setUser(user); return new HttpResponseRedirect('/'); } diff --git a/docs/docs/security/csrf-protection.md b/docs/docs/security/csrf-protection.md index 02bb5b24ef..0d398f4b01 100644 --- a/docs/docs/security/csrf-protection.md +++ b/docs/docs/security/csrf-protection.md @@ -374,7 +374,6 @@ import { dependency, Get, render, - Session, Store, UseSessions, } from '@foal/core'; @@ -396,10 +395,10 @@ export class ViewController { required: true, redirectTo: '/login' }) - async index(ctx: Context) { + async index(ctx: Context) { return render( './templates/products.html', - { csrfToken: ctx.session.get('csrfToken') }, + { csrfToken: ctx.session!.get('csrfToken') }, ); } } diff --git a/docs/docs/tutorials/real-world-example-with-react/11-sign-up.md b/docs/docs/tutorials/real-world-example-with-react/11-sign-up.md index e34f2efc6d..708b40f405 100644 --- a/docs/docs/tutorials/real-world-example-with-react/11-sign-up.md +++ b/docs/docs/tutorials/real-world-example-with-react/11-sign-up.md @@ -13,7 +13,7 @@ So far, only users created with the `create-user` script can log in and publish Open the `auth.controller.ts` file and add a new route. ```typescript -import { Context, hashPassword, HttpResponseNoContent, HttpResponseOK, HttpResponseUnauthorized, Post, Session, ValidateBody, verifyPassword } from '@foal/core'; +import { Context, hashPassword, HttpResponseNoContent, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core'; import { User } from '../../entities'; const credentialsSchema = { @@ -28,7 +28,7 @@ export class AuthController { @Post('/signup') @ValidateBody(credentialsSchema) - async signup(ctx: Context) { + async signup(ctx: Context) { const email = ctx.request.body.email; const password = ctx.request.body.password; @@ -39,7 +39,7 @@ export class AuthController { user.password = await hashPassword(password); await user.save(); - ctx.session.setUser(user); + ctx.session!.setUser(user); ctx.user = user; return new HttpResponseOK({ diff --git a/docs/docs/tutorials/real-world-example-with-react/15-social-auth.md b/docs/docs/tutorials/real-world-example-with-react/15-social-auth.md index 5ddf11ae75..0ec852f861 100644 --- a/docs/docs/tutorials/real-world-example-with-react/15-social-auth.md +++ b/docs/docs/tutorials/real-world-example-with-react/15-social-auth.md @@ -111,7 +111,7 @@ Open the file and add two new routes. | `/api/auth/google/callback` | `GET` | Handles redirection from Google once the user has approved the connection. | ```typescript -import { Context, dependency, Get, HttpResponseRedirect, Session } from '@foal/core'; +import { Context, dependency, Get, HttpResponseRedirect } from '@foal/core'; import { GoogleProvider } from '@foal/social'; import { User } from '../../entities'; import * as fetch from 'node-fetch'; @@ -136,7 +136,7 @@ export class SocialAuthController { } @Get('/google/callback') - async handleGoogleRedirection(ctx: Context) { + async handleGoogleRedirection(ctx: Context) { const { userInfo } = await this.google.getUserInfo(ctx); if (!userInfo.email) { @@ -160,7 +160,7 @@ export class SocialAuthController { await user.save(); } - ctx.session.setUser(user); + ctx.session!.setUser(user); ctx.user = user; return new HttpResponseRedirect('/'); diff --git a/docs/docs/tutorials/real-world-example-with-react/8-authentication.md b/docs/docs/tutorials/real-world-example-with-react/8-authentication.md index 6f1a563fa0..a0b5b358e9 100644 --- a/docs/docs/tutorials/real-world-example-with-react/8-authentication.md +++ b/docs/docs/tutorials/real-world-example-with-react/8-authentication.md @@ -59,7 +59,7 @@ Open the new created file and add two routes. | `/api/auth/logout` | `POST` | Logs the user out. | ```typescript -import { Context, hashPassword, HttpResponseNoContent, HttpResponseOK, HttpResponseUnauthorized, Post, Session, ValidateBody, verifyPassword } from '@foal/core'; +import { Context, hashPassword, HttpResponseNoContent, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core'; import { User } from '../../entities'; const credentialsSchema = { @@ -76,7 +76,7 @@ export class AuthController { @Post('/login') @ValidateBody(credentialsSchema) - async login(ctx: Context) { + async login(ctx: Context) { const email = ctx.request.body.email; const password = ctx.request.body.password; @@ -89,7 +89,7 @@ export class AuthController { return new HttpResponseUnauthorized(); } - ctx.session.setUser(user); + ctx.session!.setUser(user); ctx.user = user; return new HttpResponseOK({ @@ -99,8 +99,8 @@ export class AuthController { } @Post('/logout') - async logout(ctx: Context) { - await ctx.session.destroy(); + async logout(ctx: Context) { + await ctx.session!.destroy(); return new HttpResponseNoContent(); } diff --git a/packages/acceptance-tests/src/additional/session.config.spec.ts b/packages/acceptance-tests/src/additional/session.config.spec.ts index b0d028be73..eb8392bc4e 100644 --- a/packages/acceptance-tests/src/additional/session.config.spec.ts +++ b/packages/acceptance-tests/src/additional/session.config.spec.ts @@ -8,7 +8,6 @@ import { HttpResponseOK, Post, ServiceManager, - Session, SessionStore, UseSessions, } from '@foal/core'; @@ -62,8 +61,8 @@ describe('The session store', () => { @Get('/products') @UseSessions({ required: true }) - readProducts(ctx: Context) { - return new HttpResponseOK(ctx.session.get('products')); + readProducts(ctx: Context) { + return new HttpResponseOK(ctx.session!.get('products')); } } diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/quick-start/authenticating-users-in-a-stateful-spa-using-cookies.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/quick-start/authenticating-users-in-a-stateful-spa-using-cookies.feature.ts index 22352afdab..4c33c8a561 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/quick-start/authenticating-users-in-a-stateful-spa-using-cookies.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/quick-start/authenticating-users-in-a-stateful-spa-using-cookies.feature.ts @@ -15,7 +15,6 @@ import { HttpResponseUnauthorized, IAppController, Post, - Session, Store, UserRequired, UseSessions, @@ -60,21 +59,21 @@ describe('Feature: Authenticating users in a stateful SPA using cookies', () => @Post('/signup') @ValidateBody(credentialsSchema) - async signup(ctx: Context) { + async signup(ctx: Context) { const user = new User(); user.email = ctx.request.body.email; user.password = await hashPassword(ctx.request.body.password); await user.save(); - ctx.session.setUser(user); - await ctx.session.regenerateID(); + ctx.session!.setUser(user); + await ctx.session!.regenerateID(); return new HttpResponseOK(); } @Post('/login') @ValidateBody(credentialsSchema) - async login(ctx: Context) { + async login(ctx: Context) { const user = await User.findOne({ email: ctx.request.body.email }); if (!user) { @@ -85,15 +84,15 @@ describe('Feature: Authenticating users in a stateful SPA using cookies', () => return new HttpResponseUnauthorized(); } - ctx.session.setUser(user); - await ctx.session.regenerateID(); + ctx.session!.setUser(user); + await ctx.session!.regenerateID(); return new HttpResponseOK(); } @Post('/logout') - async logout(ctx: Context) { - await ctx.session.destroy(); + async logout(ctx: Context) { + await ctx.session!.destroy(); return new HttpResponseOK(); } diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/quick-start/authenticating-users-in-a-stateful-ssr-app-using-cookies.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/quick-start/authenticating-users-in-a-stateful-ssr-app-using-cookies.feature.ts index 775c6ce308..267456dc0b 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/quick-start/authenticating-users-in-a-stateful-ssr-app-using-cookies.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/quick-start/authenticating-users-in-a-stateful-ssr-app-using-cookies.feature.ts @@ -19,7 +19,6 @@ import { IAppController, Post, render, - Session, Store, UserRequired, UseSessions, @@ -64,42 +63,42 @@ describe('Feature: Authenticating users in a statefull SSR application using coo @Post('/signup') @ValidateBody(credentialsSchema) - async signup(ctx: Context) { + async signup(ctx: Context) { const user = new User(); user.email = ctx.request.body.email; user.password = await hashPassword(ctx.request.body.password); await user.save(); - ctx.session.setUser(user); - await ctx.session.regenerateID(); + ctx.session!.setUser(user); + await ctx.session!.regenerateID(); return new HttpResponseRedirect('/'); } @Post('/login') @ValidateBody(credentialsSchema) - async login(ctx: Context) { + async login(ctx: Context) { const user = await User.findOne({ email: ctx.request.body.email }); if (!user) { - ctx.session.set('errorMessage', 'Unknown email.', { flash: true }); + ctx.session!.set('errorMessage', 'Unknown email.', { flash: true }); return new HttpResponseRedirect('/login'); } if (!await verifyPassword(ctx.request.body.password, user.password)) { - ctx.session.set('errorMessage', 'Invalid password.', { flash: true }); + ctx.session!.set('errorMessage', 'Invalid password.', { flash: true }); return new HttpResponseRedirect('/login'); } - ctx.session.setUser(user); - await ctx.session.regenerateID(); + ctx.session!.setUser(user); + await ctx.session!.regenerateID(); return new HttpResponseRedirect('/'); } @Post('/logout') - async logout(ctx: Context) { - await ctx.session.destroy(); + async logout(ctx: Context) { + await ctx.session!.destroy(); return new HttpResponseRedirect('/login'); } @@ -139,10 +138,10 @@ describe('Feature: Authenticating users in a statefull SSR application using coo } @Get('/login') - login(ctx: Context) { + login(ctx: Context) { // Not in the documentation: __dirname return render('./templates/login.html', { - errorMessage: ctx.session.get('errorMessage', '') + errorMessage: ctx.session!.get('errorMessage', '') }, __dirname); } diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts index c336cc12c8..df0df0ef20 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts @@ -41,7 +41,7 @@ describe('Feature: Modifying session timeouts', () => { } @Get('/') - index(ctx: Context) { + index(ctx: Context) { return new HttpResponseOK(); } diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/regenerating-the-session-id.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/regenerating-the-session-id.feature.ts index 26aaffac74..81dfc2d902 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/regenerating-the-session-id.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/regenerating-the-session-id.feature.ts @@ -14,7 +14,6 @@ import { Get, HttpResponseOK, IAppController, - Session, Store, UseSessions } from '@foal/core'; @@ -46,14 +45,14 @@ describe('Feature: Regenerating the session ID', () => { } @Get('/regenerated-id') - async regenerateID(ctx: Context) { + async regenerateID(ctx: Context) { /* ======================= DOCUMENTATION BEGIN ======================= */ - await ctx.session.regenerateID(); + await ctx.session!.regenerateID(); /* ======================= DOCUMENTATION END ========================= */ - return new HttpResponseOK({ token: ctx.session.getToken() }); + return new HttpResponseOK({ token: ctx.session!.getToken() }); } async init() { diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/saving-and-reading-content.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/saving-and-reading-content.feature.ts index 006b7a284f..dcd2a80dc7 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/saving-and-reading-content.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/saving-and-reading-content.feature.ts @@ -14,7 +14,6 @@ import { IAppController, Post, ServiceManager, - Session, Store, UseSessions } from '@foal/core'; @@ -40,16 +39,16 @@ describe('Feature: Saving and reading content', () => { class ApiController { @Post('/subscribe') - subscribe(ctx: Context) { - const plan = ctx.session.get('plan', 'free'); + subscribe(ctx: Context) { + const plan = ctx.session!.get('plan', 'free'); // ... // Not in the documentation return new HttpResponseOK(plan); } @Post('/choose-premium-plan') - choosePremimumPlan(ctx: Context) { - ctx.session.set('plan', 'premium'); + choosePremimumPlan(ctx: Context) { + ctx.session!.set('plan', 'premium'); return new HttpResponseNoContent(); } } @@ -104,18 +103,18 @@ describe('Feature: Saving and reading content', () => { } @Post('/add-flash-content') - addFlashContent(ctx: Context) { + addFlashContent(ctx: Context) { /* ======================= DOCUMENTATION BEGIN ======================= */ - ctx.session.set('error', 'Incorrect email or password', { flash: true }); + ctx.session!.set('error', 'Incorrect email or password', { flash: true }); /* ======================= DOCUMENTATION END ========================= */ return new HttpResponseOK(); } @Get('/read-flash-content') - readFlashContent(ctx: Context) { + readFlashContent(ctx: Context) { return new HttpResponseOK( - ctx.session.get('error', 'No error') + ctx.session!.get('error', 'No error') ); } } diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts index 1bd5b993d9..9c20ea1e44 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts @@ -13,7 +13,6 @@ import { dependency, Get, HttpResponseRedirect, - Session, Store, UseSessions, } from '@foal/core'; @@ -63,7 +62,7 @@ describe('Feature: Using social auth with sessions', () => { @UseSessions({ cookie: true, }) - async handleGoogleRedirection(ctx: Context) { + async handleGoogleRedirection(ctx: Context) { const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx); if (!userInfo.email) { @@ -79,7 +78,7 @@ describe('Feature: Using social auth with sessions', () => { await user.save(); } - ctx.session.setUser(user); + ctx.session!.setUser(user); return new HttpResponseRedirect('/'); } @@ -113,7 +112,7 @@ describe('Feature: Using social auth with sessions', () => { }); // Known user - const ctx = new Context({ + const ctx = new Context({ query: { code: 'known_user' } @@ -127,7 +126,7 @@ describe('Feature: Using social auth with sessions', () => { deepStrictEqual(ctx.session.userId, user.id); // Unknown user - const ctx2 = new Context({ + const ctx2 = new Context({ query: { code: 'unknown_user' } diff --git a/packages/acceptance-tests/src/docs/security/csrf/regular-web-app.stateful.spec.ts b/packages/acceptance-tests/src/docs/security/csrf/regular-web-app.stateful.spec.ts index 3b65c686bb..72488322fa 100644 --- a/packages/acceptance-tests/src/docs/security/csrf/regular-web-app.stateful.spec.ts +++ b/packages/acceptance-tests/src/docs/security/csrf/regular-web-app.stateful.spec.ts @@ -18,7 +18,6 @@ import { HttpResponseRedirect, Post, render, - Session, UseSessions, ValidateBody, verifyPassword @@ -81,8 +80,8 @@ describe('Feature: Stateful CSRF protection in a Regular Web App', () => { // Nothing in documentation store: TypeORMStore, }) - async index(ctx: Context) { - return new HttpResponseOK({ csrfToken: ctx.session.get('csrfToken') }); + async index(ctx: Context) { + return new HttpResponseOK({ csrfToken: ctx.session!.get('csrfToken') }); // In documentation: // return render( // './templates/products.html', diff --git a/packages/core/src/core/http/context.ts b/packages/core/src/core/http/context.ts index 17b58d7e31..74410c478f 100644 --- a/packages/core/src/core/http/context.ts +++ b/packages/core/src/core/http/context.ts @@ -108,11 +108,11 @@ interface Request extends IncomingMessage { * @class Context * @template User */ -export class Context { +export class Context { request: Request; + session: Session | null; user: User; - session: ContextSession; state: ContextState; /** @@ -122,8 +122,9 @@ export class Context) { + index(ctx: Context) { return render('./templates/index.html', { - userInfo: JSON.stringify(ctx.session.get('userInfo')) + userInfo: JSON.stringify(ctx.session!.get('userInfo')) }); } diff --git a/packages/examples/src/app/controllers/auth.controller.ts b/packages/examples/src/app/controllers/auth.controller.ts index b5e0635d8c..e7b556cf82 100644 --- a/packages/examples/src/app/controllers/auth.controller.ts +++ b/packages/examples/src/app/controllers/auth.controller.ts @@ -5,7 +5,6 @@ import { Get, HttpResponse, HttpResponseRedirect, - Session, UseSessions, } from '@foal/core'; import { FacebookProvider, GithubProvider, GoogleProvider, LinkedInProvider, TwitterProvider } from '@foal/social'; @@ -37,7 +36,7 @@ export class AuthController { } @Get('/signin/google/cb') - async handleGoogleRedirection(ctx: Context) { + async handleGoogleRedirection(ctx: Context) { const { userInfo } = await this.google.getUserInfo(ctx); return this.createSessionAndSaveUserInfo(userInfo, ctx); } @@ -48,7 +47,7 @@ export class AuthController { } @Get('/signin/facebook/cb') - async handleFacebookRedirection(ctx: Context) { + async handleFacebookRedirection(ctx: Context) { const { userInfo } = await this.facebook.getUserInfo(ctx); return this.createSessionAndSaveUserInfo(userInfo, ctx); } @@ -59,7 +58,7 @@ export class AuthController { } @Get('/signin/github/cb') - async handleGithubRedirection(ctx: Context) { + async handleGithubRedirection(ctx: Context) { const { userInfo } = await this.github.getUserInfo(ctx); return this.createSessionAndSaveUserInfo(userInfo, ctx); } @@ -70,7 +69,7 @@ export class AuthController { } @Get('/signin/linkedin/cb') - async handleLinkedInRedirection(ctx: Context) { + async handleLinkedInRedirection(ctx: Context) { const { userInfo } = await this.linkedin.getUserInfo(ctx); return this.createSessionAndSaveUserInfo(userInfo, ctx); } @@ -81,14 +80,14 @@ export class AuthController { } @Get('/signin/twitter/cb') - async handleTwitterRedirection(ctx: Context) { + async handleTwitterRedirection(ctx: Context) { const { userInfo } = await this.twitter.getUserInfo(ctx); return this.createSessionAndSaveUserInfo(userInfo, ctx); } - private async createSessionAndSaveUserInfo(userInfo: any, ctx: Context): Promise { - ctx.session.set('userInfo', userInfo); - ctx.session.regenerateID(); + private async createSessionAndSaveUserInfo(userInfo: any, ctx: Context): Promise { + ctx.session!.set('userInfo', userInfo); + ctx.session!.regenerateID(); return new HttpResponseRedirect('/'); } diff --git a/packages/socket.io/src/architecture/websocket-context.ts b/packages/socket.io/src/architecture/websocket-context.ts index 822f1c9b18..dedde0d59c 100644 --- a/packages/socket.io/src/architecture/websocket-context.ts +++ b/packages/socket.io/src/architecture/websocket-context.ts @@ -17,11 +17,11 @@ import { Session } from '@foal/core'; * @template ContextSession * @template ContextState */ -export class WebsocketContext { +export class WebsocketContext { socket: Socket; + session: Session | null; user: User; - session: ContextSession; state: ContextState; @@ -34,9 +34,9 @@ export class WebsocketContext Date: Thu, 26 May 2022 19:58:00 +0200 Subject: [PATCH 5/9] [Docs] Update context definition --- docs/docs/architecture/controllers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/architecture/controllers.md b/docs/docs/architecture/controllers.md index e28824bbf1..811a981120 100644 --- a/docs/docs/architecture/controllers.md +++ b/docs/docs/architecture/controllers.md @@ -100,8 +100,8 @@ It has four properties: | Name | Type | Description | | --- | --- | --- | | `request` | `Request` | Gives information about the HTTP request. | -| `state` | object | Object which can be used to forward data accross several hooks (see [Hooks](./hooks.md)). | -| `user` | `any\|null` | The current user (see [Authentication](../authentication-and-access-control/quick-start.md)). | +| `state` | `{ [key: string]: any }` | Object which can be used to forward data accross several hooks (see [Hooks](./hooks.md)). | +| `user` | `{ [key: string]: any }\|null` | The current user (see [Authentication](../authentication-and-access-control/quick-start.md)). | | `session`| `Session\|null` | The session object if you use sessions. | From 81e645ce6499ce8bc85e87ea4012fdbe289aeec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Poullain?= Date: Fri, 27 May 2022 19:17:11 +0200 Subject: [PATCH 6/9] Fix linting --- .../session-tokens/modifying-session-timeouts.feature.ts | 1 - packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts | 2 +- .../typeorm/src/utils/fetch-user-with-permissions.util.spec.ts | 2 +- packages/typeorm/src/utils/fetch-user.util.spec.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts index df0df0ef20..f169e7f3f0 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/session-tokens/modifying-session-timeouts.feature.ts @@ -11,7 +11,6 @@ import { HttpResponseOK, IAppController, ServiceManager, - Session, Store, UseSessions } from '@foal/core'; diff --git a/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts b/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts index ae64320e81..bee91524ac 100644 --- a/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts +++ b/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts @@ -1,6 +1,6 @@ // std import { ServiceManager } from '@foal/core'; -import { notStrictEqual, strictEqual } from 'assert'; +import { strictEqual } from 'assert'; // 3p import { Column, createConnection, Entity, getConnection, getMongoManager, ObjectID, ObjectIdColumn } from 'typeorm'; diff --git a/packages/typeorm/src/utils/fetch-user-with-permissions.util.spec.ts b/packages/typeorm/src/utils/fetch-user-with-permissions.util.spec.ts index 64659c9381..5b9c19aca4 100644 --- a/packages/typeorm/src/utils/fetch-user-with-permissions.util.spec.ts +++ b/packages/typeorm/src/utils/fetch-user-with-permissions.util.spec.ts @@ -1,6 +1,6 @@ // std import { ServiceManager } from '@foal/core'; -import { notStrictEqual, ok, strictEqual } from 'assert'; +import { ok, strictEqual } from 'assert'; // 3p import { createConnection, Entity, getConnection, getManager } from 'typeorm'; diff --git a/packages/typeorm/src/utils/fetch-user.util.spec.ts b/packages/typeorm/src/utils/fetch-user.util.spec.ts index 37657f217f..bdc31a010e 100644 --- a/packages/typeorm/src/utils/fetch-user.util.spec.ts +++ b/packages/typeorm/src/utils/fetch-user.util.spec.ts @@ -1,6 +1,6 @@ // std import { ServiceManager } from '@foal/core'; -import { notStrictEqual, strictEqual } from 'assert'; +import { strictEqual } from 'assert'; // 3p import { Column, createConnection, Entity, getConnection, getManager, PrimaryGeneratedColumn } from 'typeorm'; From c586a6ca83cbcb134b0251e5f4cbf36f7513d444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Poullain?= Date: Fri, 27 May 2022 20:01:06 +0200 Subject: [PATCH 7/9] [Docs] Sync tests and docs --- docs/docs/security/csrf-protection.md | 4 +--- .../real-world-example-with-react/10-auth-with-react.md | 2 +- .../social-auth/using-social-auth-with-sessions.feature.ts | 4 +++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docs/security/csrf-protection.md b/docs/docs/security/csrf-protection.md index 0d398f4b01..4d722b9a78 100644 --- a/docs/docs/security/csrf-protection.md +++ b/docs/docs/security/csrf-protection.md @@ -378,8 +378,6 @@ import { UseSessions, } from '@foal/core'; -import { User } from '../entities'; - export class ViewController { @dependency store: Store; @@ -395,7 +393,7 @@ export class ViewController { required: true, redirectTo: '/login' }) - async index(ctx: Context) { + async index(ctx: Context) { return render( './templates/products.html', { csrfToken: ctx.session!.get('csrfToken') }, diff --git a/docs/docs/tutorials/real-world-example-with-react/10-auth-with-react.md b/docs/docs/tutorials/real-world-example-with-react/10-auth-with-react.md index 12eeb0bc60..6faa174dbe 100644 --- a/docs/docs/tutorials/real-world-example-with-react/10-auth-with-react.md +++ b/docs/docs/tutorials/real-world-example-with-react/10-auth-with-react.md @@ -53,7 +53,7 @@ import { Context } from '@foal/core'; @UseSessions({ cookie: true, user: fetchUser(User), - userCookie: (ctx: Context) => ctx.user ? JSON.stringify({ id: ctx.user.id, name: ctx.user.name }) : '', + userCookie: (ctx) => ctx.user ? JSON.stringify({ id: ctx.user.id, name: ctx.user.name }) : '', }) ``` diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts index 9c20ea1e44..10c1e23b98 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts @@ -62,7 +62,7 @@ describe('Feature: Using social auth with sessions', () => { @UseSessions({ cookie: true, }) - async handleGoogleRedirection(ctx: Context) { + async handleGoogleRedirection(ctx: Context) { const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx); if (!userInfo.email) { @@ -85,6 +85,8 @@ describe('Feature: Using social auth with sessions', () => { } + } + /* ======================= DOCUMENTATION END ========================= */ await createTestConnection([ User, DatabaseSession ]); From 78750f2d3a01b362598e428b9e987b51f585604a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Poullain?= Date: Fri, 27 May 2022 20:09:55 +0200 Subject: [PATCH 8/9] Remove a } --- .../social-auth/using-social-auth-with-sessions.feature.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts index 10c1e23b98..9f4ac2d905 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts @@ -85,8 +85,6 @@ describe('Feature: Using social auth with sessions', () => { } - } - /* ======================= DOCUMENTATION END ========================= */ await createTestConnection([ User, DatabaseSession ]); From 73f07e6dd34ce075744bee72ad00803b4a1ed55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Poullain?= Date: Fri, 27 May 2022 20:26:01 +0200 Subject: [PATCH 9/9] Fix test --- .../social-auth/using-social-auth-with-sessions.feature.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts b/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts index 9f4ac2d905..598ed9c38f 100644 --- a/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication-and-access-control/social-auth/using-social-auth-with-sessions.feature.ts @@ -112,7 +112,7 @@ describe('Feature: Using social auth with sessions', () => { }); // Known user - const ctx = new Context({ + const ctx = new Context({ query: { code: 'known_user' } @@ -126,7 +126,7 @@ describe('Feature: Using social auth with sessions', () => { deepStrictEqual(ctx.session.userId, user.id); // Unknown user - const ctx2 = new Context({ + const ctx2 = new Context({ query: { code: 'unknown_user' }