Skip to content

Commit

Permalink
Merge pull request #1073 from FoalTS/null-contexts
Browse files Browse the repository at this point in the history
Improve `Context` class interface
  • Loading branch information
LoicPoullain authored May 27, 2022
2 parents 384e0b8 + 73f07e6 commit eeb37d0
Show file tree
Hide file tree
Showing 53 changed files with 259 additions and 285 deletions.
6 changes: 3 additions & 3 deletions docs/docs/architecture/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ 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\|undefined` | The current user (see [Authentication](../authentication-and-access-control/quick-start.md)). |
| `session`| `Session\|undefined` | The session object if you use sessions. |
| `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. |


### HTTP Requests
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/architecture/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export class UserService {
};

getUser(key: string) {
return this.users[key];
return this.users[key] ?? null;
}
}

Expand Down Expand Up @@ -494,7 +494,7 @@ interface State {

export class ApiController {
// ...
readOrgName(ctx: Context<any, any, State>) {
readOrgName(ctx: Context<any, State>) {
// ...
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { Context, Hook, HttpResponseForbidden, HttpResponseUnauthorized } from '
import { User } from '../entities';

export function AdminRequired() {
return Hook((ctx: Context<User>) => {
return Hook((ctx: Context<User|null>) => {
if (!ctx.user) {
return new HttpResponseUnauthorized();
}
Expand Down Expand Up @@ -88,7 +88,7 @@ import { Context, Hook, HttpResponseForbidden, HttpResponseUnauthorized } from '
import { User } from '../entities';

export function RoleRequired(role: string) {
return Hook((ctx: Context<User>) => {
return Hook((ctx: Context<User|null>) => {
if (!ctx.user) {
return new HttpResponseUnauthorized();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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*
Expand Down
46 changes: 23 additions & 23 deletions docs/docs/authentication-and-access-control/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -158,21 +158,21 @@ export class AuthController {

@Post('/signup')
@ValidateBody(credentialsSchema)
async signup(ctx: Context<any, Session>) {
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<any, Session>) {
async login(ctx: Context) {
const user = await User.findOne({ email: ctx.request.body.email });

if (!user) {
Expand All @@ -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<any, Session>) {
await ctx.session.destroy();
async logout(ctx: Context) {
await ctx.session!.destroy();

return new HttpResponseOK();
}
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -751,9 +751,9 @@ export class AppController implements IAppController {
}

@Get('/login')
login(ctx: Context<any, Session>) {
login(ctx: Context) {
return render('./templates/login.html', {
errorMessage: ctx.session.get<string>('errorMessage', '')
errorMessage: ctx.session!.get<string>('errorMessage', '')
});
}

Expand All @@ -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';

Expand All @@ -780,42 +780,42 @@ export class AuthController {

@Post('/signup')
@ValidateBody(credentialsSchema)
async signup(ctx: Context<any, Session>) {
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<any, Session>) {
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<any, Session>) {
await ctx.session.destroy();
async logout(ctx: Context) {
await ctx.session!.destroy();

return new HttpResponseRedirect('/login');
}
Expand Down
24 changes: 12 additions & 12 deletions docs/docs/authentication-and-access-control/session-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,9 +398,9 @@ export class ApiController {
}

@Get('/products')
readProducts(ctx: Context) {
readProducts(ctx: Context<User>) {
// 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([]);
}

Expand Down Expand Up @@ -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';
}
Expand All @@ -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<User>) {
const products = await Product.find({ owner: ctx.user });
return new HttpResponseOK(products);
}
Expand All @@ -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<any, Session>) {
const plan = ctx.session.get<string>('plan', 'free');
subscribe(ctx: Context) {
const plan = ctx.session!.get<string>('plan', 'free');
// ...
}

@Post('/choose-premium-plan')
choosePremimumPlan(ctx: Context<any, Session>) {
ctx.session.set('plan', 'premium');
choosePremimumPlan(ctx: Context) {
ctx.session!.set('plan', 'premium');
return new HttpResponseNoContent();
}
}
Expand Down Expand Up @@ -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<any>
type FetchUser = (id: string|number, services: ServiceManager) => Promise<Context['user']>
```
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.
Expand Down
5 changes: 2 additions & 3 deletions docs/docs/authentication-and-access-control/social-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ import {
dependency,
Get,
HttpResponseRedirect,
Session,
Store,
UseSessions,
} from '@foal/core';
Expand All @@ -218,7 +217,7 @@ export class AuthController {
@UseSessions({
cookie: true,
})
async handleGoogleRedirection(ctx: Context<User, Session>) {
async handleGoogleRedirection(ctx: Context<User>) {
const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx);

if (!userInfo.email) {
Expand All @@ -234,7 +233,7 @@ export class AuthController {
await user.save();
}

ctx.session.setUser(user);
ctx.session!.setUser(user);

return new HttpResponseRedirect('/');
}
Expand Down
5 changes: 1 addition & 4 deletions docs/docs/databases/using-another-orm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
};
}
Expand Down
7 changes: 2 additions & 5 deletions docs/docs/security/csrf-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,10 @@ import {
dependency,
Get,
render,
Session,
Store,
UseSessions,
} from '@foal/core';

import { User } from '../entities';

export class ViewController {
@dependency
store: Store;
Expand All @@ -396,10 +393,10 @@ export class ViewController {
required: true,
redirectTo: '/login'
})
async index(ctx: Context<User, Session>) {
async index(ctx: Context) {
return render(
'./templates/products.html',
{ csrfToken: ctx.session.get('csrfToken') },
{ csrfToken: ctx.session!.get('csrfToken') },
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { Context } from '@foal/core';
@UseSessions({
cookie: true,
user: fetchUser(User),
userCookie: (ctx: Context<User|undefined>) => 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 }) : '',
})
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -28,7 +28,7 @@ export class AuthController {

@Post('/signup')
@ValidateBody(credentialsSchema)
async signup(ctx: Context<User|undefined, Session>) {
async signup(ctx: Context<User|null>) {
const email = ctx.request.body.email;
const password = ctx.request.body.password;

Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class ProfileController {

@Get('/avatar')
@ValidateQueryParam('userId', { type: 'number' }, { required: false })
async readProfileImage(ctx: Context<User|undefined>) {
async readProfileImage(ctx: Context<User|null>) {
let user = ctx.user;

const userId: number|undefined = ctx.request.query.userId;
Expand Down
Loading

0 comments on commit eeb37d0

Please sign in to comment.