This tutorial gives you an overview on how to create a TypeScript-based Cloudflare Worker which allows you to control file access based on a simple username and password authentication. To achieve this, we will use a [D1 database](https://developers.cloudflare.com/d1/) for user management and an [R2 bucket](https://developers.cloudflare.com/r2/) for file storage.

The following sections will guide you through the process of creating a Worker using the Cloudflare CLI, creating and setting up a D1 database and R2 bucket, and then implementing the functionality to securely upload and fetch files from the created R2 bucket.

## Prerequisites

1. Sign up for a [Cloudflare account](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [`Node.js`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

## 1. Create a new Worker application

To get started developing your Worker you will use the [`create-cloudflare` CLI](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare). To do this, open a terminal window and run the following command:

* npm

  ```sh
  npm create cloudflare@latest -- custom-access-control
  ```

* pnpm

  ```sh
  pnpm create cloudflare@latest custom-access-control
  ```

* yarn

  ```sh
  yarn create cloudflare custom-access-control
  ```

For setup, select the following options:

* For *What would you like to start with?*, choose `Hello World Starter`.
* For *Which template would you like to use?*, choose `Worker only`.
* For *Which language do you want to use?*, choose `TypeScript`.
* For *Do you want to use git for version control?*, choose `Yes`.
* For *Do you want to deploy your application?*, choose `No` (we will be making some changes before deploying).

Then, move into your newly created Worker:

```sh
cd custom-access-control
```

## 2. Create a new D1 database and binding

Now that you have created your Worker, next you will need to create a D1 database. This can be done through the Cloudflare Portal or the Wrangler CLI. For this tutorial, we will use the Wrangler CLI for simplicity.

To create a D1 database, just run the following command. If you get asked to install wrangler, just confirm by pressing `y` and then press `Enter`.

```sh
npx wrangler d1 create <YOUR_DATABASE_NAME>
```

Replace `<YOUR_DATABASE_NAME>` with the name you want to use for your database. Keep in mind that this name can't be changed later on.

After the database is successfully created, you will see the data for the binding displayed as an output. The binding declaration will start with `[[d1_databases]]` and contain the binding name, database name and ID. To use the database in your worker, you will need to add the binding to your Wrangler file, by copying the declaration and pasting it into the wrangler file, as shown in the example below.

* wrangler.jsonc

  ```jsonc
  {
    "d1_databases": [
      {
        "binding": "DB",
        "database_name": "<YOUR_DATABASE_NAME>",
        "database_id": "<YOUR_DATABASE_ID>"
      }
    ]
  }
  ```

* wrangler.toml

  ```toml
  [[d1_databases]]
  binding = "DB"
  database_name = "<YOUR_DATABASE_NAME>"
  database_id = "<YOUR_DATABASE_ID>"
  ```

## 3. Create R2 bucket and binding

Now that the D1 database is created, you also need to create an R2 bucket which will be used to store the uploaded files. This step can also be done through the Cloudflare Portal, but as before, we will use the Wrangler CLI for this tutorial. To create an R2 bucket, run the following command:

```sh
npx wrangler r2 bucket create <YOUR_BUCKET_NAME>
```

This works similar to the D1 database creation, where you will need to replace `<YOUR_BUCKET_NAME>` with the name you want to use for your bucket. To do this, go to the Wrangler file again and then add the following lines:

* wrangler.jsonc

  ```jsonc
  {
    "r2_buckets": [
      {
        "binding": "BUCKET",
        "bucket_name": "<YOUR_BUCKET_NAME>"
      }
    ]
  }
  ```

* wrangler.toml

  ```toml
  [[r2_buckets]]
  binding = "BUCKET"
  bucket_name = "<YOUR_BUCKET_NAME>"
  ```

Now that you have prepared the Wrangler configuration, you should update the `worker-configuration.d.ts` file to include the new bindings. This file will then provide TypeScript with the correct type definitions for the bindings, which allows for type checking and code completion in your editor. You could either update it manually or run the following command in the directory of your project to update it automatically based on the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) (recommended).

```sh
npm run cf-typegen
```

## 4. Database preparation

Before you can start developing the Worker, you need to prepare the D1 database.

For this you need to

1. Create a table in the database which will then be used to store the user data
2. Create a unique index on the username column, which will speed up database queries and ensure that the username is unique
3. Insert a test user into the table, so you can test your code later on

As this operation only needs to be done once, this will be done through the Wrangler CLI and not in the Worker's code. Copy the commands listed below, replace the placeholders and then run them in order to prepare the database. For this tutorial you can replace the `<YOUR_USERNAME>` and `<YOUR_HASHED_PASSWORD>` placeholders with `admin` and `5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8` respecively. And `<YOUR_DATABASE_NAME>` should be replaced with the name you used to create the database.

```sh
npx wrangler d1 execute <YOUR_DATABASE_NAME> --command "CREATE TABLE user (id INTEGER PRIMARY KEY NOT NULL, username STRING NOT NULL, password STRING NOT NULL)" --remote
npx wrangler d1 execute <YOUR_DATABASE_NAME> --command "CREATE UNIQUE INDEX user_username ON user (username)"  --remote
npx wrangler d1 execute <YOUR_DATABASE_NAME> --command "INSERT INTO user (username, password) VALUES ('<YOUR_USERNAME>', '<YOUR_HASHED_PASSWORD>')"  --remote
```

## 5. Implement authentication in the Worker

Now that the database and bucket are all set up, you can start to develop the Worker application. The first thing you will need to do is to implement the authentication for the requests.

This tutorial will use a simple username and password authentication, where the username and password (hashed) are stored in the D1 database. The requests will contain the username and password as a base64 encoded string, which is also called Basic Authentication. Depending on the request method, this string will be retrieved from the `Authorization` header for POST requests or the `Authorization` search parameter for GET requests.

To handle the authentication, you will need to replace the current code within `index.ts` file with the following code:

```ts
export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext,
  ): Promise<Response> {
    try {
      const url = new URL(request.url);
      let authBase64;
      if (request.method === "POST") {
        authBase64 = request.headers.get("Authorization");
      } else if (request.method === "GET") {
        authBase64 = url.searchParams.get("Authorization");
      } else {
        return new Response("Method Not Allowed!", { status: 405 });
      }
      if (!authBase64 || authBase64.substring(0, 6) !== "Basic ") {
        return new Response("Unauthorized!", { status: 401 });
      }


      const authString = atob(authBase64.substring(6));
      const [username, password] = authString.split(":");
      if (!username || !password) {
        return new Response("Unauthorized!", { status: 401 });
      }


      // TODO: Check if the username and password are correct
    } catch (error) {
      console.error("An error occurred!", error);
      return new Response("Internal Server Error!", { status: 500 });
    }
  },
};
```

The code above currently extracts the username and password from the request, but does not yet check if the username and password are correct.

To check the username and password, you will need to hash the password and then query the D1 database table `user` with the given username and hashed password. If the username and password are correct, you will retrieve a record from D1. If the username or password is incorrect, undefined will be returned and a `401 Unauthorized` response will be sent. To add this functionality, you will need to add the following code to the `fetch` function by replacing the TODO comment from the last code snippet:

```ts
const passwordHashBuffer = await crypto.subtle.digest(
  { name: "SHA-256" },
  new TextEncoder().encode(password),
);
const passwordHashArray = Array.from(new Uint8Array(passwordHashBuffer));
const passwordHashString = passwordHashArray
  .map((b) => b.toString(16).padStart(2, "0"))
  .join("");


const user = await env.DB.prepare(
  "SELECT id FROM user WHERE username = ? AND password = ? LIMIT 1",
)
  .bind(username, passwordHashString)
  .first<{ id: number }>();
if (!user) {
  return new Response("Unauthorized!", { status: 401 });
}


// TODO: Implement upload functionality
```

This code will now ensure that every request is authenticated before it can be processed further.

## 6. Upload a file through the Worker

Now that the authentication is set up, you can start to implement the functionality for uploading a file through the Worker. To do this, you will need to add a new code path that handles HTTP `POST` requests. Then within it, you will need to get the data from the request, which is sent within the body of the request, by using the `request.blob()` function. After that, you can upload the data to the R2 bucket by using the `env.BUCKET.put` function. And finally, you will return a `200 OK` response to the client.

To implement this functionality, you will need to replace the TODO comment from the last code snippet with the following code:

```ts
if (request.method === "POST") {
  // Upload the file to the R2 bucket with the user id followed by a slash as the prefix and then the path of the URL
  await env.BUCKET.put(`${user.id}/${url.pathname}`, request.body);
  return new Response("OK", { status: 200 });
}
// TODO: Implement GET request handling
```

This code will now allow you to upload a file through the Worker, which will be stored in your R2 bucket.

## 7. Fetch from the R2 bucket

To round up the Worker application, you will need to implement the functionality to fetch files from the R2 bucket. This can be done by adding a new code path that handles `GET` requests. Within this code path, you will need to extract the URL pathname and then retrieve the asset from the R2 bucket by using the `env.BUCKET.get` function.

To finalize the code, just replace the TODO comment for handling GET requests from the last code snippet with the following code:

```ts
if (request.method === "GET") {
  const file = await env.BUCKET.get(`${user.id}/${url.pathname.slice(1)}`);
  if (!file) {
    return new Response("Not Found!", { status: 404 });
  }
  const headers = new Headers();
  file.writeHttpMetadata(headers);
  return new Response(file.body, { headers });
}
return new Response("Method Not Allowed!", { status: 405 });
```

This code now allows you to fetch and return data from the R2 bucket when a `GET` request is made to the Worker application.

## 8. Deploy your Worker

After completing the code for this Cloudflare Worker tutorial, you will need to deploy it to Cloudflare. To do this open the terminal in the directory created for your application, and then run:

```sh
npx wrangler deploy
```

You might get asked to authenticate (if not logged in already) and select an account. After that, the Worker will be deployed to Cloudflare. When the deployment finished successfully, you will see a success message with the URL where your Worker is now accessible.

## 9. Test your Worker (optional)

To finish this tutorial, you should test your Worker application by sending a `POST` request to upload a file and after that a `GET` request to fetch the file. This can be done by using a tool like `curl` or `Postman`, but for simplicity, this will describe the usage of `curl`.

Copy the following command which can be used to upload a simple JSON file with the content `{"Hello": "Worker!"}`. Replace `<YOUR_API_SECRET>` with the base64 encoded username and password combination and then run the command. For this example you can use `YWRtaW46cGFzc3dvcmQ=`, which can be decoded to `admin` and `test`, for the api secret placeholder.

```sh
curl --location '<YOUR_WORKER_URL>/myFile.json' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic <YOUR_API_SECRET>' \
--data '{
    "Hello": "Worker!"
}'
```

Then run the next command, or simply open the URL in your browser, to fetch the file you just uploaded:

```sh
curl --location '<YOUR_WORKER_URL>/myFile.json?Authorization=Basic%20YWRtaW46cGFzc3dvcmQ%3D'
```

## Next steps

If you want to learn more about Cloudflare Workers, R2, or D1 you can check out the following documentation:

* [Cloudflare Workers](https://developers.cloudflare.com/workers/)
* [Cloudflare R2](https://developers.cloudflare.com/r2/)
* [Cloudflare D1](https://developers.cloudflare.com/d1/)
