Integrate Ory in a NestJS application
Tags: tutorial, javascript, nestjs, ory
Published: 2024-05-16 07:31:04

---
title: Integrate Ory in a NestJS application
published: true
description: 
tags: tutorial, javascript, nestjs, ory
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uket2a93xgt06clpfv6z.png
series: Integrating Ory in Production with NestJS
canonical_url: https://push-based.io/article/integrate-ory-in-a-nestjs-application
---

This article is the third part of a series that explores integrating Ory, within the NestJS framework. In the previous articles, we introduced Ory and its core components and discussed creating dedicated NestJS libraries to simplify Ory integration. In this post, we will build upon the concepts and libraries developed in the previous articles and dive into the practical aspects of integrating Ory in a NestJS application.

What is better than a concrete example to demonstrate the integration? Our demonstration app is a web-based platform (REST API) called `CatFoster`. `CatFoster` is a community hub where cat owners needing temporary care for their pets can connect with cat lovers willing to foster animals in their homes.

> **Note**
> The [CatFoster](https://github.com/getlarge/cat-fostering/blob/main) application is a simplified example that demonstrates the integration of Ory in a NestJS application. The application will not cover frontend development and deployment, focusing solely on showcasing the backend implementation of the authentication and authorization features of Ory.

The journey to integrate Ory in the `CatFoster` application will be divided into three main phases:

1. **Design Phase:** We will outline the architecture, user flows, entities, and permissions for the `CatFoster` application. This phase will help us understand the requirements and functionalities of the application before diving into the implementation.

2. **Implementation Phase:** We will create a new [Nx](https://nx.dev) workspace, set up a NestJS application, configure [Ory Kratos](https://ory.sh/kratos) and Ory Keto using Docker Compose, and implement the necessary modules, services, controllers, and entities to integrate Ory into the CatFoster application.

3. **Testing Phase:** We will start with manual testing and then write end-to-end tests for the application by running the application locally. This phase will show how to set up an Ory environment for testing.

> **Note**
> If this is your first time working with Ory, I recommend reading the [Introduction to Ory](./Introduction-to-ory.md) article to familiarize yourself with the core components and concepts of Ory.

## Key Features

1. **User Authentication and Profile Management:**
   - **Ory Integration:** Utilize Ory's authentication system to handle user registrations, logins, password resets, and profile management.
   - **User Roles:** There is one static user role, `Admin`, that a super admin can assign to users after registration.

2. **Cat Profiles:**
   - **Listing and Management:** Cat owners can create profiles for their cats, including photos, descriptions, special care instructions, and availability for fostering. Admins can edit and delete cat profiles.
   - **Search and Filters:** Users looking to foster cats can search for them based on filters.

3. **Fostering Matchmaking:**
   - **Requests and Approvals:** Cat fosters can send fostering requests to cat owners, who can review and approve or deny them based on the foster's profiles.
   - **Authorization Checks:** Use Ory to manage authorization, ensuring that only cat owners can approve fostering requests and only registered users can send requests.

## Design phase

### Architecture

![CatFostering Architecture Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g47abqeq8xfnhnqn4ocw.png)

- **Self-service UI**: This is the frontend where users can log in and manage their accounts. It communicates directly with Ory Kratos for authentication-related tasks.
- **Ory Kratos**: Handles authentication. It's responsible for user login, account management, and session management. It interacts with the NestJS app via HTTP webhooks to replicate user data on signup.
- **HTTP Webhooks**: Serve as the communication link between Ory Kratos and the NestJS app, ensuring the user is replicated in the local database upon signup.
- **NestJS App**: The core of your application is handling business logic, CRUD operations with the Postgres database, authentication checks with Ory Kratos, and authorization with Ory Keto.
- **Ory Keto**: Manages authorization, determining what authenticated users are allowed to do within the application.
- **Postgres**: The database where user data (replicated from Ory on signup), cat profiles and fostering requests are stored. The NestJS app interacts with Postgres for all data storage and retrieval operations.

### User Flows

To visualize the user flow for the `CatFoster` project, we will create a series of diagrams using [Mermaid](https://mermaid.js.org/) to illustrate the different user interactions within the system. These interactions include signing up and signing in, creating a cat profile, updating and deleting their cat profiles, requesting to foster a cat, and approving fostering requests.

#### User Sign-Up and Sign-In Flow

![User Sign Up and Sign In Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/69jlutywbzrxcsgx2nf0.png)

#### Cat Profile Edition Flows

It all starts with the user creating a cat profile. Once the profile is created, the user (**owner** or member of the **admin** group) can update or delete it. The following sequence diagrams illustrate these flows.

![Create Cat Profile Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/00bpf96pv0fxnxg4apbd.png)

![Update Cat Profile Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7rger36639ahu4wpk85r.png)

#### Request Fostering Flow

This flow ensures that users can request to foster a cat only if they meet specific criteria, such as not being the cat's owner and not already fostering this cat. This mechanism helps prevent conflicts and ensures a smooth fostering process.

![Request Fostering Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mb26r64u0a1a819rgelm.png)

#### Approve Fostering Request Flow

This flow ensures that only cat owners can approve fostering requests, maintaining control over who fosters their cats.

![Approve Fostering Request Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j0h7bo8xshwh4fz453h0.png)

### Entities and Relationships

The diagram below represents the entities of `CatProfile`, `User`, and `Fostering` in our CatFoster application and illustrates their relationships. This diagram shows each entity's attributes and their associations, such as ownership and fostering relationships.

- **User:** Represents users of the system, which can be cat owners, fosters, or both. Attributes include basic user information like `id`, `name`, `email`. Relationships include `ownedCats`, a list of `CatProfile` entities that the user owns, and `fosteringActivities`, a list of `Fostering` entities indicating the cats they are fostering or have fostered.

- **CatProfile:** Represents the profiles of cats available for fostering. Attributes include the cat's `id`, `name`, `age`, `description`, `ownerId` (linking back to the `User` who owns the cat), and `photosUrls`, a list of URLs to photos of the cat. It has a relationship to `Fostering`, which indicates any fostering activities it's involved in.

- **Fostering:** Represents a fostering arrangement between a user (foster) and a cat. Attributes include `id`, `catId` (linking to the `CatProfile` being fostered), `fosterUserId` (linking to the `User` who is fostering the cat), `startDate`, `endDate`, and `status` (which can include states like pending, active, or completed).

![CatFostering Entity Relationship Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kohe7fx170jqkmj2bm52.png)

### Permissions

In Ory Keto (the authorization component of Ory), developers can express relationships using the [Ory Permission Language](https://www.ory.sh/docs/keto/reference/ory-permission-language). You can compare them to [DDD Aggregates](https://martinfowler.com/bliki/DDD_Aggregate.html), where relations between entities and permissions are defined based on the context of the user and the entity with which they interact.

Ory uses the following terminology:

- **Subjects**: users or groups interacting with the system (e.g., `User`)
- **Objects**: entities in the system (e.g., `CatProfile`, `Fostering`)
- **Relations**: associations between objects (e.g., `owns`, `participatesIn`, `includedIn`)
- **Permissions**: actions that users can perform on objects (e.g., `edit`, `foster`, `approve`)

To manage access control in the `CatFoster` application, we will translate the user flows and entity relationships into Ory permissions:

> **Note**
> The permissions will be implemented using [Ory Permission Language Code](https://www.ory.sh/docs/keto/#ory-permission-language) in the following steps.

![CatFostering Permissions Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ihlotuoz638o5fay2q5m.png)

> **Note**
> The relations [are always defined as arrays](https://www.ory.sh/docs/keto/modeling/create-permission-model#define-relationships-in-the-opl) to allow multiple users or groups to be associated with a specific entity. The permissions are defined as functions that receive a context object and return a boolean value based on the user's authorization level.

## Implementation Phase

In the implementation phase, we will create a new Nx workspace containing our NestJS application, configure Ory Kratos and Ory Keto using Docker Compose, and set up the necessary modules, services, controllers, and entities to integrate Ory into the CatFoster application.

### Setting Up the Nx Workspace

Create a new Nx workspace using the following command:
```bash
npx create-nx-workspace@latest cat-fostering --preset=npm --no-interactive --nxCloud=skip --yes

# Navigate to the newly created workspace
cd cat-fostering
```

Install the NestJS plugin for Nx:
```bash
npx nx add @nx/nest
```

Add a new NestJS application to the workspace:
```bash
npx nx generate @nx/nest:app cat-fostering-api --directory=apps/cat-fostering-api \
--projectNameAndRootFormat="as-provided" --tags "type:app,platform:node"
```

Install the required libraries:
```bash
npm install dotenv @nestjs/config @nestjs/swagger @nestjs/typeorm typeorm pg \
class-validator class-transformer @getlarge/kratos-client-wrapper @getlarge/kratos-cli \
@getlarge/keto-client-wrapper @getlarge/keto-cli @getlarge/keto-relations-parser \
js-yaml nestjs-pino

npm install -D @types/js-yaml
```

### Configuring external services

We will use Docker Compose to set up Ory Kratos and Ory Keto services. This setup will allow us to run these services locally and interact with them from our NestJS application.

To make the Ory services configuration process more straightforward and reusable for all environments (self-hosted and cloud), we will create separate (template) configuration files for Ory Kratos and Ory Keto that can be extended with environment-specific values.

As a convention, we will:

- create an `infra` directory at the root of our workspace to store the configuration files for Ory Kratos and Ory Keto
- prefix environment variables with the service name (e.g., `kratos_` for Ory Kratos and `keto_` for Ory Keto)

> **Note**
> Ory Kratos and Ory Keto supports directly using environment variables for configuration. However, using configuration files can be more convenient for managing multiple environments and sharing configurations across different services and more importantly, to configure the Ory Network (the cloud offering of Ory) tenant.

#### Ory Kratos Configuration

Our Ory Kratos configuration will require four different files:

**kratos-template.yaml:**

In this file, we use the notation `@@` (array) and `##` (string) as placeholders for environment-specific values that a script will substitute with actual values during deployment.
This approach allows us to maintain a single configuration file template that can be customized for different environments.

The configuration file template includes settings for Ory Kratos's self-service UI, authentication methods, error handling, and session management. These settings can be adjusted based on your application's requirements and security policies.

In the configuration file:

- In `dsn`, we define the connection string to the Ory Kratos database. Default to **memory**.
- In `selfservice.flows.registration.after`, we define a webhook that will be consumed after a successful registration, allowing us to create our user in the local database after the user has signed up with Ory Kratos.
- In `identity.schemas`, we define the path to the default schema for user identities. Leaving this field configurable allows us to provide a different identity schema when running the application in various environments.

```yaml
# from https://github.com/getlarge/cat-fostering/blob/main/infra/ory-kratos/kratos-template.yaml
dsn: '##kratos_dsn##'
# ...

identity:
  default_schema_id: default
  schemas:
    - id: default
      url: '##kratos_identity_schemas_default##'

# ...

selfservice:
  allowed_return_urls: @@kratos_selfservice_allowed_return_urls@@
  default_browser_return_url: '##kratos_selfservice_default_browser_return_url##'
  flows:
    error:
      ui_url: '##kratos_selfservice_flows_errors_ui_url##'

    login:
      after:
        code:
          hooks: []
        hooks: []
        lookup_secret:
          hooks: []
        oidc:
          hooks: []
        password:
          hooks:
            - hook: web_hook
              config:
                method: '##kratos_selfservice_flows_login_after_hook_config_method##'
                auth:
                  type: api_key
                  config:
                    value: '##kratos_selfservice_flows_login_after_hook_config_auth_config_value##'
                    in: header
                    name: X-Ory-API-Key
                url: '##kratos_selfservice_flows_login_after_hook_config_url##'
                body: '##kratos_selfservice_flows_login_after_hook_config_body##'
                can_interrupt: ##kratos_selfservice_flows_login_after_hook_config_can_interrupt##
                response:
                  ignore: ##kratos_selfservice_flows_login_after_hook_config_response_ignore##
                  parse: ##kratos_selfservice_flows_login_after_hook_config_response_parse##
        totp:
          hooks: []
        webauthn:
          hooks: []
      before:
        hooks: []
      lifespan: 30m0s
      ui_url: '##kratos_selfservice_flows_login_ui_url##'

    logout:
      after:
        default_browser_return_url: '##kratos_selfservice_default_browser_return_url##'

    recovery:
      after:
        hooks: []
      before:
        hooks: []
      enabled: true
      lifespan: 30m0s
      notify_unknown_recipients: false
      ui_url: '##kratos_selfservice_flows_recovery_ui_url##'
      use: code

    registration:
      after:
        code:
          hooks: []
        hooks: []
        oidc:
          hooks:
          - hook: show_verification_ui
        password:
          hooks:
          - hook: web_hook
            config:
              method: '##kratos_selfservice_flows_registration_after_hook_config_method##'
              auth:
                type: api_key
                config:
                  value: '##kratos_selfservice_flows_registration_after_hook_config_auth_config_value##'
                  in: header
                  name: X-Ory-API-Key
              url: '##kratos_selfservice_flows_registration_after_hook_config_url##'
              body: '##kratos_selfservice_flows_registration_after_hook_config_body##'
              can_interrupt: ##kratos_selfservice_flows_registration_after_hook_config_can_interrupt##
              response:
                ignore: ##kratos_selfservice_flows_registration_after_hook_config_response_ignore##
                parse: ##kratos_selfservice_flows_registration_after_hook_config_response_parse##
          - hook: show_verification_ui
        webauthn:
          hooks:
          - hook: show_verification_ui
      before:
        hooks: []
      enabled: true
      lifespan: 30m0s
      login_hints: true
      ui_url: '##kratos_selfservice_flows_registration_ui_url##'
  #...
  methods:
    code:
      config:
        lifespan: 15m0s
      enabled: true
      mfa_enabled: false
      passwordless_enabled: false
    link:
      config:
        base_url: ""
        lifespan: 15m0s
      enabled: true
    lookup_secret:
      enabled: false
    oidc:
      config:
        providers: []
      enabled: false
    password:
      config:
        haveibeenpwned_enabled: true
        identifier_similarity_check_enabled: true
        ignore_network_errors: true
        max_breaches: 1
        min_password_length: 8
      enabled: true
    profile:
      enabled: true
    totp:
      config:
        issuer: CatFostering
      enabled: true
    webauthn:
      config:
        passwordless: false
        rp:
          display_name: CatFostering
          id: '##kratos_selfservice_methods_webauthn_config_rp_id##'
          origins: @@kratos_selfservice_methods_webauthn_config_rp_origins@@
      enabled: ##kratos_selfservice_methods_webauthn_enabled##
# ...
version: v1.1.0
```

> **Tips**
>
> - For a complete list of configuration options and their descriptions, refer to the [Configuration Reference](https://www.ory.sh/docs/kratos/reference/configuration).
> - The easiest way to create a configuration file for Ory Kratos is to use the [Configuration Editor](https://www.ory.sh/docs/kratos/reference/configuration-editor) tool. This tool provides a user-friendly interface to generate a configuration file based on your application's requirements and security policies.

**identity.schema.json:**

In this file, we set the JSON schema to represent the user identity, including the required properties, formats, and additional settings for authentication, verification, and recovery. Ory Kratos will use this schema to validate user data and enforce security policies.

```json
// infra/ory-kratos/identity.schema.json
{
  "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Person",
  "type": "object",
  "properties": {
    "traits": {
      "type": "object",
      "properties": {
        "email": {
          "type": "string",
          "format": "email",
          "title": "E-Mail",
          "minLength": 3,
          "ory.sh/kratos": {
            "credentials": {
              "password": {
                "identifier": true
              }
            },
            "verification": {
              "via": "email"
            },
            "recovery": {
              "via": "email"
            }
          }
        }
      },
      "required": ["email"],
      "additionalProperties": false
    }
  }
}
```

> **Tips**
> You can learn more about the Ory Kratos identity schema and how to customize it for your application in the [Ory Kratos documentation](https://www.ory.sh/docs/kratos/manage-identities/customize-identity-schema).

**after-webhook.jsonnet:**

This Jsonnet file defines the payload to be sent to the webhook server after a successful registration. In this example, we check if the user's email starts with "test-" and cancel the registration process. You can customize this file to include additional data or perform specific actions based on your application's requirements.

```js
// infra/ory-kratos/after-webhook.jsonnet
function(ctx)
if std.startsWith(ctx.identity.traits.email, "test-") then
  error "cancel"
else
  {
    identity: ctx.identity,
  }
```

> **Tips**
> You can customize this file to include additional data or perform specific actions based on your application's requirements, see [docs](https://www.ory.sh/docs/guides/integrate-with-ory-cloud-through-webhooks#customizing-request-body-with-jsonnet).

#### Ory Keto Configuration

Ory Keto's configuration is less complex than Ory Kratos, as it focuses on defining relationships between entities and permissions based on the Ory Permission Language. We will create three files for Ory Keto:

**keto-template.yaml:**

The `namespace.location` field specifies the path to the policy definitions (see the `namespaces.ts` file below).

```yaml
# ignored by Ory Network
dsn: '##keto_dsn##'

#...
namespaces:
  location: '##keto_namespaces_location##'

# ...
version: v0.12.0-alpha.0
```

> **Tips**
>
> - For a complete list of configuration options and their descriptions, refer to the [Configuration Reference](https://www.ory.sh/docs/keto/reference/configuration).
> - The easiest way to create a configuration file for Ory Keto is to use the [Configuration Editor](https://www.ory.sh/docs/keto/reference/configuration-editor) tool.

**namespaces.ts:**

This file defines the namespaces, relations, and permissions for Ory Keto. It includes the relationships between entities (e.g., `User`, `CatProfile`, `Fostering`) and the permissions that users or groups have on these entities (e.g., `edit`, `foster`, `approve`, etc).

```ts
import type {
  Namespace,
  Context,
  SubjectSet,
  // @ts-expect-error - This is a private type from the internal Ory Keto SDK
} from '@ory/permission-namespace-types';

class User implements Namespace {}

class Group implements Namespace {
  related: {
    members: User[];
  };
}

class CatProfile implements Namespace {
  related: {
    owners: User[];
    editors: SubjectSet<Group, 'members'>[];
  };

  permits = {
    edit: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.editors.includes(ctx.subject),
    foster: (ctx: Context) => !this.related.owners.includes(ctx.subject),
  };
}

class Fostering implements Namespace {
  related: {
    catProfiles: CatProfile[];
    fosterUsers: User[];
  };

  permits = {
    approve: (ctx: Context) =>
      // @ts-expect-error - This is a private type from the internal Ory Keto SDK
      this.related.catProfiles.traverse((cp) =>
        cp.related.owners.includes(ctx.subject)
      ),
    reject: (ctx: Context) => this.permits.approve(ctx),
    edit: (ctx: Context) => this.related.fosterUsers.includes(ctx.subject),
    read: (ctx: Context) => this.permits.approve(ctx) || this.permits.edit(ctx),
  };
}
```

The built-in types from the `@ory/permission-namespace-types` are defined in Typescript as follows:

```ts
declare interface Array<T extends INamespace> {
  /**
   * Checks whether the elements of this Array have a Relation to the given Subject
   * @param element usually `ctx.subject`
   */
  includes(element: T): boolean;

  /**
   * Executes the {@link iteratorfn} on every element in the Array and evaluates to true if {@link iteratorfn} returns true for any element.
   *
   * @param iteratorfn The function that checks if a connection exits
   */
  traverse(iteratorfn: (element: T) => boolean): boolean;
}

interface Context {
  subject: never;
}

interface Namespace {
  /**
   * Possible Relations to Objects of Namespaces
   */
  related?: { [relation: string]: INamespace[] };

  /**
   * Dynamically computed Relations
   */
  permits?: { [method: string]: (ctx: IContext) => boolean };
}
```

#### Ory helpers

To substitute the placeholders in the configuration files with actual values (from the `.env` file), we use a script that reads environment variables and generates the final configuration files for Ory Kratos and Ory Keto.

This script makes the workflow (much) more efficient and less error-prone than manually updating the configuration files. You should check it out in the [tools/ory/generate-config.ts](https://github.com/getlarge/cat-fostering/blob/main/tools/ory/generate-config.ts).

We can consume it with the `package.json` scripts:

```sh
# Generate configuration files in infra/ory-keto/kratos.yaml
npm run ory:generate:kratos
# Generate configuration files in infra/ory-keto/keto.yaml
npm run ory:generate:keto
```

> **Note** As a convention, I created a `tools` directory at the root of the workspace to store the helper scripts and utilities.

#### Docker Compose Configuration

**docker-compose.yaml:**

Our Docker Compose configuration will include services for Ory Kratos, Kratos self-service UI, Ory Keto, and a PostgreSQL database to store user and cat profile data. We will also include a service for MailSlurper, a fake SMTP server that we will use to test email notifications.

```yaml
# docker-compose.yaml
version: '3.2'

networks:
  ory:
    driver: bridge

volumes:
  app_postgres_volume:
  kratos_postgres_volume:
  keto_postgres_volume:

services:
  postgres:
    image: postgres:16
    volumes:
      - app_postgres_volume:/var/lib/postgresql/data
    ports:
      - '5432:5432'
    environment:
      - POSTGRES_USER=dbuser
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=appdb

  kratos-migrate:
    image: oryd/kratos:v1.0.0
    volumes:
      - ./infra/ory-kratos:/etc/config/kratos:ro
    command: -c /etc/config/kratos/kratos.yaml migrate sql -e --yes
    restart: on-failure
    networks:
      - ory

  kratos:
    depends_on:
      - kratos-migrate
    image: oryd/kratos:v1.0.0
    ports:
      - '4433:4433' # public
      - '4434:4434' # admin
    restart: unless-stopped
    command: serve -c /etc/config/kratos/kratos.yaml --dev --watch-courier
    volumes:
      - ./infra/ory-kratos:/etc/config/kratos:ro
    networks:
      - ory
    # for docker on linux
    # extra_hosts:
    #   - "host.docker.internal:host-gateway"

  kratos-postgres:
    image: postgres:16
    environment:
      - POSTGRES_USER=dbuser
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=kratosdb
    volumes:
      - kratos_postgres_volume:/var/lib/postgresql/data
    networks:
      - ory

  kratos-selfservice-ui-node:
    image: oryd/kratos-selfservice-ui-node:v1.0.0
    environment:
      - KRATOS_PUBLIC_URL=http://kratos:4433
      - KRATOS_BROWSER_URL=http://127.0.0.1:4433
      - PORT=4455
    ports:
      - '4455:4455'
    networks:
      - ory
    restart: on-failure

  mailslurper:
    image: oryd/mailslurper:latest-smtps
    ports:
      - '1025:1025'
      - '4436:4436'
      - '4437:4437'
    networks:
      - ory

  keto-migrate:
    image: oryd/keto:v0.12.0
    volumes:
      - ./infra/ory-keto:/etc/config/keto:ro
    command: ['migrate', 'up', '-y', '-c', '/etc/config/keto/keto.yaml']
    restart: on-failure
    networks:
      - ory

  keto:
    depends_on:
      - keto-migrate
    image: oryd/keto:v0.12.0
    ports:
      - '4466:4466' # public
      - '4467:4467' # admin
    command: serve -c /etc/config/keto/keto.yaml
    restart: on-failure
    volumes:
      - ./infra/ory-keto:/etc/config/keto:ro
    networks:
      - ory

  keto-postgres:
    image: postgres:16
    environment:
      - POSTGRES_USER=dbuser
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=accesscontroldb
    volumes:
      - keto_postgres_volume:/var/lib/postgresql/data
    networks:
      - ory
```

> **Note**
>
> - The `kratos-migrate` and `keto-migrate` services run the database migrations for Ory Kratos and Ory Keto, respectively, before starting the main services.
> - The `kratos-selfservice-ui-node` service runs the self-service UI for Ory Kratos, allowing users to interact with the authentication flows with a user-friendly interface.

**.env:**

Create a `.env` file at the root of the workspace, copy the content of `.env.example` file, and update the following values:

```sh
keto_dsn="postgres://dbuser:secret@keto-postgres:5432/accesscontroldb?sslmode=disable"
kratos_courier_smtp_connection_uri="smtps://test:test@mailslurper:1025/?skip_ssl_verify=true"
kratos_dsn="postgres://dbuser:secret@kratos-postgres:5432/kratosdb?sslmode=disable"
kratos_selfservice_flows_registration_after_hook_config_auth_config_value="fetfek-fizNow-guqby0"
kratos_selfservice_flows_login_after_hook_config_auth_config_value="fetfek-fizNow-guqby0"
```

> **Note**
> The `keto_dsn` and `kratos_dsn` values are the connection strings for the PostgreSQL databases used by Ory Keto and Ory Kratos, respectively. These values should match the database services' configurations in the `docker-compose.yaml` file. They are set to `memory` by default, meaning the data will be stored in memory and lost when the service is restarted.

#### Testing the Configuration

To test the configuration, run the following commands:

```sh
npm run ory:generate:kratos
npm run ory:generate:keto
# Start the Docker services
npm run docker:dev:up
# Check the Docker logs to ensure the services are running without errors
#...
# Stop the services
npm run docker:dev:down
```

### Creating the NestJS Application

In the next steps, we will create the necessary modules, services, controllers, and entities in the NestJS application to integrate Ory Kratos and Ory Keto into the CatFoster application.

Each domain (User, CatProfile, Fostering) will have a scoped directory. Under the `libs` directory, we will create a `user` directory for user-related modules, a `cat` directory for cat-related modules, and a `fostering` directory for fostering-related modules. Each directory will contain the service, controller, and other related files in their respective subdirectories/library.

However, entities will be shared in a single directory - `entities` - under the `libs` directory to avoid circular dependencies.

#### Entities

Each domain will have an entity class using TypeORM decorators to define the database schema. Entities' primary and foreign keys will be UUIDs, and the relationships will be declared using the `@ManyToOne`, `@OneToMany`, or `@ManyToMany` decorators. In Ory Keto, the entities' names will qualify as `subjects` and the keys as `objects`.

The entities library with Nx generators:

```sh
npx nx g @nx/js:lib entities --directory libs/shared/entities --importPath @cat-fostering/entities \
--projectNameAndRootFormat as-provided --tags "scope:shared,type:core,platform:node" --no-interactive
```

You can refer to the entity classes in the following files:

- [`libs/shared/entities/src/lib/user-entities.ts`](https://github.com/getlarge/cat-fostering/blob/main/libs/shared/entities/src/lib/user-entities.ts)
- [`libs/shared/entities/src/lib/catprofile-entities.ts`](https://github.com/getlarge/cat-fostering/blob/main/libs/shared/entities/src/lib/catprofile-entities.ts)
- [`libs/shared/entities/src/lib/fostering-entities.ts`](https://github.com/getlarge/cat-fostering/blob/main/libs/shared/entities/src/lib/fostering-entities.ts)

#### Application configuration

The [@nestjs/config](https://github.com/nestjs/config) module will load environment variables and configuration files into the NestJS application. The configuration module will be responsible for loading the Ory Kratos and Ory Keto configuration files and making them available to the application.
To validate the configuration, we will create an `environment-variables.ts` file that checks if the required environment variables are set and the configuration files exist.

```ts
import { Expose, plainToClass } from 'class-transformer';
import {
  IsNumber,
  IsOptional,
  IsString,
  IsUrl,
  validateSync,
} from 'class-validator';

export class EnvironmentVariables {
  @Expose()
  @IsNumber()
  PORT?: number = 3000;

  @Expose()
  @IsUrl({
    protocols: ['postgres', 'postgresql'],
    require_valid_protocol: true,
    require_tld: false,
  })
  POSTGRES_URL?: string = 'postgres://localhost:5432/cat_fostering';

  @Expose()
  @IsUrl({
    require_protocol: true,
    require_valid_protocol: true,
    require_host: true,
    require_tld: false,
  })
  ORY_KETO_ADMIN_URL?: string = 'http://localhost:4467';

  @Expose()
  @IsUrl({
    require_protocol: true,
    require_valid_protocol: true,
    require_host: true,
    require_tld: false,
  })
  ORY_KETO_PUBLIC_URL?: string = 'http://localhost:4466';

  @Expose()
  @IsString()
  @IsOptional()
  ORY_KETO_API_KEY?: string = null;

  @Expose()
  @IsUrl({
    require_protocol: true,
    require_valid_protocol: true,
    require_host: true,
    require_tld: false,
  })
  @IsOptional()
  ORY_KRATOS_ADMIN_URL?: string = 'http://localhost:4434';

  @Expose()
  @IsUrl({
    require_protocol: true,
    require_valid_protocol: true,
    require_host: true,
    require_tld: false,
  })
  @IsOptional()
  ORY_KRATOS_PUBLIC_URL?: string = 'http://localhost:4433';

  @Expose()
  @IsString()
  @IsOptional()
  ORY_KRATOS_API_KEY?: string = null;

  @Expose()
  @IsString()
  ORY_ACTION_API_KEY: string;
}

export function validateEnvironmentVariables(
  processEnv: Record<string, unknown>
) {
  const config = Object.fromEntries(
    Object.entries(processEnv).filter(
      ([, val]) => !!val && val !== 'null' && val !== 'undefined'
    )
  );

  const validatedConfig = plainToClass(EnvironmentVariables, config, {
    enableImplicitConversion: true,
    excludeExtraneousValues: true,
    exposeDefaultValues: true,
  });

  const errors = validateSync(validatedConfig, {
    skipMissingProperties: false,
    forbidUnknownValues: true,
    whitelist: true,
    validationError: {
      target: false,
    },
  });

  if (errors.length > 0) {
    throw new Error(JSON.stringify(errors, null, 2));
  }
  return validatedConfig;
}
```

> **Note**
>
> - When using the cloud hosted Ory Network, `ORY_KETO_ADMIN_URL`, `ORY_KETO_PUBLIC_URL`, `ORY_KRATOS_PUBLIC_URL`, and `ORY_KRATOS_ADMIN_URL` should be configured with the Ory Network tenant URL. `ORY_KETO_API_KEY` and `ORY_KRATOS_API_KEY` should be set to the API key generated on the Ory Network tenant.
> - `ORY_ACTION_API_KEY` is a custom API key used to authenticate requests from Ory Kratos webhooks. It should be the same as the one configured in the Ory Kratos configuration file via `selfservice_flows_registration_after_hook_config_auth_config_value` environment variable.
> - The `validateEnvironmentVariables` function is consumed by the `ConfigModule`, it uses the `class-validator` library to validate the environment variables based on the `EnvironmentVariables` class. If any validation errors occur, the function throws an error with the details of the validation errors.

#### Creating the Users Module

The `UsersModule` will contain the service and controller related to user management; the library will be generated with the NestJS plugin from Nx:

```sh
npx nx g @nx/nest:lib nestjs-user-module --directory libs/user/nestjs-module \
--importPath @cat-fostering/nestjs-user-module --projectNameAndRootFormat as-provided \
--tags "scope:user,type:api,platform:node" --no-interactive
```

The `UsersController` will handle Ory Kratos webhooks registered in [Ory Kratos configuration](#ory-kratos-configuration) to create users in the local database after successful registration and HTTP requests to get current user information.

To protect the routes, we will create the `OryActionGuard` that authenticates requests from Kratos webhooks and use the `OryAuthenticationGuard` (from [@getlarge/kratos-client-wrapper](https://github.com/getlarge/nestjs-ory-integration/blob/main/packages/kratos-client-wrapper/README.md)) that authenticates users using the Kratos session.

```ts
// from https://github.com/getlarge/cat-fostering/blob/main/libs/user/nestjs-module/src/lib/users.controller.ts
// ...
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @UseGuards(OryActionGuard)
  @UsePipes(
    new ValidationPipe({
      transform: true,
      forbidUnknownValues: true,
    })
  )
  @Post('on-sign-up')
  @HttpCode(HttpStatus.OK)
  onSignUp(@Body() body: OnOrySignUpDto): Promise<OnOrySignUpDto> {
    return this.usersService.onSignUp(body);
  }

  @UseGuards(OryActionGuard)
  @UsePipes(
    new ValidationPipe({
      transform: true,
      forbidUnknownValues: true,
    })
  )
  @Post('on-sign-in')
  @HttpCode(HttpStatus.OK)
  onSignIn(@Body() body: OnOrySignInDto): Promise<OnOrySignInDto> {
    return this.usersService.onSignIn(body);
  }

  @UseGuards(
    OryAuthenticationGuard({
      cookieResolver: (ctx) =>
        ctx.switchToHttp().getRequest<Request>().headers.cookie ?? '',
      sessionTokenResolver: (ctx) =>
        ctx
          .switchToHttp()
          .getRequest<Request>()
          .headers?.authorization?.replace('Bearer ', '') ?? '',
      postValidationHook: (ctx, session) => {
        const request = ctx.switchToHttp().getRequest<
          Request & {
            session: ValidOrySession;
            user: {
              id: string;
              email: string;
              identityId: string;
            };
          }
        >();
        if (!isValidOrySession(session)) {
          throw new HttpException('Invalid session', HttpStatus.UNAUTHORIZED);
        }
        request.session = session;
        request.user = {
          id: session.identity.metadata_public['id'],
          email: session.identity.traits.email,
          identityId: session.identity.id,
        };
      },
    })
  )
  @Get('current-user')
  getCurrentUser(@CurrentUser() user: User): User {
    return user;
  }
}
```

> **Note**
>
> - The `onSignUp` and `onSignIn` methods handle the requests from the Ory Kratos webhooks for user registration and sign-in. The `getCurrentUser` method retrieves the current user information from the Ory Kratos session.
> - The `OryActionGuard` authenticates requests from Ory Kratos webhooks using the `ORY_ACTION_API_KEY` environment variable. The `OryAuthenticationGuard` authenticates users using the Kratos session passed in the request headers (cookie or authorization token) and sets the user information in the request object.
> - The paths for the `onSignUp` and `onSignIn` methods should match the paths defined in the Ory Kratos configuration file for the respective webhooks (`selfservice_flows_login_after_hook_config_url` and `selfservice_flows_registration_after_hook_config_url`)
> - Check the previous article for more details on the [OryAuthenticationGuard](./create-nestjs-libraries.md#create-an-authentication-guard)

The `UsersService` is responsible for two tasks:

- After signing up, create an internal user and bind it to the new identity under the `metadata_public` field to store the user's ID in the Ory Kratos session.
- Before signing in, check if the identity has a verified email address unless the identity schema does not require email verification.

```ts
// from https://github.com/getlarge/cat-fostering/blob/main/libs/user/nestjs-module/src/lib/users.service.ts
// ...
@Injectable()
export class UsersService {
  readonly logger = new Logger(UsersService.name);

  constructor(
    @InjectRepository(UserSchema) private userRepository: Repository<UserSchema>
  ) {}

  async onSignUp(body: OnOrySignUpDto): Promise<OnOrySignUpDto> {
    this.logger.debug(
      inspect(body, {
        showHidden: false,
        depth: null,
      }),
      'onSignUp'
    );
    const { email } = body.identity.traits;
    const existingUser = await this.userRepository.findOne({
      where: { email },
    });
    if (existingUser) {
      throw new OryWebhookError(
        'email already used',
        [
          {
            instance_ptr: '#/traits/email',
            messages: [
              {
                id: 123,
                text: 'email already used',
                type: 'validation',
                context: {
                  value: email,
                },
              },
            ],
          },
        ],
        HttpStatus.BAD_REQUEST
      );
    }
    const result = await this.userRepository.save({ email, name: email });
    body.identity.metadata_public = { id: result.id };
    return { identity: body.identity };
  }

  async onSignIn(body: OnOrySignInDto): Promise<OnOrySignInDto> {
    const { identity } = body;
    this.logger.debug(
      inspect(body, {
        showHidden: false,
        depth: null,
      }),
      'onSignIn'
    );
    const email = identity.traits.email;
    const userId = identity.metadata_public?.id;
    const user = await this.userRepository.findOne({
      where: {
        id: userId,
        email,
      },
    });

    if (!user) {
      throw new OryWebhookError(
        'user not found',
        [
          {
            instance_ptr: '#/traits/email',
            messages: [
              {
                id: 123,
                text: 'user not found',
                type: 'validation',
                context: {
                  value: email,
                },
              },
            ],
          },
        ],
        HttpStatus.NOT_FOUND
      );
    }
    if (!identity.verifiable_addresses?.length) {
      return { identity };
    }
    const hasAddressVerified = identity.verifiable_addresses.some(
      (address) => address.verified
    );
    if (!hasAddressVerified) {
      throw new OryWebhookError(
        'Email not verified',
        [
          {
            instance_ptr: '#/verifiable_addresses',
            messages: [
              {
                id: 123,
                text: 'Email not verified',
                type: 'validation',
                context: {
                  value: email,
                },
              },
            ],
          },
        ],
        HttpStatus.UNAUTHORIZED
      );
    }
    return { identity };
  }
}
```

> **Note**
>
> - In the `onSignUp` method modifies the identity before its storage in the Ory Kratos DB, see documentation [here](https://www.ory.sh/docs/guides/integrate-with-ory-cloud-through-webhooks#modify-identities). Since the identity is not created yet, the identity id is set to `00000000-0000-0000-0000-000000000000`.
> - In the `onSignIn` method, if the user's email address is not verified, an error is thrown to prevent login, see [documentation](https://www.ory.sh/docs/guides/integrate-with-ory-cloud-through-webhooks#flow-interrupting-webhooks). This logic is similar to the original `require_verified_address` [hook](https://github.com/ory/kratos/blob/34751a1a3ad9b217af2de7b435b9ee70df510265/selfservice/hook/address_verifier.go) in Ory Kratos. Unless the identity schema does not require email verification, the user can log in without a verified email address. We will use this logic to skip the email verification step for our end-to-end tests.
>   The `OryWebhookError` class is a custom error class that extends the `HttpException` class from NestJS. It formats the error response in the format expected by Ory Kratos webhooks allowing the error message to be displayed in the Self-Service UI.

#### Creating the Cat Profiles Module

The `CatProfilesModule` will contain the service and controller related to cat profile management; the library will be generated with the NestJS plugin from Nx:

```sh
npx nx g @nx/nest:lib nestjs-catprofile-module --directory libs/catprofile/nestjs-module \
--importPath @cat-fostering/nestjs-catprofile-module --projectNameAndRootFormat as-provided \
--tags "scope:catprofile,type:api,platform:node" --no-interactive
```

The `CatProfilesController` will apply the permissions rules from the [cat profile edition flow](#cat-profile-edition-flows). It will handle CRUD operations for cat profiles and use the `OryAuthenticationGuard` and the `OryAuthorizationGuard` (from [@getlarge/keto-client-wrapper](https://github.com/getlarge/nestjs-ory-integration/blob/main/packages/keto-client-wrapper/README.md)) to check the user's permissions before allowing access to the routes.

```ts
// from https://github.com/getlarge/cat-fostering/blob/main/libs/catprofile/nestjs-module/src/lib/catprofiles.controller.ts
// ...
@Controller('cat-profiles')
export class CatProfilesController {
  constructor(private readonly catProfilesService: CatProfilesService) {}

  @UseGuards(AuthenticationGuard())
  @Get()
  find(): Promise<CatProfile[]> {
    return this.catProfilesService.find();
  }

  @UseGuards(AuthenticationGuard())
  @UsePipes(
    new ValidationPipe({
      transform: true,
      transformOptions: { enableImplicitConversion: true },
    })
  )
  @Post()
  create(
    @CurrentUser() user: CurrentUser,
    @Body() body: CreateCatProfile
  ): Promise<CatProfile> {
    return this.catProfilesService.create(body, user.id);
  }

  @UseGuards(AuthenticationGuard())
  @Get(':id')
  findById(@Param('id', ParseUUIDPipe) id: string): Promise<CatProfile> {
    return this.catProfilesService.findById(id);
  }

  @OryPermissionChecks({
    type: 'OR',
    conditions: [isOwnerPermission, isAdminPermission],
  })
  @UseGuards(AuthenticationGuard(), AuthorizationGuard())
  @UsePipes(
    new ValidationPipe({
      transform: true,
      transformOptions: { enableImplicitConversion: true },
    })
  )
  @Patch(':id')
  updateById(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() body: UpdateCatProfile
  ): Promise<CatProfile> {
    return this.catProfilesService.updateById(id, body);
  }

  @OryPermissionChecks({
    type: 'OR',
    conditions: [isOwnerPermission, isAdminPermission],
  })
  @UseGuards(AuthenticationGuard(), AuthorizationGuard())
  @Delete(':id')
  deleteById(@Param('id', ParseUUIDPipe) id: string): Promise<{
    id: string;
  }> {
    return this.catProfilesService.deleteById(id);
  }
}
```

> **Note**
>
> - The `CatProfilesController` uses the `OryPermissionChecks` decorator to define permissions that needs to be evaluated in `OryAuthorizationGuard`.
> - For more details on the `OryAuthorizationGuard` check the [previous article](./create-nestjs-libraries.md#create-an-authorization-guard) and the source code in the [GitHub repository](https://github.com/getlarge/nestjs-ory-integration/blob/main/packages/keto-client-wrapper/src/lib/ory-authorization.guard.ts).

The functions `isOwnerPermission` and `isAdminPermission` are factories constructing the stringified relationships tuples from NestJS `ExecutionContext`.

```ts
// from https://github.com/getlarge/cat-fostering/blob/main/libs/catprofile/nestjs-module/src/lib/helpers.ts
import { CurrentUser, getCurrentUser } from '@cat-fostering/nestjs-utils';
import { relationTupleBuilder } from '@getlarge/keto-relations-parser';
import type { ExecutionContext } from '@nestjs/common';
import type { Request } from 'express';

//...
export const isAdminPermission = (ctx: ExecutionContext): string => {
  const req = ctx.switchToHttp().getRequest<Request & { user: CurrentUser }>();
  const currentUserId = getCurrentUser(req).id;
  return relationTupleBuilder()
    .subject('User', currentUserId)
    .isIn('members')
    .of('Group', 'admin')
    .toString();
};

export const isOwnerPermission = (ctx: ExecutionContext): string => {
  const req = ctx.switchToHttp().getRequest<Request & { user: CurrentUser }>();
  const currentUserId = getCurrentUser(req).id;
  const catProfileId = req.params['id'];
  return relationTupleBuilder()
    .subject('User', currentUserId)
    .isIn('owners')
    .of('CatProfile', catProfileId)
    .toString();
};
```

> **Note**
> The tuple will look like `CatProfile:<catProfileId>#owners@User:<currentUserId>` for the `isOwnerPermission` function and `Group:admin#members@User:<currentUserId>` for the `isAdminPermission` function.

The `CatProfilesService` will use the `CatProfileSchema Repository` to interact with the database. The service will also create relationships between users and cat profiles in Ory Keto.

```ts
// from https://github.com/getlarge/cat-fostering/blob/main/libs/catprofile/nestjs-module/src/lib/catprofiles.service.ts
// ...
@Injectable()
export class CatProfilesService {
  readonly logger = new Logger(CatProfilesService.name);

  constructor(
    @InjectRepository(CatProfileSchema)
    private readonly catProfileRepository: Repository<CatProfileSchema>,
    @Inject(OryRelationshipsService)
    private readonly oryRelationshipsService: OryRelationshipsService
  ) {}

  find() {
    return this.catProfileRepository.find();
  }

  async findById(id: string) {
    const profile = await this.catProfileRepository.findOne({
      where: { id },
      relations: {
        owner: true,
      },
    });
    if (!profile) {
      throw new HttpException('Profile not found', HttpStatus.NOT_FOUND);
    }
    return profile;
  }

  private async createAdminRelationship(catProfileId: string) {
    await this.oryRelationshipsService.createRelationship({
      createRelationshipBody: adminRelationQuery(catProfileId),
    });
  }

  private async createOwnerRelationship(catProfileId: string, userId: string) {
    await this.oryRelationshipsService.createRelationship({
      createRelationshipBody: ownerRelationQuery(catProfileId, userId),
    });
  }

  private async deleteAdminRelationship(catProfileId: string) {
    await this.oryRelationshipsService.deleteRelationships(
      adminRelationQuery(catProfileId)
    );
  }

  private async deleteOwnerRelationship(catProfileId: string, userId: string) {
    await this.oryRelationshipsService.deleteRelationships(
      ownerRelationQuery(catProfileId, userId)
    );
  }

  async create(body: CreateCatProfile, userId: string) {
    const queryRunner =
      this.catProfileRepository.manager.connection.createQueryRunner();
    let catProfile = queryRunner.manager.create(CatProfileSchema, {
      ...body,
      owner: { id: userId },
    });
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      catProfile = await queryRunner.manager.save(catProfile);
      await this.createAdminRelationship(catProfile.id);
      await this.createOwnerRelationship(catProfile.id, userId);
      await queryRunner.commitTransaction();
      return catProfile;
    } catch (err) {
      this.logger.error(err);
      await queryRunner.rollbackTransaction();
      if (catProfile.id) {
        await this.deleteOwnerRelationship(catProfile.id, userId);
        await this.deleteAdminRelationship(catProfile.id);
      }
      throw new HttpException(
        'Failed to create cat profile',
        HttpStatus.BAD_REQUEST
      );
    } finally {
      await queryRunner.release();
    }
  }

  async updateById(id: string, body: UpdateCatProfile) {
    const catProfile = await this.findById(id);
    return this.catProfileRepository.save({
      ...catProfile,
      ...body,
    });
  }

  async deleteById(id: string): Promise<{ id: string }> {
    const catProfile = await this.findById(id);
    await this.deleteOwnerRelationship(id, catProfile.owner.id);
    await this.deleteAdminRelationship(id);
    await this.catProfileRepository.remove(catProfile);
    return { id };
  }
}
```

> **Note**
>
> - The `CatProfilesService` uses the `OryRelationshipsService` to create relationships between users and cat profiles in Ory Keto. The `createAdminRelationship` and `createOwnerRelationship` methods create relationships between the cat profile and the admin group and the user, respectively. The `deleteAdminRelationship` and `deleteOwnerRelationship` methods delete the relationships when the cat profile is deleted.
> - To understand the relationship queries, refer to the [Ory Keto example](https://www.ory.sh/docs/keto/examples/olymp-file-sharing) and the `@getlarge/keto-relations-parser` [documentation](https://github.com/getlarge/nestjs-ory-integration/tree/main/packages/keto-relations-parser)

#### Creating the Fostering Module

The `FosteringModule` will contain the service and controller related to fostering management; the library will be generated with the NestJS plugin from Nx:

```sh
npx nx g @nx/nest:lib nestjs-fostering-module --directory libs/fostering/nestjs-module \
--importPath @cat-fostering/nestjs-fostering-module --projectNameAndRootFormat as-provided \
--tags "scope:fostering,type:api,platform:node" --no-interactive
```

The `FosteringController` will implement the [request fostering](#request-fostering-flow) and [approve fostering request](#approve-fostering-request-flow) flows. As the `CatProfilesController`, it will use the `OryAuthenticationGuard` and the `OryAuthorizationGuard` to authenticate and authorize the requests.

```ts
// from https://github.com/getlarge/cat-fostering/blob/main/libs/fostering/nestjs-module/src/lib/fostering.controller.ts
// ...
@Controller('fostering')
export class FosteringController {
  constructor(private readonly fosteringService: FosteringService) {}

  @UseGuards(AuthenticationGuard())
  @Get()
  find(@CurrentUser() user: CurrentUser): Promise<Fostering[]> {
    return this.fosteringService.find(user.id);
  }

  @OryPermissionChecks(canRequestFosteringPermission)
  @UseGuards(AuthenticationGuard(), AuthorizationGuard())
  @UsePipes(
    new ValidationPipe({
      transform: true,
      transformOptions: { enableImplicitConversion: true },
    })
  )
  @Post()
  request(
    @CurrentUser() user: CurrentUser,
    @Body() body: RequestFostering
  ): Promise<Fostering> {
    return this.fosteringService.request(body, user.id);
  }

  @OryPermissionChecks(canReadFosteringPermission)
  @UseGuards(AuthenticationGuard(), AuthorizationGuard())
  @Get(':id')
  findById(@Param('id', ParseUUIDPipe) id: string): Promise<Fostering> {
    return this.fosteringService.findById(id);
  }

  @OryPermissionChecks(canApproveFosteringPermission)
  @UseGuards(AuthenticationGuard(), AuthorizationGuard())
  @Patch(':id/approve')
  approve(@Param('id', ParseUUIDPipe) id: string): Promise<Fostering> {
    return this.fosteringService.approve(id);
  }

  @OryPermissionChecks(canRejectFosteringPermission)
  @UseGuards(AuthenticationGuard(), AuthorizationGuard())
  @Patch(':id/reject')
  reject(@Param('id', ParseUUIDPipe) id: string): Promise<{
    id: string;
  }> {
    return this.fosteringService.reject(id);
  }
}
```

> **Note**
> The `canRequestFosteringPermission`, `canReadFosteringPermission`, `canApproveFosteringPermission` and `canRejectFosteringPermission` factories will return the following relationship tuples:
>
> - `CatProfile:<catProfileId>#foster@User:<userId>` for the `canRequestFosteringPermission` function
> - `Fostering:<fosteringId>#read@User:<userId>` for the `canReadFosteringPermission` function
> - `Fostering:<fosteringId>#approve@User:<userId>` for the `canApproveFosteringPermission` function
> - `Fostering:<fosteringId>#reject@User:<userId>` for the `canRejectFosteringPermission` function

The `FosteringService` will use the `FosteringSchema Repository` to interact with the database. When a user requests a fostering, the service will create relationships with the cat profile and the current user to limit access to the fostering request. With these relationships:

- The user who requested the fostering can edit the request
- The cat profile owner can approve or reject the fostering request
- Both can read the fostering request

```ts
// from https://github.com/getlarge/cat-fostering/blob/main/libs/fostering/nestjs-module/src/lib/fostering.service.ts
// ...
@Injectable()
export class FosteringService {
  readonly logger = new Logger(FosteringService.name);

  constructor(
    @InjectRepository(FosteringSchema)
    private readonly fosteringRepository: Repository<FosteringSchema>,
    @Inject(OryRelationshipsService)
    private readonly oryRelationshipsService: OryRelationshipsService
  ) {}

  find(userId: string) {
    return this.fosteringRepository.find({
      where: [
        { participant: { id: userId } },
        { catProfile: { owner: { id: userId } } },
      ],
    });
  }

  async findById(id: string) {
    const fostering = await this.fosteringRepository.findOne({
      where: { id },
      relations: {
        participant: true,
        catProfile: true,
      },
    });
    if (!fostering) {
      throw new HttpException('Fostering not found', HttpStatus.NOT_FOUND);
    }
    return fostering;
  }

  private async createParticipantRelationship(
    fosteringId: string,
    userId: string
  ) {
    await this.oryRelationshipsService.createRelationship({
      createRelationshipBody: participantRelationQuery(fosteringId, userId),
    });
  }

  private async createCatProfileRelationship(
    fosteringId: string,
    catProfileId: string
  ) {
    await this.oryRelationshipsService.createRelationship({
      createRelationshipBody: catProfileRelationQuery(
        fosteringId,
        catProfileId
      ),
    });
  }

  private async deleteParticipantRelationship(
    catProfileId: string,
    userId: string
  ) {
    await this.oryRelationshipsService.deleteRelationships(
      participantRelationQuery(catProfileId, userId)
    );
  }

  private async deleteCatProfileRelationship(
    fosteringId: string,
    catProfileId: string
  ) {
    await this.oryRelationshipsService.deleteRelationships(
      catProfileRelationQuery(fosteringId, catProfileId)
    );
  }

  async request(body: RequestFostering, userId: string) {
    const { catProfileId } = body;
    const queryRunner =
      this.fosteringRepository.manager.connection.createQueryRunner();
    let fostering = queryRunner.manager.create(FosteringSchema, {
      ...body,
      status: FosteringStatus.PENDING,
      catProfile: { id: catProfileId },
      participant: { id: userId },
    });

    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      await queryRunner.manager.findOneOrFail(CatProfileSchema, {
        where: { id: catProfileId },
      });
      fostering = await queryRunner.manager.save(fostering);
      await this.createParticipantRelationship(fostering.id, userId);
      await this.createCatProfileRelationship(fostering.id, catProfileId);
      await queryRunner.commitTransaction();
      return fostering;
    } catch (err) {
      this.logger.error(err);
      await queryRunner.rollbackTransaction();
      if (fostering.id) {
        await this.deleteParticipantRelationship(fostering.id, userId);
        await this.deleteCatProfileRelationship(fostering.id, catProfileId);
      }
      throw new HttpException(
        'Failed to request fostering',
        HttpStatus.BAD_REQUEST
      );
    } finally {
      await queryRunner.release();
    }
  }

  // ...
}
```

> **Note**
> The `catProfileRelationQuery` and `participantRelationQuery` factories will return the following relationship tuples:
>
> - `Fostering:<fosteringId>#participants@User:<userId>` for the `participantRelationQuery` function
> - `Fostering:<fosteringId>#catProfiles@CatProfile:<catProfileId>` for the `catProfileRelationQuery` function

#### Creating the App Module

The `AppModule` will import the following modules: the `UsersModule`, the `CatProfilesModule`, and the `FosteringModule` to expose the API endpoints. The module imports and makes available globally the following modules:

- `ConfigModule` to load the environment variables.
- `LoggerModule` to log the application events with Pino.
- `TypeOrmModule` to configure the database connection.

```ts
import { CatProfilesModule } from '@cat-fostering/nestjs-catprofile-module';
import { FosteringModule } from '@cat-fostering/nestjs-fostering-module';
import { UsersModule } from '@cat-fostering/nestjs-user-module';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LoggerModule } from 'nestjs-pino';
import path from 'node:path';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { validateEnvironmentVariables } from './environment-variables';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validate: validateEnvironmentVariables,
      envFilePath: path.resolve(
        path.join(
          'apps',
          'cat-fostering-api',
          process.env['NODE_ENV'] === 'test' ? '.env.test' : '.env'
        )
      ),
    }),
    LoggerModule.forRoot(),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        url: configService.get('POSTGRES_URL'),
        autoLoadEntities: true,
        synchronize: true,
      }),
    }),
    UsersModule,
    CatProfilesModule,
    FosteringModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

> **Note**
>
> - When running the application in test mode, the `ConfigModule` will load the `.env.test` file; this will allow us to use a different database for end-to-end testing.
> - The `TypeOrmModule` is configured to connect to the PostgreSQL database using the `POSTGRES_URL` environment variable.

## Testing phase

We made it; we are the Ory champions! 🏆 But wait, we must test our application to ensure everything works as expected.

### Manual tests

We will start with a round of manual tests to ensure the application behaves as expected. We will create users, assign users to the admin group, edit cat profiles, and manage fostering requests.

I built some CLI tools to help you with the manual tests:

- [kratos-cli](https://github.com/getlarge/nestjs-ory-integration/tree/main/packages/kratos-cli) will help you create users and generate session tokens to authenticate against the API.
- [keto-cli](https://github.com/getlarge/nestjs-ory-integration/tree/main/packages/keto-cli) will help you to create relationships and check permissions.

First of all, [start the services](#testing-the-configuration) and the API:

```sh
npx nx run cat-fostering-api:serve
```

#### Create a new user

```sh
# the CLI will ask for the password
npx @getlarge/kratos-cli register --email <email>
```

#### Verify the user's email

Use the link in the email sent to the user:

- check email in the [local MailSlurper UI](http://localhost:4436)
- open the latest email with the subject: **Please verify your email address**
- click on the link 🎉

#### Test user login

```sh
# the CLI will ask for the password
npx @getlarge/kratos-cli login --email <email>
```

Then copy the session token (starting with **ory_st**) and paste it in the `Authorization` header for the following request:

```sh
export ORY_SESSION_TOKEN=ory_st***

curl -X GET \
 'http://localhost:3000/api/users/current-user' \
 --header 'Content-Type: application/json' \
 --header "Authorization: Bearer ${ORY_SESSION_TOKEN}"

# should return the user info
{"id":"<user_id>","email":"<user_email>","identityId":"<ory_user_id>"}
```

#### Add the user to the admin group

Reproduce the steps above to create a new user and get the `user_id`, which was returned by the previous request.
Then, use the `keto-cli` to add the user to the admin group:

```sh
export USER_ID=<user_id>
npx @getlarge/keto-cli create --tuple "Group:admin#members@User:$USER_ID"
```

It should log the generated relationship:

```json
{
  "namespace": "Group",
  "object": "admin",
  "relation": "members",
  "subject_set": {
    "namespace": "User",
    "object": "<user_id>"
  }
}
```

#### Check the user's permissions

```sh
npx @getlarge/keto-cli check --tuple "Group:admin#members@User:$USER_ID"
# it should return true
```

#### Create a cat profile

```sh
curl -X POST \
  http://localhost:3000/api/cat-profiles \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer ${ORY_SESSION_TOKEN}" \
  -d '{
    "name": "Romeo",
    "age": 2,
    "description": "Romeo is a very playful cat, he loves to play with curtains."
  }'

# it should return the cat profile info
{"name":"Romeo","age":2,"description":"Romeo is a very playful cat, he loves to play with curtains.","owner":{"id":"<user_id>"},"photosUrls":null,"id":"<cat_profile_id>"}
```

#### Update a cat profile

```sh
export CAT_PROFILE_ID=<cat_profile_id>

curl -X PATCH \
  "http://localhost:3000/api/cat-profiles/${CAT_PROFILE_ID}" \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer ${ORY_SESSION_TOKEN}" \
  -d '{
    "description": "Romeo is a very playful cat, he loves to play with curtains and sleep on the couch."
  }'
```

> **Task**
>
> You can test the permissions by:
>
> - updating a cat profile that does not belong to you, using the `cat_profile_id` from another user => it should be forbidden.
> - updating a cat profile with an admin user => it should succeed.

#### Request a fostering

```sh
curl -X POST \
  http://localhost:3000/api/fostering \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer ${ORY_SESSION_TOKEN}" \
  -d '{
    "catProfileId": "<cat_profile_id>",
    "startDate": "2023-12-01",
    "endDate": "2023-12-07"
  }'
# in case of success, it should return the fostering request info
{"startDate":"2023-12-01T00:00:00.000Z","endDate":"2023-12-07T00:00:00.000Z","status":"PENDING","catProfile":{"id":"<cat_profile_id>"},"participant":{"id":"<user_id>"},"id":"<fostering_id>"}
```

> **Task**
>
> You can test the permissions by:
>
> - requesting a fostering for a cat profile that does **not** belong to the current user => it should succeed.
> - requesting a fostering for a cat profile that belongs to the current user => it should be forbidden.

#### Check the fostering request

```sh
export FOSTERING_ID=<fostering_id>

curl -X GET \
  "http://localhost:3000/api/fostering/${FOSTERING_ID}" \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer ${ORY_SESSION_TOKEN}"
```

#### Approve a fostering request

```sh
export FOSTERING_ID=<fostering_id>

curl -X PATCH \
  "http://localhost:3000/api/fostering/${FOSTERING_ID}/approve" \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer ${ORY_SESSION_TOKEN}"
```

> **Task**
>
> You can test the permissions by:
>
> - approving a fostering request targeting the current user's cat profile => it should succeed.
> - approving a fostering request targeting another user's cat profile => it should be forbidden.

### Automated tests

To show you how to configure Ory for your automated tests, we will create end-to-end tests to manage cat profiles.

One of the great benefits of Ory is that you can quickly adapt the configuration for the testing environment and apply it to your Docker containers. For instance, we don't need to persist the data in the Kratos and Keto databases between tests. We can also turn off the email address verification in the Kratos configuration to simplify the testing process.

1. create `.env.test` at the root to use in-memory storage, and load a different identity schema that does not require email verification
2. create `apps/cat-fostering-api/.env.test` with a different database name
3. use Jest global setup to reconfigure Ory Kratos and Ory Keto and initialize the test database before running the tests
4. use Jest global teardown to clean up the test database and revert the Ory Kratos and Ory Keto configurations after running the tests
5. build factories to create users and cat profiles
6. write the end-to-end tests
7. start the application in test mode `npx nx run cat-fostering-api:serve:test` (which will set the `NODE_ENV` to `test`)
8. run the tests `npx nx run cat-fostering-api-e2e:e2e`

#### Configure the testing environment

Create a `.env.test` file, copy the content from the `.env.example` file at the root of the project and update the following variables:

```sh
# .env.test
kratos_identity_schemas_default="file:///etc/config/kratos/identity.schema.test.json"
kratos_selfservice_flows_registration_after_hook_config_auth_config_value="test_api_key"
kratos_selfservice_flows_login_after_hook_config_auth_config_value="test_api_key"
```

Create the `apps/cat-fostering-api/.env.test` file:

```sh
POSTGRES_URL="postgresql://dbuser:secret@localhost:5432/cat_fostering_api_e2e"
ORY_KETO_ADMIN_URL="http://localhost:4467"
ORY_KETO_PUBLIC_URL="http://localhost:4466"
ORY_KRATOS_ADMIN_URL="http://localhost:4434"
ORY_KRATOS_PUBLIC_URL="http://localhost:4433"
ORY_ACTION_API_KEY="test_api_key"
```

#### Configure Jest global setup and teardown

We will use the Jest files auto-generated by Nx to configure the global setup and teardown hooks.

To configure the global setup, update the `apps/cat-fostering-api-e2e/src/support/global-setup.ts` file with:

```ts
import { execSync } from 'node:child_process';

import { createTestConnection } from './helpers';

const envPath = 'apps/cat-fostering-api/.env.test';
const cwd = process.cwd();

export default async (): Promise<void> => {
  console.log('\nSetting up...\n');

  const __TEARDOWN_MESSAGE__ = '\nTearing down...\n';
  globalThis.__TEARDOWN_MESSAGE__ = __TEARDOWN_MESSAGE__;

  execSync(
    'npx ts-node --project tools/tsconfig.json tools/ory/generate-config.ts keto -e .env.test',
    { cwd, stdio: 'ignore' }
  );
  execSync('docker compose restart keto', { cwd, stdio: 'ignore' });
  execSync(
    'npx ts-node --project tools/tsconfig.json tools/ory/generate-config.ts kratos -e .env.test',
    { cwd, stdio: 'ignore' }
  );
  execSync('docker compose restart kratos', { cwd, stdio: 'ignore' });

  globalThis.__DB_CONNECTION__ = await createTestConnection(envPath);
};
```

> **Note**
>
> - The `createTestConnection` function is a helper function to ensure the test database exists and is clean. It will use the same configuration as the application.
> - The `globalThis.__DB_CONNECTION__` will be used to destroy the connection in the global teardown.
> - The `globalThis.__TEARDOWN_MESSAGE__` will be used to log the teardown message.
> - The `execSync` function is used to run the scripts to generate the Ory test configuration and restart the Docker containers.

Continue with the global teardown configuration in the `apps/cat-fostering-api-e2e/src/support/global-teardown.ts` file:

```ts
import { execSync } from 'node:child_process';
import { DataSource } from 'typeorm';

const cwd = process.cwd();

export default async (): Promise<void> => {
  console.log(globalThis.__TEARDOWN_MESSAGE__);

  await (globalThis.__DB_CONNECTION__ as DataSource)?.destroy();

  execSync(
    'npx ts-node --project tools/tsconfig.json tools/ory/generate-config.ts keto -e .env',
    { cwd, stdio: 'ignore' }
  );
  execSync('docker compose restart keto', { cwd, stdio: 'ignore' });
  execSync(
    'npx ts-node --project tools/tsconfig.json tools/ory/generate-config.ts kratos -e .env',
    { cwd, stdio: 'ignore' }
  );
  execSync('docker compose restart kratos', { cwd, stdio: 'ignore' });
};
```

> **Note**
> The global teardown will destroy the test database connection and revert the Ory configurations to the initial state.

#### Create factories

The factories will wrap the CLI tools we used for the manual tests to create users and permissions. You can find them in the [`apps/cat-fostering-api-e2e/src/cat-fostering-api/helpers.ts`](https://github.com/getlarge/cat-fostering/blob/main/apps/cat-fostering-api-e2e/src/cat-fostering-api/helpers.ts).

#### Write the end-to-end tests

The end-to-end tests suite will use the factories to create users (including one admin) and cat profiles. To verify that the application creates relationships correctly, we will send HTTP requests to the API to test the following scenarios:

- An (authenticated) user can access their profile
- An (authenticated) user can create a cat profile
- A user can update a cat profile they own
- An admin can update any cat profile
- A user cannot update a cat profile they do not own

```ts
import axios from 'axios';

import {
  createCat,
  createOryAdminRelation,
  createOryUser,
  TestUser,
} from './helpers';

describe('E2E API tests', () => {
  let user1: TestUser;
  let user2: TestUser;

  beforeAll(async () => {
    user1 = await createOryUser({
      email: 'admin@test.it',
      password: 'p4s$worD!',
    });
    createOryAdminRelation({ userId: user1.id });

    user2 = await createOryUser({
      email: 'user@test.it',
      password: 'p4s$worD!',
    });
  });

  describe('GET /api', () => {
    it('should return a message', async () => {
      const res = await axios.get(`/api`);

      expect(res.status).toBe(200);
      expect(res.data).toEqual({ message: 'Hello API' });
    });
  });

  describe('GET /api/users/current-user', () => {
    it('should return the current user', async () => {
      const res = await axios.get(`/api/users/current-user`, {
        headers: {
          Authorization: `Bearer ${user1.sessionToken}`,
        },
      });

      expect(res.status).toBe(200);
      expect(res.data.email).toBe(user1.email);
    });

    it('should return 401 if no token is provided', async () => {
      const res = await axios.get(`/api/users/current-user`);

      expect(res.status).toBe(401);
    });

    it('should return 401 if an invalid token is provided', async () => {
      const res = await axios.get(`/api/users/current-user`, {
        headers: {
          Authorization: `Bearer ory_st_invalid`,
        },
      });

      expect(res.status).toBe(401);
    });
  });

  describe('POST /api/cat-profiles', () => {
    it('should create a cat profile', async () => {
      const res = await axios.post(
        `/api/cat-profiles`,
        {
          name: 'Godard',
          description: 'Black and white cat, knows how to open doors',
          age: 3,
        },
        {
          headers: {
            Authorization: `Bearer ${user1.sessionToken}`,
          },
        }
      );

      expect(res.status).toBe(201);
    });
  });

  describe('PATCH /api/cat-profiles/:id', () => {
    it('should update a cat profile when user is the owner', async () => {
      const cat = await createCat({
        name: 'Romeo',
        description: 'Grey cat, loves to cuddle',
        age: 2,
        sessionToken: user1.sessionToken,
      });

      const res = await axios.patch(
        `/api/cat-profiles/${cat.id}`,
        {
          age: 3,
        },
        {
          headers: {
            Authorization: `Bearer ${user1.sessionToken}`,
          },
        }
      );

      expect(res.status).toBe(200);
    });

    it('should update a cat profile when user an admin', async () => {
      const cat = await createCat({
        name: 'Juliet',
        description: 'White cat, loves to play',
        age: 1,
        sessionToken: user2.sessionToken,
      });

      const res = await axios.patch(
        `/api/cat-profiles/${cat.id}`,
        {
          age: 2,
        },
        {
          headers: {
            Authorization: `Bearer ${user1.sessionToken}`,
          },
        }
      );

      expect(res.status).toBe(200);
    });

    it(`should return 403 if the user is not an admin or the cat profile's owner`, async () => {
      const cat = await createCat({
        name: 'Crousti',
        description: 'Tabby brown, with a duke attitude',
        age: 8,
        sessionToken: user1.sessionToken,
      });

      const res = await axios.patch(
        `/api/cat-profiles/${cat.id}`,
        {
          age: 9,
        },
        {
          headers: {
            Authorization: `Bearer ${user2.sessionToken}`,
          },
        }
      );

      expect(res.status).toBe(403);
    });
  });
});
```

## Conclusion

If you have made it this far, congratulations! 🎉 You have successfully integrated Ory into your NestJS application. You have learned how to configure Ory Kratos and Ory Keto for multiple environments, create libraries to interact with the Ory services, and use the Ory APIs in your application.

The following step would be to deploy the application to a cloud provider and use the Ory Network instead of the local containers, which I highly recommend and will be the topic of the upcoming post.