Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding Node.js + Express.js webhooks app example #293

Merged
merged 17 commits into from
Sep 4, 2024
Merged
24 changes: 24 additions & 0 deletions examples/node-webhooks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.next

# testing
/coverage

# misc
.DS_Store
*.pem
.idea

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env
dist
3 changes: 3 additions & 0 deletions examples/node-webhooks/.sample.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
MIRO_CLIENT_ID="<your-client-id>"
MIRO_CLIENT_SECRET="<your-client-secret>"
MIRO_REDIRECT_URL="<your-ngrok-url>"
106 changes: 106 additions & 0 deletions examples/node-webhooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Node Webhooks

This app shows how to get webhook events on your Miro board using Node.js and Express.js.
horeaporutiu marked this conversation as resolved.
Show resolved Hide resolved

# 👨🏻‍💻 App Demo

https://github.com/user-attachments/assets/1448b658-9e6f-4652-8300-6cebbf081f7a

# 📒 Table of Contents

- [Included Features](#features)
- [Tools and Technologies](#tools)
- [Prerequisites](#prerequisites)
- [Associated Developer Tutorial](#tutorial)
- [Run the app locally](#run)
- [Folder Structure](#folder)
- [Contributing](#contributing)
- [License](#license)

# ⚙️ Included Features <a name="features"></a>

- [Miro Node Client Library with Express SDK](https://miroapp.github.io/api-clients/node/index.html)
- [miro.exchangeCodeForAccessToken()](https://miroapp.github.io/api-clients/node/classes/index.Miro.html#exchangeCodeForAccessToken)
- [miro.isAuthorized()](https://miroapp.github.io/api-clients/node/classes/index.Miro.html#isAuthorized)
- [miro.getAuthUrl()](https://miroapp.github.io/api-clients/node/classes/index.Miro.html#getAuthUrl)
- [miro.as()](https://miroapp.github.io/api-clients/node/classes/index.Miro.html#as)
- [api.getAllBoards()](https://miroapp.github.io/api-clients/node/classes/index.MiroApi.html#getAllBoards)

# 🛠️ Tools and Technologies <a name="tools"></a>

- [Node.js](https://nodejs.org/en)
- [Express.js](https://expressjs.com/)

# ✅ Prerequisites <a name="prerequisites"></a>

- You have a [Miro account](https://miro.com/signup/).
- You're [signed in to Miro](https://miro.com/login/).
- Your Miro account has a [Developer team](https://developers.miro.com/docs/create-a-developer-team).
- Your development environment includes [Node.js 14.13](https://nodejs.org/en/download) or a later version, and npm.
- Your development environment includes [ngrok](https://ngrok.com/) or something similar.

# 📖 Associated Developer Tutorial <a name="tutorial"></a>

> To view a more in depth developer tutorial
> of this app (including code explanations) see the [Getting started with webhooks tutorial](https://developers.miro.com/docs/getting-started-with-webhooks) on Miro's Developer documentation.
horeaporutiu marked this conversation as resolved.
Show resolved Hide resolved

# 🏃🏽‍♂️ Run the app locally <a name="run"></a>

1. Create a new Miro app on [developers.miro.com](https://developers.miro.com/). This will take you to the app settings page. There you
will find the `MIRO_CLIENT_ID` and `MIRO_CLIENT_SECRET` to be added to your `.env` file. Ensure that `boards:read` scope is checked,
and then go ahead and install the app on your developer team. You will get an **access token** which you will need later to
authenticate the creation of your webhook subscription.
horeaporutiu marked this conversation as resolved.
Show resolved Hide resolved

2. In a new terminal session, run:

```
ngrok http 3000
```

This will output something like this:

```
Forwarding https:<your-ngrok-url> -> http://localhost:3000
```

The `https:<your-ngrok-url>` is your `MIRO_REDIRECT_URL` to be used in the `.env` file and then later when calling the API to create a webhook subscription.

3. Rename the `.sample.env` file to `.env` and then add in your `MIRO_CLIENT_ID` and `MIRO_CLIENT_SECRET` from your [developers.miro.com](https://developers.miro.com/) app settings page. Use the `forwarding URL` from the previous step for the `MIRO_REDIRECT_URL` in the .env file. Save the file as `.env` with your new variables.

4. Run `npm i` to install dependencies.

5. Run `npm start` to start the dev server.

6. Go to your developer team, and open the board you want to receive webhook events for.

7. In a separate browser tab, open up the API Exporer for the [Create Webhook Subscription endpoint](https://developers.miro.com/reference/create-board-subscription).

8. Provide the following information in the API Explorer:

> **Access Token**: Once you get the access token after installing your app on a developer team (from step 4 above), you can add the access token to the Authorization section of the API reference page.
>
> **boardId:** Get the board ID of the board you want to receive notifications for. This board should be in the same team where you installed the app. You can find board ID in the URL when you go to your board: https://miro.com/app/board/<boardId>.
>
> **callbackUrl:** This is the URL where you will receive events. It should be the same as `MIRO_REDIRECT_URL` in `.env`.

9. Select Try It! to run the API request right from the browser. If you get a 201 response, you are ready to receive events!

10. Next, go to to the same board which you referenced in the request above, and create a sticky. You should now receive a webhook event! To learn more about the events you can expect to receive,
horeaporutiu marked this conversation as resolved.
Show resolved Hide resolved

# 🗂️ Folder structure <a name="folder"></a>

```
.
├── src
│ └── app.js - main logic to receive webhooks and start the server
│ └── miroMiddleware.css <-- Middleware file to setup OAuth
├── .sample.env <-- File with sample env variables. Need to rename to .env and then add in your variables.
```

# 🫱🏻‍🫲🏽 Contributing <a name="contributing"></a>

If you want to contribute to this example, or any other Miro Open Source project, please review [Miro's contributing guide](https://github.com/miroapp/app-examples/blob/main/CONTRIBUTING.md).

# 🪪 License <a name="license"></a>

[MIT License](https://github.com/miroapp/app-examples/blob/main/LICENSE).
5 changes: 5 additions & 0 deletions examples/node-webhooks/app-manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# See https://developers.miro.com/docs/app-manifest on how to use this
appName: Node Webhooks
sdkUri: "http://localhost:3000"
scopes:
- boards:read
7 changes: 7 additions & 0 deletions examples/node-webhooks/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./node_modules/@mirohq"]
},
"include": ["src"],
"exclude": ["node_modules"]
}
21 changes: 21 additions & 0 deletions examples/node-webhooks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "node-webhooks",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"start": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"express": "^4.18.1",
"@mirohq/miro-api": "^2.0.0",
"cookie-parser": "^1.4.6",
"dotenv": "^16.0.3"
},
"devDependencies": {
"vite": "3.0.3",
"vite-plugin-node": "^2.0.0"
},
"type": "module"
}
64 changes: 64 additions & 0 deletions examples/node-webhooks/src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { config } from "dotenv";

import express from "express";
import cookieParser from "cookie-parser";

import miroMiddleware from "./miroMiddleware";

config();

const app = express();

app.use(cookieParser("<RANDOMLY-GENERATED-SECRET-STRING>"));
app.use(miroMiddleware);
app.use(express.json());

app.get("/auth/miro/callback", async (req, res) => {
if (typeof req.query.code !== "string") {
res.status(400);
res.send("Missing code query parameter!");
return;
}
await req.miro.exchangeCodeForAccessToken(req.cookies.id, req.query.code);
res.redirect("/");
});

app.get("/", async (req, res) => {
if (!(await req.miro.isAuthorized(req.cookies.id))) {
res.redirect(req.miro.getAuthUrl());
return;
}

const api = req.miro.as(req.cookies.id);

res.header("content-type", "text/html");
res.write("These are the boards that you have access to: <br/>");

const allBoards = api.getAllBoards();
for await (const board of allBoards) {
res.write(`<a href="${board.viewLink}">${board.name}</a><br/>`);
}
res.send();
});

app.post("/", async (req, res) => {
if (req.body.event) {
console.log("Webhook event:");
console.log(req.body.event);
}

if (req.body.challenge) {
console.log("Challenge:", req.body.challenge);
res.send(req.body);
return;
}
res.send("OK");
});

if (import.meta.env.PROD) {
app.listen(3000, () =>
console.log("Started server on http://127.0.0.1:3000"),
);
}

export const viteNodeApp = app;
22 changes: 22 additions & 0 deletions examples/node-webhooks/src/miroMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Miro } from "@mirohq/miro-api";

export default function middlware(req, res, next) {
req.miro = new Miro({
storage: {
// eslint-disable-next-line no-unused-vars
async get(_userId) {
try {
return JSON.parse(req.cookies.state);
} catch (err) {
return undefined;
}
},

set(userId, state) {
res.cookie("id", userId, { path: "/", secure: true });
res.cookie("state", JSON.stringify(state), { path: "/", secure: true });
},
},
});
next();
}
21 changes: 21 additions & 0 deletions examples/node-webhooks/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"typeRoots": ["./node_modules/@types", "./node_modules/@mirohq"]
},
"include": ["./src", "node_modules"]
}
18 changes: 18 additions & 0 deletions examples/node-webhooks/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig } from "vite";
import { VitePluginNode } from "vite-plugin-node";

export default defineConfig({
server: {
port: 3000,
},
plugins: [
...VitePluginNode({
adapter: "express",

appPath: "./src/app.js",

exportName: "viteNodeApp",
}),
],
optimizeDeps: {},
});
Loading