From 23802385206406dd3811fea946dd4b77a160cd34 Mon Sep 17 00:00:00 2001 From: "Ryan J. Shaw" <610578+ryanjshaw@users.noreply.github.com> Date: Thu, 10 Oct 2024 20:30:09 +0200 Subject: [PATCH 1/8] Update .env.example with Figma OAuth App keys --- .env.example | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.env.example b/.env.example index a4dd1801..1d55fb89 100644 --- a/.env.example +++ b/.env.example @@ -35,3 +35,7 @@ ARBSCAN_API_KEY= FANTOMSCAN_API_KEY= POLYGONSCAN_API_KEY= BSCSCAN_API_KEY= + +# Figma App for OAuth +FIGMA_CLIENT_ID= +FIGMA_CLIENT_SECRET= \ No newline at end of file From f61c2f08d1a8336c6e37c4ff3c19f033a5ac0daf Mon Sep 17 00:00:00 2001 From: "Ryan J. Shaw" <610578+ryanjshaw@users.noreply.github.com> Date: Fri, 11 Oct 2024 22:47:54 +0200 Subject: [PATCH 2/8] Blocked on next-auth v5 and non-standard Figma OAuth2 impl --- .env.example | 1 + auth.ts | 65 ++++- templates/figma/Inspector.tsx | 246 +++++++++--------- .../figma/components/FigmaTokenEditor.tsx | 63 ++--- templates/figma/components/PropertiesTab.tsx | 17 +- templates/figma/components/SlideEditor.tsx | 4 +- 6 files changed, 210 insertions(+), 186 deletions(-) diff --git a/.env.example b/.env.example index 1d55fb89..cd7bd6f5 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ NEYNAR_API_KEY= +NEXTAUTH_URL=http://localhost:3000 NEXT_PUBLIC_HOST=http://localhost:3000 NEXT_PUBLIC_DOMAIN=localhost:3000 NEXT_PUBLIC_CDN_HOST= diff --git a/auth.ts b/auth.ts index f7ff4dae..1686e968 100644 --- a/auth.ts +++ b/auth.ts @@ -1,6 +1,57 @@ import { createAppClient, viemConnector } from '@farcaster/auth-client' import NextAuth from 'next-auth' import CredentialsProvider from 'next-auth/providers/credentials' +import type { Provider } from "next-auth/providers"; +import type { Session } from 'next-auth'; + +// Extend the Session type to include figmaAccessToken +export interface FrameTrainSession extends Session { + figmaAccessToken?: string; +} + +export interface FrameTrainSession extends Session { + figmaAccessToken?: string; +} + +const FigmaProvider: Provider = { + id: "figma", + name: "Figma", + type: "oauth", + authorization: { + url: "https://www.figma.com/oauth", + params: { + scope: "files:read", + response_type: "code", + state: "{}" + }, + }, + token: { + url: "https://api.figma.com/v1/oauth/token", + async request(context: any) { + const provider = context.provider; + + const res = await fetch( + `https://api.figma.com/v1/oauth/token?client_id=${provider.clientId}&client_secret=${provider.clientSecret}&redirect_uri=${provider.CallbackUrl}&code=${context.params.code}&grant_type=authorization_code`, + { method: "POST" } + ); + const json = await res.json(); + + return { tokens: json }; + }, + }, + userinfo: "https://api.figma.com/v1/me", + profile(profile) { + return { + id: profile.id, + name: `${profile.handle}`, + email: profile.email, + image: profile.img_url,nre + }; + }, + clientId: process.env.FIGMA_CLIENT_ID, + clientSecret: process.env.FIGMA_CLIENT_SECRET +}; + export const { handlers: { GET, POST }, @@ -63,19 +114,25 @@ export const { } }, }), + FigmaProvider, ], callbacks: { jwt: async ({ token, user, account, profile, trigger }) => { - if (user) token.user = user if (user) { + token.user = user token.uid = user.id } + if (account?.provider === 'figma') { + // If the user is connecting Figma, store the access token for Figma + token.figmaAccessToken = account.access_token; + } return token }, session: async ({ session, token, user }) => { - if (token.user) session.user = { ...session.user, id: (token.user as any).id } - // session.user.uid = user.uid; - return session + const frameTrainSession = session as FrameTrainSession; + if (token.user) frameTrainSession.user = { ...frameTrainSession.user, id: (token.user as any).id }; + frameTrainSession.figmaAccessToken = token.figmaAccessToken as string | undefined; + return frameTrainSession; }, }, events: { diff --git a/templates/figma/Inspector.tsx b/templates/figma/Inspector.tsx index 3fa82d89..1867f94a 100644 --- a/templates/figma/Inspector.tsx +++ b/templates/figma/Inspector.tsx @@ -36,6 +36,7 @@ export default function Inspector() { buttonIndex: 0, inputText: '', params: `slideId=${id}`, + postUrl: undefined }) } @@ -126,6 +127,7 @@ export default function Inspector() { } // Must run after rendering as it modifies the document + // biome-ignore lint/correctness/useExhaustiveDependencies: circular rulese useEffect(() => { if (!config.slides) return for (const slide of config.slides) { @@ -137,7 +139,7 @@ export default function Inspector() { } } } - }, [config.slides, identifyFontsUsed]) + }, [config.slides]) // Setup default slides if this is a new instance useEffect(() => { @@ -169,140 +171,132 @@ export default function Inspector() { return ( - - {editingFigmaPAT ? ( - setEditingFigmaPAT(false)} - /> - ) : ( - - )} + + setEditingFigmaPAT(false)} + /> + - {!editingFigmaPAT ? ( - -
-
- - - -
+ +
+
+ + +
-
- {config.slides.map((slideConfig, index) => ( +
+
+ {config.slides.map((slideConfig, index) => ( +
{ + setSelectedSlideIndex(index) + previewSlide(slideConfig.id) + }} + className={`w-40 h-40 flex items-center justify-center mr-1 border-[1px] rounded-md cursor-pointer select-none ${ + selectedSlideIndex === index + ? 'border-highlight' + : 'border-input' + }`} + >
{ - setSelectedSlideIndex(index) - previewSlide(slideConfig.id) + style={{ + 'transform': + slideConfig.aspectRatio == '1:1' + ? 'scale(0.245)' + : 'scale(0.130)', + // Handle the case where no image has been configured but we need a min-width + ...(!slideConfig.baseImagePaths + ? { + 'width': + slideConfig.aspectRatio == '1:1' + ? dimensionsForRatio['1/1'].width + : dimensionsForRatio['1.91/1'].height, + } + : {}), + 'overflow': 'clip', }} - className={`w-40 h-40 flex items-center justify-center mr-1 border-[1px] rounded-md cursor-pointer select-none ${ - selectedSlideIndex === index - ? 'border-highlight' - : 'border-input' - }`} - > -
- -
-
- ))} - - {figmaUnderstood ? ( -
- + +
- ) : ( - - -
- + -
-
- - - Resolution Notice - - The Figma URL entered must lead to an artboard/section - that is either 630x630 or 1200x630 pixels in size. -
- - (figmaUnderstoodRef.current = e === true) - } - /> - -
-
-
- - Back - { - addSlide() - - if (figmaUnderstoodRef.current) { - setFigmaUnderstood(true) +
+ ))} + + {figmaUnderstood ? ( +
+ + +
+ ) : ( + + +
+ + +
+
+ + + Resolution Notice + + The Figma URL entered must lead to an artboard/section + that is either 630x630 or 1200x630 pixels in size. +
+ + (figmaUnderstoodRef.current = e === true) } - }} - > - Understood - - - - - )} -
- - {config.slides?.[selectedSlideIndex] && ( - updateSlide(updatedSlideConfig)} - /> + /> + +
+ + + + Back + { + addSlide() + + if (figmaUnderstoodRef.current) { + setFigmaUnderstood(true) + } + }} + > + Understood + + + + )} - - ) : undefined} +
+ + {config.slides?.[selectedSlideIndex] && ( + updateSlide(updatedSlideConfig)} + /> + )} +
) } diff --git a/templates/figma/components/FigmaTokenEditor.tsx b/templates/figma/components/FigmaTokenEditor.tsx index 31fe35ca..70862aef 100644 --- a/templates/figma/components/FigmaTokenEditor.tsx +++ b/templates/figma/components/FigmaTokenEditor.tsx @@ -1,55 +1,24 @@ 'use client' +import type { FrameTrainSession } from '@/auth' import { Button, Input } from '@/sdk/components' import { InfoIcon, SaveIcon, XIcon } from 'lucide-react' +import { signIn, useSession } from 'next-auth/react' import { useState } from 'react' -type FigmaTokenEditorProps = { - figmaPAT: string - onChange: (figmaPAT: string) => void - onCancel: () => void -} +export default function FigmaTokenEditor() { + const { data: session } = useSession(); -export default function FigmaTokenEditor({ figmaPAT, onChange, onCancel }: FigmaTokenEditorProps) { - const [newFigmaPAT, setNewFigmaPAT] = useState('') + const figmaAccessToken = (session as FrameTrainSession)?.figmaAccessToken; - return ( -
-

Figma Personal Access Token (PAT)

-
- setNewFigmaPAT(e.target.value)} - /> - -
-
- - -
-
- ) + return ( +
+ {!figmaAccessToken ? ( + + ) : ( +

Figma Account Connected

+ )} +
+ ) } diff --git a/templates/figma/components/PropertiesTab.tsx b/templates/figma/components/PropertiesTab.tsx index 8c056669..8062284e 100644 --- a/templates/figma/components/PropertiesTab.tsx +++ b/templates/figma/components/PropertiesTab.tsx @@ -13,6 +13,8 @@ import type { TextLayerConfigs, } from '../Config' import { getFigmaDesign, svgToDataUrl } from '../utils/FigmaApi' +import { useSession } from 'next-auth/react' +import type { FrameTrainSession } from '@/auth' const SVG_TEXT_DEBUG_ENABLED = false @@ -22,7 +24,6 @@ type PropertiesTabProps = { description: string textLayers: TextLayerConfigs aspectRatio: AspectRatio - figmaPAT: string figmaUrl?: string figmaMetadata?: FigmaMetadata onUpdateTitle: (title: string) => void @@ -42,7 +43,6 @@ export const PropertiesTab = ({ description, textLayers, aspectRatio, - figmaPAT, figmaUrl, figmaMetadata, onUpdateTitle, @@ -55,13 +55,18 @@ export const PropertiesTab = ({ const [newUrl, setNewUrl] = useState(figmaUrl) const [isUpdating, setIsUpdating] = useState(false) + // Access the figmaAccessToken from the session + const { data: session } = useSession(); + const figmaAccessToken = (session as FrameTrainSession)?.figmaAccessToken; + + const updateUrl = async () => { console.debug(`updateFigmaUrl(${slideConfigId})`) setIsUpdating(true) // Fetch the Figma design - const figmaDesignResult = await getFigmaDesign(figmaPAT, newUrl) + const figmaDesignResult = await getFigmaDesign(figmaAccessToken!, newUrl) if (!figmaDesignResult.success) { toast.error(figmaDesignResult.error) setIsUpdating(false) @@ -210,16 +215,16 @@ export const PropertiesTab = ({ right click > copy as > copy link' : 'Configure Figma PAT first' } - disabled={!figmaPAT} + disabled={!figmaAccessToken} value={newUrl} onChange={(e) => setNewUrl(e.target.value)} className="mr-2" /> - - - + + + + + + + +
+
+ + + +
-
-
- {config.slides.map((slideConfig, index) => ( -
{ - setSelectedSlideIndex(index) - previewSlide(slideConfig.id) - }} - className={`w-40 h-40 flex items-center justify-center mr-1 border-[1px] rounded-md cursor-pointer select-none ${ - selectedSlideIndex === index +
+ {config.slides.map((slideConfig, index) => ( +
{ + setSelectedSlideIndex(index) + previewSlide(slideConfig.id) + }} + className={`w-40 h-40 flex items-center justify-center mr-1 border-[1px] rounded-md cursor-pointer select-none ${selectedSlideIndex === index ? 'border-highlight' : 'border-input' - }`} - > -
+
- + : {}), + 'overflow': 'clip', + }} + > + +
-
- ))} + ))} - {figmaUnderstood ? ( -
- + -
- ) : ( - - -
- + -
-
- - - Resolution Notice - - The Figma URL entered must lead to an artboard/section - that is either 630x630 or 1200x630 pixels in size. -
- - (figmaUnderstoodRef.current = e === true) + {figmaUnderstood ? ( +
+ + +
+ ) : ( + + +
+ + +
+
+ + + Resolution Notice + + The Figma URL entered must lead to an artboard/section + that is either 630x630 or 1200x630 pixels in size. +
+ + (figmaUnderstoodRef.current = e === true) + } + /> + +
+
+
+ + Back + { + addSlide() + + if (figmaUnderstoodRef.current) { + setFigmaUnderstood(true) } - /> - -
-
-
- - Back - { - addSlide() + }} + > + Understood + + +
+
+ )} +
- if (figmaUnderstoodRef.current) { - setFigmaUnderstood(true) - } - }} - > - Understood - - - - + {config.slides?.[selectedSlideIndex] && ( + updateSlide(updatedSlideConfig)} + /> )} -
- - {config.slides?.[selectedSlideIndex] && ( - updateSlide(updatedSlideConfig)} - /> - )} - - + + + ) } diff --git a/templates/figma/components/FigmaConnector.tsx b/templates/figma/components/FigmaConnector.tsx new file mode 100644 index 00000000..382be392 --- /dev/null +++ b/templates/figma/components/FigmaConnector.tsx @@ -0,0 +1,28 @@ +'use client'; +import { Button } from '@/sdk/components'; +import { useFigmaToken } from './FigmaTokenContext'; +import { useRouter } from 'next/navigation'; + +export default function FigmaTokenEditor() { + const router = useRouter(); + const { figmaAccessToken, loading } = useFigmaToken(); + + const handleConnectFigma = () => { + const currentPage = window.location.href; + router.push(`/api/auth/figma?original_url=${encodeURIComponent(currentPage)}`); + }; + + return ( +
+ {loading ? ( +

Loading...

+ ) : !figmaAccessToken ? ( + + ) : ( +

Figma Account Connected

+ )} +
+ ); +} diff --git a/templates/figma/components/FigmaTokenContext.tsx b/templates/figma/components/FigmaTokenContext.tsx new file mode 100644 index 00000000..34a2ec48 --- /dev/null +++ b/templates/figma/components/FigmaTokenContext.tsx @@ -0,0 +1,54 @@ +import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; + +// Define the shape of the context +interface FigmaTokenContextType { + figmaAccessToken: string | null; + loading: boolean; +} + +// Create the context with default values +const FigmaTokenContext = createContext(undefined); + +// Context provider component +export const FigmaTokenProvider = ({ children }: { children: ReactNode }) => { + const [figmaAccessToken, setFigmaAccessToken] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchToken = async () => { + try { + const response = await fetch('/api/auth/figma/status'); + const data = await response.json(); + + if (data.accessToken) { + setFigmaAccessToken(data.accessToken); + } else { + setFigmaAccessToken(null); + } + } catch (error) { + console.error('Error checking Figma connection status:', error); + setFigmaAccessToken(null); + } finally { + setLoading(false); + } + }; + + fetchToken(); + }, []); // Run once on mount + + return ( + + {children} + + ); +}; + + +// Hook to use the FigmaTokenContext +export const useFigmaToken = () => { + const context = useContext(FigmaTokenContext); + if (context === undefined) { + throw new Error('useFigmaToken must be used within a FigmaTokenProvider'); + } + return context; +}; diff --git a/templates/figma/components/FigmaTokenEditor.tsx b/templates/figma/components/FigmaTokenEditor.tsx deleted file mode 100644 index 70862aef..00000000 --- a/templates/figma/components/FigmaTokenEditor.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client' -import type { FrameTrainSession } from '@/auth' -import { Button, Input } from '@/sdk/components' -import { InfoIcon, SaveIcon, XIcon } from 'lucide-react' -import { signIn, useSession } from 'next-auth/react' -import { useState } from 'react' - -export default function FigmaTokenEditor() { - const { data: session } = useSession(); - - const figmaAccessToken = (session as FrameTrainSession)?.figmaAccessToken; - - return ( -
- {!figmaAccessToken ? ( - - ) : ( -

Figma Account Connected

- )} -
- ) -} diff --git a/templates/figma/components/PropertiesTab.tsx b/templates/figma/components/PropertiesTab.tsx index 8062284e..3db266e4 100644 --- a/templates/figma/components/PropertiesTab.tsx +++ b/templates/figma/components/PropertiesTab.tsx @@ -15,6 +15,7 @@ import type { import { getFigmaDesign, svgToDataUrl } from '../utils/FigmaApi' import { useSession } from 'next-auth/react' import type { FrameTrainSession } from '@/auth' +import { useFigmaToken } from './FigmaTokenContext' const SVG_TEXT_DEBUG_ENABLED = false @@ -55,10 +56,7 @@ export const PropertiesTab = ({ const [newUrl, setNewUrl] = useState(figmaUrl) const [isUpdating, setIsUpdating] = useState(false) - // Access the figmaAccessToken from the session - const { data: session } = useSession(); - const figmaAccessToken = (session as FrameTrainSession)?.figmaAccessToken; - + const { figmaAccessToken, loading } = useFigmaToken(); const updateUrl = async () => { console.debug(`updateFigmaUrl(${slideConfigId})`) @@ -187,8 +185,12 @@ export const PropertiesTab = ({ const aspectRatioFormatted = !figmaMetadata ? '' : figmaMetadata.aspectRatio % 1 === 0 - ? figmaMetadata?.aspectRatio?.toString() + ':1' - : figmaMetadata?.aspectRatio?.toFixed(2) + ':1' + ? figmaMetadata?.aspectRatio?.toString() + ':1' + : figmaMetadata?.aspectRatio?.toFixed(2) + ':1' + + if (loading) { + return
Loading...
+ } return (
@@ -217,7 +219,7 @@ export const PropertiesTab = ({ placeholder={ figmaAccessToken ? 'Figma Frame > right click > copy as > copy link' - : 'Configure Figma PAT first' + : 'Connect Figma Account' } disabled={!figmaAccessToken} value={newUrl} diff --git a/templates/figma/utils/FigmaApi.ts b/templates/figma/utils/FigmaApi.ts index 43196da6..0cd725e8 100644 --- a/templates/figma/utils/FigmaApi.ts +++ b/templates/figma/utils/FigmaApi.ts @@ -227,11 +227,11 @@ export async function getFigmaDesign( * See: https://www.figma.com/developers/api#get-files-endpoint */ export async function getFigmaFile( - figmaPAT: string, + figmaAccessToken: string, linkUrl?: string ): Promise> { try { - if (!figmaPAT) { + if (!figmaAccessToken) { return { success: false, error: 'Personal Access Token (PAT) is missing or empty' } } @@ -245,7 +245,8 @@ export async function getFigmaFile( { method: 'GET', headers: { - 'X-FIGMA-TOKEN': figmaPAT, + 'Authorization': `Bearer ${figmaAccessToken}`, + 'X-FIGMA-TOKEN': figmaAccessToken, }, cache: 'no-store', // always get the latest } @@ -274,11 +275,11 @@ export async function getFigmaFile( * See: https://www.figma.com/developers/api#get-images-endpoint */ export async function getFigmaSvgImage( - figmaPAT: string, + figmaAccessToken: string, linkUrl?: string ): Promise> { try { - if (!figmaPAT) { + if (!figmaAccessToken) { return { success: false, error: 'Personal Access Token (PAT) is missing or empty' } } @@ -292,7 +293,8 @@ export async function getFigmaSvgImage( { method: 'GET', headers: { - 'X-FIGMA-TOKEN': figmaPAT, + 'Authorization': `Bearer ${figmaAccessToken}`, + 'X-FIGMA-TOKEN': figmaAccessToken, }, cache: 'no-store', // PERFORMANCE: think about how we can avoid this } From e6298e14ead9e9b52539680713b4374fa36b80ff Mon Sep 17 00:00:00 2001 From: "Ryan J. Shaw" <610578+ryanjshaw@users.noreply.github.com> Date: Mon, 14 Oct 2024 01:26:15 +0200 Subject: [PATCH 4/8] Signout button --- app/api/auth/figma/signout/route.ts | 16 ++++++++++++++++ templates/figma/components/FigmaConnector.tsx | 14 +++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 app/api/auth/figma/signout/route.ts diff --git a/app/api/auth/figma/signout/route.ts b/app/api/auth/figma/signout/route.ts new file mode 100644 index 00000000..357cbbb2 --- /dev/null +++ b/app/api/auth/figma/signout/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server'; + +/** + * Figma signout + */ +export async function POST() { + const response = NextResponse.json({ message: 'Signout successful' }); + + // Clear Figma-related cookies + response.cookies.delete('figma_access_token'); + response.cookies.delete('figma_refresh_token'); + response.cookies.delete('figma_user_id'); + response.cookies.delete('figma_token_expires_at'); + + return response; +} \ No newline at end of file diff --git a/templates/figma/components/FigmaConnector.tsx b/templates/figma/components/FigmaConnector.tsx index 382be392..82cd8848 100644 --- a/templates/figma/components/FigmaConnector.tsx +++ b/templates/figma/components/FigmaConnector.tsx @@ -12,6 +12,16 @@ export default function FigmaTokenEditor() { router.push(`/api/auth/figma?original_url=${encodeURIComponent(currentPage)}`); }; + const handleSignout = async () => { + await fetch('/api/auth/figma/signout', { + method: 'POST' + }); + + // Refresh the page + // biome-ignore lint/correctness/noSelfAssign: + window.location.href = window.location.href; + }; + return (
{loading ? ( @@ -21,7 +31,9 @@ export default function FigmaTokenEditor() { Connect Figma Account ) : ( -

Figma Account Connected

+ )}
); From 5721889e8ad10b5bd83a174b6ed7545342340335 Mon Sep 17 00:00:00 2001 From: "Ryan J. Shaw" <610578+ryanjshaw@users.noreply.github.com> Date: Mon, 14 Oct 2024 01:30:18 +0200 Subject: [PATCH 5/8] Remove NEXTAUTH_URL from env example --- .env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.example b/.env.example index cd7bd6f5..1d55fb89 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,5 @@ NEYNAR_API_KEY= -NEXTAUTH_URL=http://localhost:3000 NEXT_PUBLIC_HOST=http://localhost:3000 NEXT_PUBLIC_DOMAIN=localhost:3000 NEXT_PUBLIC_CDN_HOST= From 4551359303e337a6a7402fab7c783a7657b3e8c7 Mon Sep 17 00:00:00 2001 From: "Ryan J. Shaw" <610578+ryanjshaw@users.noreply.github.com> Date: Mon, 14 Oct 2024 08:08:10 +0200 Subject: [PATCH 6/8] Revert next-auth figma implementation --- auth.ts | 65 ++++----------------------------------------------------- 1 file changed, 4 insertions(+), 61 deletions(-) diff --git a/auth.ts b/auth.ts index 1686e968..f7ff4dae 100644 --- a/auth.ts +++ b/auth.ts @@ -1,57 +1,6 @@ import { createAppClient, viemConnector } from '@farcaster/auth-client' import NextAuth from 'next-auth' import CredentialsProvider from 'next-auth/providers/credentials' -import type { Provider } from "next-auth/providers"; -import type { Session } from 'next-auth'; - -// Extend the Session type to include figmaAccessToken -export interface FrameTrainSession extends Session { - figmaAccessToken?: string; -} - -export interface FrameTrainSession extends Session { - figmaAccessToken?: string; -} - -const FigmaProvider: Provider = { - id: "figma", - name: "Figma", - type: "oauth", - authorization: { - url: "https://www.figma.com/oauth", - params: { - scope: "files:read", - response_type: "code", - state: "{}" - }, - }, - token: { - url: "https://api.figma.com/v1/oauth/token", - async request(context: any) { - const provider = context.provider; - - const res = await fetch( - `https://api.figma.com/v1/oauth/token?client_id=${provider.clientId}&client_secret=${provider.clientSecret}&redirect_uri=${provider.CallbackUrl}&code=${context.params.code}&grant_type=authorization_code`, - { method: "POST" } - ); - const json = await res.json(); - - return { tokens: json }; - }, - }, - userinfo: "https://api.figma.com/v1/me", - profile(profile) { - return { - id: profile.id, - name: `${profile.handle}`, - email: profile.email, - image: profile.img_url,nre - }; - }, - clientId: process.env.FIGMA_CLIENT_ID, - clientSecret: process.env.FIGMA_CLIENT_SECRET -}; - export const { handlers: { GET, POST }, @@ -114,25 +63,19 @@ export const { } }, }), - FigmaProvider, ], callbacks: { jwt: async ({ token, user, account, profile, trigger }) => { + if (user) token.user = user if (user) { - token.user = user token.uid = user.id } - if (account?.provider === 'figma') { - // If the user is connecting Figma, store the access token for Figma - token.figmaAccessToken = account.access_token; - } return token }, session: async ({ session, token, user }) => { - const frameTrainSession = session as FrameTrainSession; - if (token.user) frameTrainSession.user = { ...frameTrainSession.user, id: (token.user as any).id }; - frameTrainSession.figmaAccessToken = token.figmaAccessToken as string | undefined; - return frameTrainSession; + if (token.user) session.user = { ...session.user, id: (token.user as any).id } + // session.user.uid = user.uid; + return session }, }, events: { From 8aaf68c9ddcad9a024cdd2ce329c8aeeb274a3d1 Mon Sep 17 00:00:00 2001 From: "Ryan J. Shaw" <610578+ryanjshaw@users.noreply.github.com> Date: Mon, 14 Oct 2024 08:09:30 +0200 Subject: [PATCH 7/8] Use signin URL structure --- app/api/auth/figma/{ => signin}/route.ts | 0 templates/figma/components/FigmaConnector.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/api/auth/figma/{ => signin}/route.ts (100%) diff --git a/app/api/auth/figma/route.ts b/app/api/auth/figma/signin/route.ts similarity index 100% rename from app/api/auth/figma/route.ts rename to app/api/auth/figma/signin/route.ts diff --git a/templates/figma/components/FigmaConnector.tsx b/templates/figma/components/FigmaConnector.tsx index 82cd8848..70b730d8 100644 --- a/templates/figma/components/FigmaConnector.tsx +++ b/templates/figma/components/FigmaConnector.tsx @@ -9,7 +9,7 @@ export default function FigmaTokenEditor() { const handleConnectFigma = () => { const currentPage = window.location.href; - router.push(`/api/auth/figma?original_url=${encodeURIComponent(currentPage)}`); + router.push(`/api/auth/figma/signin?original_url=${encodeURIComponent(currentPage)}`); }; const handleSignout = async () => { From db3f73ae496f900a9974a1198bf1c202f6b29d9e Mon Sep 17 00:00:00 2001 From: "Ryan J. Shaw" <610578+ryanjshaw@users.noreply.github.com> Date: Mon, 14 Oct 2024 08:19:33 +0200 Subject: [PATCH 8/8] Cleanup and wait spiny thing --- .env.example | 1 + app/api/auth/figma/callback/route.ts | 11 ++++++++++- app/api/auth/figma/signin/route.ts | 10 ++++++++++ templates/figma/Config.ts | 1 - templates/figma/components/FigmaConnector.tsx | 16 +++++++++++++--- templates/figma/components/PropertiesTab.tsx | 2 -- templates/figma/utils/FigmaApi.ts | 6 +++--- templates/figma/utils/FigmaFrameBuilder.ts | 2 +- 8 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 1d55fb89..1b98182b 100644 --- a/.env.example +++ b/.env.example @@ -37,5 +37,6 @@ POLYGONSCAN_API_KEY= BSCSCAN_API_KEY= # Figma App for OAuth +# Create these here: https://www.figma.com/developers/apps FIGMA_CLIENT_ID= FIGMA_CLIENT_SECRET= \ No newline at end of file diff --git a/app/api/auth/figma/callback/route.ts b/app/api/auth/figma/callback/route.ts index 79088c1d..6ee2a0f9 100644 --- a/app/api/auth/figma/callback/route.ts +++ b/app/api/auth/figma/callback/route.ts @@ -8,11 +8,20 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const { FIGMA_CLIENT_ID, FIGMA_CLIENT_SECRET, NEXT_PUBLIC_HOST } = process.env; + // biome-ignore lint/complexity/useSimplifiedLogicExpression: + if (!FIGMA_CLIENT_ID || !FIGMA_CLIENT_SECRET || !NEXT_PUBLIC_HOST) { + return NextResponse.json({ error: 'Missing environment variables' }, { status: 500 }); + } + // Retrieve the stored state from the cookie and verify the CSRF token const stateParam = searchParams.get('state'); const storedState = request.cookies.get('oauth_state')?.value; + // biome-ignore lint/complexity/useSimplifiedLogicExpression: + if (!stateParam || !storedState) { + return NextResponse.json({ error: 'Missing state parameter' }, { status: 500 }); + } if (storedState !== stateParam) { - return NextResponse.json({ error: 'State mismatch. Possible CSRF attack.' }, { status: 400 }); + return NextResponse.json({ error: 'State mismatch. Possible CSRF attack.' }, { status: 500 }); } // Get the original page URL from the state diff --git a/app/api/auth/figma/signin/route.ts b/app/api/auth/figma/signin/route.ts index 66f62530..83c6a598 100644 --- a/app/api/auth/figma/signin/route.ts +++ b/app/api/auth/figma/signin/route.ts @@ -7,9 +7,19 @@ import { type NextRequest, NextResponse } from 'next/server'; export async function GET(request: NextRequest) { const { FIGMA_CLIENT_ID, NEXT_PUBLIC_HOST } = process.env; + // biome-ignore lint/complexity/useSimplifiedLogicExpression: + if (!FIGMA_CLIENT_ID || !NEXT_PUBLIC_HOST) { + return NextResponse.json({ error: 'Missing environment variables' }, { status: 500 }); + } + // Get the original page URL from query params or referrer header const originalUrl = request.nextUrl.searchParams.get('original_url'); + // URL redirection attacks + if (!originalUrl?.startsWith(NEXT_PUBLIC_HOST)) { + return NextResponse.json({ error: 'Invalid redirect URL' }, { status: 400 }); + } + const redirectUri = `${NEXT_PUBLIC_HOST}/api/auth/figma/callback`; const state = JSON.stringify({ csrfToken: Math.random().toString(36).substring(2), diff --git a/templates/figma/Config.ts b/templates/figma/Config.ts index 2a79fa15..b8da8bc3 100644 --- a/templates/figma/Config.ts +++ b/templates/figma/Config.ts @@ -1,7 +1,6 @@ import type { FigmaTextLayer } from './utils/FigmaApi' export interface FramePressConfig { - figmaPAT: string slides: SlideConfig[] nextSlideId: number } diff --git a/templates/figma/components/FigmaConnector.tsx b/templates/figma/components/FigmaConnector.tsx index 70b730d8..19717335 100644 --- a/templates/figma/components/FigmaConnector.tsx +++ b/templates/figma/components/FigmaConnector.tsx @@ -2,29 +2,39 @@ import { Button } from '@/sdk/components'; import { useFigmaToken } from './FigmaTokenContext'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; -export default function FigmaTokenEditor() { +export default function FigmaConnector() { const router = useRouter(); const { figmaAccessToken, loading } = useFigmaToken(); + const [isConnecting, setIsConnecting] = useState(false); // To track connection state const handleConnectFigma = () => { + setIsConnecting(true); // Set connecting state + + // Change cursor to 'wait' + document.body.style.cursor = 'wait'; + const currentPage = window.location.href; router.push(`/api/auth/figma/signin?original_url=${encodeURIComponent(currentPage)}`); }; const handleSignout = async () => { + // Change cursor to 'wait' + document.body.style.cursor = 'wait'; + await fetch('/api/auth/figma/signout', { method: 'POST' }); // Refresh the page - // biome-ignore lint/correctness/noSelfAssign: + // biome-ignore lint/correctness/noSelfAssign: this is legit window.location.href = window.location.href; }; return (
- {loading ? ( + {loading || isConnecting ? (

Loading...

) : !figmaAccessToken ? (