Skip to content

Commit

Permalink
Add example of using board.getIdToken
Browse files Browse the repository at this point in the history
  • Loading branch information
kirillsud committed Apr 3, 2024
1 parent 9914531 commit c2e4828
Show file tree
Hide file tree
Showing 23 changed files with 1,095 additions and 39 deletions.
3 changes: 3 additions & 0 deletions examples/jwt-auth/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
MIRO_CLIENT_ID=""
MIRO_CLIENT_SECRET=""
NEXT_PUBLIC_GIPHY_API_KEY="sXpGFDGZs0Dv1mmNFvYaGUvYwKX0PWIh"
25 changes: 25 additions & 0 deletions examples/jwt-auth/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 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
/store.json
113 changes: 113 additions & 0 deletions examples/jwt-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# JWT Authentication with Miro

This app shows how to add Miro user's signature to API requests using JWT token.
Whenever a server endpoint needs to verify the user's identity, it can use the user ID JWT token provided by Miro.

# 👨🏻‍💻 App Demo

...

# 📒 Table of Contents

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

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

- [Miro Web SDK](https://developers.miro.com/docs/web-sdk-reference)
- [on(icon:click)](https://developers.miro.com/docs/ui_boardui#iconclick-event)
- [openPanel(options)](https://developers.miro.com/docs/ui_boardui#openpanel)
- [board.getIdToken()](https://developers.miro.com/docs/websdk-reference-board#getidtoken)
- [board.createImage(...)](https://developers.miro.com/docs/websdk-reference-board#createimage)
- [viewport.get()](https://developers.miro.com/docs/websdk-reference-viewport#get)

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

- [React](https://react.dev/)
- [TypeScript](https://www.typescriptlang.org/)
- [Next.js](https://nextjs.org/)
- [Stripe CLI](https://stripe.com/docs/stripe-cli)
- [Giphy SDK for Web](https://developers.giphy.com/docs/sdk/#web)

# ✅ 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 18.17](https://nodejs.org/en/download) or a later version.
- All examples use `npm` as a package manager and `npx` as a package runner.

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

1. Rename the ['.env.example' file](.env.example) to `.env`. You will need to create a Miro app and then add in the client ID and client secret.
2. Run `npm install` to install dependencies.
3. Run `npm start` to start developing. \
Your URL should be similar to this example:
```
http://localhost:3000
```
4. Open the [app manifest editor](https://developers.miro.com/docs/manually-create-an-app#step-2-configure-your-app-in-miro) by clicking **Edit in Manifest**. \
In the app manifest editor, configure the app as follows:

- [`appName`](): change it to `JWT demo` or whatever you want to name your app.
- [`sdkUri`](https://developers.miro.com/docs/app-manifest#sdkuri): assign `http://localhost:3000` as a value for this property.
- [`scopes`](https://developers.miro.com/docs/app-manifest#scopes): add the permission scopes that users need to grant the app when they install it.
To enable the app to write to the board, add the following permissions:
- `boards:read`

5. Go back to your app home page, and under the `Permissions` section, you will see a blue button that says `Install app and get OAuth token`. Click that button. Then click on `Add` as shown in the video below.

> ⚠️ We recommend to install your app on a [developer team](https://developers.miro.com/docs/create-a-developer-team) while you are developing or testing apps.⚠️
https://github.com/miroapp/app-examples/assets/10428517/1e6862de-8617-46ef-b265-97ff1cbfe8bf

6. Go to your developer team, and open your boards.
7. Click on the plus icon from the bottom section of your left sidebar. If you hover over it, it will say `More apps`.
8. Search for your app `JWT demo` or whatever you chose to name it. Click on your app to use it, as shown in the video below.

https://github.com/horeaporutiu/app-examples-template/assets/10428517/b23d9c4c-e785-43f9-a72e-fa5d82c7b019

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

```
./src
│ └── components
│ │── SdkInfo.tsx <-- React component to initialize the app in a Miro board.
│ │── Grid.tsx <-- React component to render a grid with gifs.
│ │── RecentGifs.tsx <-- React component to render recent gifs based on data from the app's server.
│ └── Search.tsx <-- React component to render a gif search block.
│ └── app
│ │── layout.tsx <-- Root layout for the app.
│ │── loading.tsx <-- Loading placeholder for the app.
│ │── page.tsx <-- Index empty page to initialize the app.
│ └── api
│ └── recent
│ └── route.ts <-- Route controller to get and update recent gifs for a user.
│ └── panel
│ └── page.ts <-- a page to render the app's panel.
│ └── public
│ └── favicon.ico <-- Icon for the web app.
│ └── assets
│ └── style.css <-- CSS styles for the app.
│ └── hooks
│ └── useRecent.ts <-- Custom hook to read and update recent gifs.
│ └── utils
│ │── api.ts <-- API utility to make requests to the app's server with Miro's user JWT header.
│ │── miro.ts <-- Miro utility to interact with the Miro Web SDK.
│ │── user.ts <-- User utility to get the user's ID from the JWT token.
│ └── storage.ts <-- Implementation of storage logic. Will create a file `store.json` with userID and recent gifs.
└── initMiro.ts <-- This is where the Node Client is initialized.
```

# 🫱🏻‍🫲🏽 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).
3 changes: 3 additions & 0 deletions examples/jwt-auth/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// https://vitejs.dev/guide/features.html#typescript-compiler-options
/// <reference types="vite/client" />
/// <reference types="@mirohq/websdk-types" />
5 changes: 5 additions & 0 deletions examples/jwt-auth/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
30 changes: 30 additions & 0 deletions examples/jwt-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "jwt-auth",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"build": "next build",
"start": "next dev",
"lint": "next lint"
},
"dependencies": {
"@giphy/react-components": "^9.3.0",
"@mirohq/miro-api": "^2.0.0",
"cookie": "^0.5.0",
"dotenv": "^16.0.3",
"jsonwebtoken": "9.0.2",
"mirotone": "5",
"next": "14.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-resize-observer": "1.1.1"
},
"devDependencies": {
"@mirohq/websdk-types": "latest",
"@types/cookie": "^0.5.1",
"@types/jsonwebtoken": "9.0.6",
"@types/node": "^18.8.2",
"@types/react": "^18.0.24",
"typescript": "4.9.5"
}
}
26 changes: 26 additions & 0 deletions examples/jwt-auth/src/app/api/recent/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getUserIdFromRequest } from "../../../utils/user";
import { storage } from "../../../utils/storage";
import { NextResponse } from "next/server";

export async function GET() {
const userId = getUserIdFromRequest();
const record = await storage.get(userId);

return NextResponse.json({ recent: record?.recent ?? [] });
}

export async function PUT(request: Request) {
const userId = getUserIdFromRequest();
const gifId = await request.json();

const record = (await storage.get(userId)) ?? { recent: [] };

const recent = record.recent.filter((id) => id !== gifId);
recent.unshift(gifId);

record.recent = recent.slice(0, 4);

await storage.set(userId, record);

return NextResponse.json({ recent: record.recent });
}
20 changes: 20 additions & 0 deletions examples/jwt-auth/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { PropsWithChildren } from "react";
import Script from "next/script";
import "../assets/style.css";

import { MiroSDKInit } from "../components/SDKInit";

export default function RootLayout({ children }: PropsWithChildren) {
return (
<html>
<body>
<Script
src="https://miro.com/app/static/sdk/v2/miro.js"
strategy="beforeInteractive"
/>
<MiroSDKInit />
<div id="root">{children}</div>
</body>
</html>
);
}
5 changes: 5 additions & 0 deletions examples/jwt-auth/src/app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Loading = () => {
return <div>...Loading</div>;
};

export default Loading;
3 changes: 3 additions & 0 deletions examples/jwt-auth/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async function Page() {
return null;
}
5 changes: 5 additions & 0 deletions examples/jwt-auth/src/app/panel/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { RecentGifs } from "../../components/RecentGifs";

export default async function Page() {
return <RecentGifs />;
}
18 changes: 18 additions & 0 deletions examples/jwt-auth/src/assets/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@import "mirotone/dist/styles.css";

body {
display: flex;
}

#root {
width: 100%;
overflow: auto;
margin: var(--space-medium);
margin-top: 0;
}

.giphy-search-bar {
margin-bottom: var(--space-xsmall);
box-shadow: inset 0 0 0 1px var(--indigo400);
padding-left: 1px;
}
32 changes: 32 additions & 0 deletions examples/jwt-auth/src/components/Grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Grid as GiphyGrid } from "@giphy/react-components";
import ResizeObserver from "react-resize-observer";
import { ComponentProps, SyntheticEvent, useCallback, useState } from "react";
import { IGif } from "@giphy/js-types";

export type GridProps = {
onSelect: (gif: IGif) => void;
} & Pick<ComponentProps<typeof GiphyGrid>, "fetchGifs">;

export function Grid({ fetchGifs, onSelect }: GridProps) {
const [width, setWidth] = useState(320);

const handleClick = useCallback(
(gif: IGif, e: SyntheticEvent) => {
e.preventDefault();
onSelect?.(gif);
},
[onSelect],
);

return (
<>
<GiphyGrid
fetchGifs={fetchGifs}
columns={2}
width={width}
onGifClick={handleClick}
/>
<ResizeObserver onResize={({ width }) => setWidth(width)} />
</>
);
}
64 changes: 64 additions & 0 deletions examples/jwt-auth/src/components/RecentGifs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import { IGif } from "@giphy/js-types";
import { GifsResult } from "@giphy/js-fetch-api";
import { useCallback } from "react";
import { createImage } from "../utils/miro";
import { useRecent } from "../hooks/useRecent";
import { Search } from "./Search";
import { Grid } from "./Grid";

function createGifsResult(recent: IGif[]): Promise<GifsResult> {
return Promise.resolve({
data: recent,
pagination: {
total_count: recent.length,
count: recent.length,
offset: 0,
},
meta: {
status: 200,
msg: "OK",
response_id: "1",
},
});
}

export function RecentGifs() {
const [recent, addRecent] = useRecent();

const handleSelectedGif = useCallback(
async (gif: IGif) => {
await createImage(gif);
addRecent(gif);
},
[addRecent],
);

if (!recent) return <p>Loading...</p>;

const fetchGifs = () => createGifsResult(recent);
const recentKey = recent.map((gif) => gif.id).join(",");

return (
<div className="grid">
<div className="cs1 ce12">
<h4>Recent gifs:</h4>
{recent.length === 0 ? (
<p>No recent GIFs. Try search below to add some!</p>
) : (
<Grid
key={recentKey}
fetchGifs={fetchGifs}
onSelect={handleSelectedGif}
/>
)}
</div>

<div className="cs1 ce12">
<h4>Search for more gifs:</h4>
<Search onSelect={handleSelectedGif} />
</div>
</div>
);
}
13 changes: 13 additions & 0 deletions examples/jwt-auth/src/components/SdkInit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use client";

import { useEffect } from "react";

export const MiroSDKInit = () => {
useEffect(() => {
miro.board.ui.on("icon:click", async () => {
await miro.board.ui.openPanel({ url: "/panel" });
});
});

return null;
};
Loading

0 comments on commit c2e4828

Please sign in to comment.