-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
27 changed files
with
4,998 additions
and
1,517 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
#!/bin/bash | ||
|
||
node ./index.mjs $@ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
72
examples/express/with-m2m/assistant-client/eventFunctions.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
36
examples/express/with-m2m/assistant-client/getAccessToken.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.