Skip to content

Commit

Permalink
docs: finish M2M example app
Browse files Browse the repository at this point in the history
  • Loading branch information
porcellus committed Oct 30, 2024
1 parent a60f7e4 commit 8083d26
Show file tree
Hide file tree
Showing 27 changed files with 4,998 additions and 1,517 deletions.
67 changes: 67 additions & 0 deletions examples/express/with-m2m/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png)

# SuperTokens M2M Demo app

In this example we showcase M2M (Machine to Machine) communication between an assistant (cli) and two services (calendar-service and note-service), both the cli and the services rely on a single auth-provider service to obtain/validate tokens. We showcase:

- How to set up an OAuth2Provider using SuperTokens
- How to obtain a token using the client-credentials flow
- How to validate M2M tokens using a generic JWT library

## Project setup

You can run set up the example by running the following command:

```bash
git clone https://github.com/supertokens/supertokens-node
cd supertokens-node/examples/express/with-m2m
npm install
```

## Run the demo app

```bash
npm start
```

and then in a new terminal run:

```bash
# Please note that when running through npm, you need to add `--` before the argument list passed to the assistant cli
npm run assistant -- --help
```

OR

```bash
./assistant --help
```

## Project structure (notable files/folders)

```
├── assistant-client
├── eventFunctions.mjs The functions to interact with the calendar-service
├── noteFunctions.mjs The functions to interact with the note-service
├── getAccessToken.mjs The function to get the access token from the auth-service
├── index.mjs The main function to run the assistant
├── auth-provider-service
├── config.ts The configuration for SuperTokens
├── setupClient.ts The function to set up the OAuth2 client used by the assistant-client
├── index.ts The main function to run the auth-provider-service
├── calendar-service
├── index.ts Sets up the APIs for calendar-service (w/ token validation and a simple in-memory DB)
├── note-service
├── index.ts Sets up the APIs for note-service (w/ token validation and a simple in-memory DB)
```

## Author

Created with :heart: by the folks at supertokens.com.

## License

This project is licensed under the Apache 2.0 license.
3 changes: 3 additions & 0 deletions examples/express/with-m2m/assistant
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

node ./index.mjs $@
161 changes: 161 additions & 0 deletions examples/express/with-m2m/assistant-client/cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Command } from "commander";
import { getAccessToken } from "./getAccessToken.mjs";
import { addEvent, getEvents, deleteEvent } from "./eventFunctions.mjs";
import { addNote, deleteNote, getNotes, updateNote } from "./noteFunctions.mjs";

/* This file is just setting up the CLI commands, and the functions to print the output */
/* You can ignore this part, and focus on the functions above */
function printEvent(event) {
console.log();
console.log(`${event.title} (${event.id})`);
console.log(` Start: ${new Date(event.start).toLocaleString()}`);
console.log(` End: ${new Date(event.end).toLocaleString()}`);
console.log(` Description: ${event.description}`);
console.log();
}
function printNote(note) {
console.log();
console.log(`${note.title} (${note.id})`);
console.log(`${note.description}`);
console.log();
}
export const program = new Command();
program.name("assistant-client").description("A client for the calendar and note services").version("1.0.0");
const calendarCommand = new Command("calendar").description("Interact with the calendar service");
calendarCommand.addCommand(
new Command("add")
.description("Add an event")
.option("-t, --title <title>", "The title of the event")
.option("-s, --start <start>", "The start time of the event")
.option("-e, --end <end>", "The end time of the event")
.argument("<description...>", "The description of the event")
.action(async (description, options) => {
if (description.length === 0) {
console.error("Please provide a description for the event");
return;
}

const accessToken = await getAccessToken("calendar-service", "calendar.write");

try {
const event = await addEvent(accessToken, {
title: options.title ?? "Untitled Event",
start: options.start ? Date.parse(options.start) : Date.now(),
end: options.end ? Date.parse(options.end) : Date.now() + 1000 * 60 * 60,
description: description.join(" "),
});
console.log("Event added successfully");
printEvent(event);
} catch (error) {
console.error("Failed to add event", error);
}
})
);
calendarCommand.addCommand(
new Command("list").description("List events").action(async () => {
const accessToken = await getAccessToken("calendar-service", "calendar.read");

try {
const events = await getEvents(accessToken);
console.log("Events:");
for (const event of events) {
console.log("--------------------------------");
printEvent(event);
}
console.log("--------------------------------");
} catch (error) {
console.error("Failed to list events", error);
}
})
);
calendarCommand.addCommand(
new Command("delete")
.description("Delete an event")
.argument("<id>", "The ID of the event to delete")
.action(async (id) => {
const accessToken = await getAccessToken("calendar-service", "calendar.write");

try {
const { deleted } = await deleteEvent(accessToken, parseInt(id));
if (deleted) {
console.log("Event deleted successfully");
} else {
console.log("Event was already deleted");
}
} catch (error) {
console.error("Failed to delete event", error);
}
})
);
program.addCommand(calendarCommand);
const noteCommand = new Command("note").description("Interact with the note service");
noteCommand.addCommand(
new Command("add")
.description("Add a note")
.option("-t, --title <title>", "The title of the note")
.argument("<description...>", "The description of the note")
.action(async (description, options) => {
const accessToken = await getAccessToken("note-service", "note.write");
try {
const note = await addNote(accessToken, {
title: options.title ?? "Untitled Note",
description: description.join(" "),
});
console.log("Note added successfully");
printNote(note);
} catch (error) {
console.error("Failed to add note", error);
}
})
);
noteCommand.addCommand(
new Command("delete")
.description("Delete a note")
.argument("<id>", "The ID of the note to delete")
.action(async (id) => {
const accessToken = await getAccessToken("note-service", "note.write");
try {
await deleteNote(accessToken, parseInt(id));
console.log("Note deleted successfully");
} catch (error) {
console.error("Failed to delete note", error);
}
})
);
noteCommand.addCommand(
new Command("list").description("List notes").action(async () => {
const accessToken = await getAccessToken("note-service", "note.read");
try {
const notes = await getNotes(accessToken);
console.log("Notes:");
for (const note of notes) {
console.log("--------------------------------");
printNote(note);
}
console.log("--------------------------------");
} catch (error) {
console.error("Failed to list notes", error);
}
})
);
noteCommand.addCommand(
new Command("update")
.description("Update a note")
.argument("<id>", "The ID of the note to update")
.option("-t, --title <title>", "The title of the note")
.argument("<description...>", "The description of the note")
.action(async (id, description, options) => {
const accessToken = await getAccessToken("note-service", "note.write");
try {
const updatedNote = await updateNote(accessToken, parseInt(id), {
title: options.title,
description: description.join(" "),
});
console.log("Note updated successfully");
printNote(updatedNote);
} catch (error) {
console.error("Failed to update note", error);
}
})
);
program.addCommand(noteCommand);
3 changes: 3 additions & 0 deletions examples/express/with-m2m/assistant-client/constants.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const authProviderBaseUrl = "http://localhost:3001";
export const calendarServiceBaseUrl = "http://localhost:3011";
export const noteServiceBaseUrl = "http://localhost:3012";
72 changes: 72 additions & 0 deletions examples/express/with-m2m/assistant-client/eventFunctions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { calendarServiceBaseUrl } from "./constants.mjs";

/**
* @typedef {Object} AssistantEvent
* @property {number} id - The unique identifier for the event
* @property {string} title - The title of the event
* @property {string} description - The description of the event
* @property {number} start - The start timestamp of the event
* @property {number} end - The end timestamp of the event
*/
/**
* Adds a new event to the calendar service
* @param {string} accessToken - The access token for authorization
* @param {Omit<AssistantEvent, "id">} event - The event details to add
* @returns {Promise<AssistantEvent>} The created event
* @throws {Error} If the request fails
*/

export async function addEvent(accessToken, event) {
const resp = await fetch(`${calendarServiceBaseUrl}/event`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(event),
});

if (!resp.ok) {
throw new Error(`Failed to add event: ${await resp.text()}`);
}
return resp.json();
}
/**
* Deletes an event from the calendar service
* @param {string} accessToken - The access token for authorization
* @param {number} eventId - The ID of the event to delete
* @returns {Promise<{ deleted: boolean }>} Returns true if the event was deleted successfully, false if the event was already deleted/not found
* @throws {Error} If the request fails
*/

export async function deleteEvent(accessToken, eventId) {
const resp = await fetch(`${calendarServiceBaseUrl}/event/${eventId}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!resp.ok) {
throw new Error(`Failed to delete event: ${await resp.text()}`);
}
return resp.json();
}
/**
* Retrieves all events from the calendar service
* @param {string} accessToken - The access token for authorization
* @returns {Promise<AssistantEvent[]>} The list of events
* @throws {Error} If the request fails
*/

export async function getEvents(accessToken) {
const resp = await fetch(`${calendarServiceBaseUrl}/event`, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!resp.ok) {
throw new Error(`Failed to get events: ${await resp.text()}`);
}
return resp.json();
}
36 changes: 36 additions & 0 deletions examples/express/with-m2m/assistant-client/getAccessToken.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { readFile } from "fs/promises";
import { authProviderBaseUrl } from "./constants.mjs";

export async function getAccessToken(audience, scope) {
let clientId, clientSecret;
try {
const file = await readFile("./clients.json", "utf-8");
const clients = JSON.parse(file);
({ clientId, clientSecret } = clients.assistant);
} catch (error) {
throw new Error("Failed to read clients.json, please run npm start first.");
}

const resp = await fetch(`${authProviderBaseUrl}/auth/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
},
body: JSON.stringify({
client_id: clientId,
grant_type: "client_credentials",
audience: audience,
scope: scope,
}),
});

if (!resp.ok) {
throw new Error(
`Failed to get access token: ${await resp.text()}. Please make sure that the auth-provider-service is running and that the clients.json file is correct. You can try deleting the clients.json file and re-runing npm start.`
);
}

const tokens = await resp.json();
return tokens.access_token;
}
Loading

0 comments on commit 8083d26

Please sign in to comment.