Skip to content

Commit

Permalink
fix figma authentication when browser is not authenticated; add next …
Browse files Browse the repository at this point in the history
…query string to auth (#46)
  • Loading branch information
tomasfrancisco authored Sep 26, 2024
1 parent 579706a commit 76b0b49
Show file tree
Hide file tree
Showing 18 changed files with 137 additions and 121 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ certificates
# monorepo
.turbo

# vite timestamp - remove after it's fixed: https://github.com/vitejs/vite/issues/13267
**/vite.config.ts.timestamp-*


# package specific
packages/components/src/components
Expand Down
2 changes: 0 additions & 2 deletions apps/engine/src/app/auth/_actions/auth.action.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use server';

import { z } from 'zod';
import { config } from '@/config';
import { unprotectedAction } from '@/lib/safe-action';

export const authAction = unprotectedAction
Expand All @@ -16,7 +15,6 @@ export const authAction = unprotectedAction
email: email,
options: {
shouldCreateUser: true,
emailRedirectTo: `${config.pageUrl}/auth/callback`,
},
});

Expand Down
5 changes: 4 additions & 1 deletion apps/engine/src/app/auth/_components/auth-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import Image from 'next/image';
import authBackground from '../_assets/auth-bg.svg';
import { AuthForm } from './auth-form';
import { Suspense } from 'react';

export function AuthCard() {
return (
Expand All @@ -29,7 +30,9 @@ export function AuthCard() {
</CardHeader>

<CardContent className="grid gap-4">
<AuthForm />
<Suspense fallback={'Loading...'}>
<AuthForm />
</Suspense>
</CardContent>
</Card>
);
Expand Down
5 changes: 3 additions & 2 deletions apps/engine/src/app/auth/_components/auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';

const FormSchema = z.object({
email: z.string().email({
Expand All @@ -39,6 +39,7 @@ const FormSchema = z.object({

export const AuthForm = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [loading, setLoading] = useState(false);
const [stage, setStage] = useState<'signin' | 'verify'>('signin');
const [error, setError] = useState<string>();
Expand All @@ -59,7 +60,7 @@ export const AuthForm = () => {
});

if (result?.data?.ok) {
router.replace('/app');
router.replace(searchParams.get('next') ?? '/app');
}

if (result?.data?.error) {
Expand Down
26 changes: 0 additions & 26 deletions apps/engine/src/app/auth/callback/route.ts

This file was deleted.

36 changes: 13 additions & 23 deletions apps/engine/src/lib/middleware/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ export const getUrlFromResponse = (
};

export const isAuthPath = (url: URL): boolean => {
return (
url.pathname === '/' ||
url.pathname.startsWith('/auth/callback') ||
url.pathname.startsWith('/auth/sign-in') ||
url.pathname.startsWith('/auth/auth')
);
return url.pathname === '/' || url.pathname.startsWith('/auth/sign-in');
};

export const isAuthenticatedPath = (url: URL): boolean => {
Expand All @@ -36,17 +31,15 @@ export const isFigmaAuthPath = (url: URL): boolean => {
);
};

export const hasOnGoingFigmaAuth = (request: NextRequest): boolean => {
return request.cookies.has(config.FIGMA_COOKIE_KEY);
export const hasOnGoingFigmaAuth = (url: URL): boolean => {
return url.searchParams.has(config.FIGMA_QUERY_KEY);
};

export const handleFigmaAuth = async ({
request,
response,
supabase,
url,
}: {
request: NextRequest;
response: NextResponse;
url: URL;
supabase: SupabaseClient<Database>;
Expand All @@ -65,36 +58,33 @@ export const handleFigmaAuth = async ({
url,
user,
supabase,
request,
});
} else {
response.cookies.set(config.FIGMA_COOKIE_KEY, figmaKey, {
maxAge: 5 * 60,
expires: 5 * 60 * 1000,
});
url.pathname = '/auth/sign-in';
url.search = '';
// Encode the next url which includes the figma key sign up page to return to after authentication
url.search = `?next=${encodeURI(`${url.pathname}${url.search}`)}`;
return NextResponse.redirect(url, { ...response, status: 307 });
}
};

export const exchangeApiKey = async ({
request,
response,
supabase,
url,
user,
}: {
request: NextRequest;
response: NextResponse;
url: URL;
user: User;
supabase: SupabaseClient<Database>;
}) => {
const figmaKey =
request.cookies.get(config.FIGMA_COOKIE_KEY)?.value ??
url.searchParams.get(config.FIGMA_QUERY_KEY);
const keyValue = await kv.getdel<KVCredentialsRead>(figmaKey ?? '');
const figmaKey = url.searchParams.get(config.FIGMA_QUERY_KEY);

if (!figmaKey) {
return response;
}

const keyValue = await kv.getdel<KVCredentialsRead>(figmaKey);

if (!keyValue) {
return response;
Expand Down Expand Up @@ -123,8 +113,8 @@ export const exchangeApiKey = async ({
return response;
}

response.cookies.delete(config.FIGMA_COOKIE_KEY);
url.pathname = '/auth/success';
// Remove the figma key from the url and any other query strings
url.search = '';
return NextResponse.redirect(url, { ...response, status: 307 });
};
9 changes: 5 additions & 4 deletions apps/engine/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export async function middleware(request: NextRequest) {

// Figma middleware logic
if (isFigmaAuthPath(url)) {
return handleFigmaAuth({ request, response, url, supabase });
return handleFigmaAuth({ response, url, supabase });
}

if (hasOnGoingFigmaAuth(request)) {
if (hasOnGoingFigmaAuth(url)) {
const {
data: { user },
} = await supabase.auth.getUser();
Expand All @@ -47,7 +47,6 @@ export async function middleware(request: NextRequest) {
url,
user,
supabase,
request,
});
}
}
Expand All @@ -58,12 +57,14 @@ export async function middleware(request: NextRequest) {
} = await supabase.auth.getUser();

if (!user && !isAuthPath(url) && isAuthenticatedPath(url)) {
// Encode the next url and redirect the user to the sign-in page
url.search = `?next=${encodeURI(`${url.pathname}${url.search}`)}`;
url.pathname = '/auth/sign-in';
return NextResponse.redirect(url, { ...response, status: 307 });
}

if (user && url.pathname.startsWith('/auth/sign-in')) {
url.pathname = '/app';
url.pathname = url.searchParams.get('next') ?? '/app';
return NextResponse.redirect(url, { ...response, status: 307 });
}

Expand Down
108 changes: 79 additions & 29 deletions packages/figma-utilities/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
import type { DesignTokens } from 'style-dictionary/types';
import type { Credentials } from './credentials';

const DEFAULT_EVENT_TIMEOUT = 10 * 1000; // 10 seconds

type RequestResponse = Record<
string,
{
Expand Down Expand Up @@ -63,60 +65,108 @@ export function on<Name extends EventName>(
name: Name,
handler: ResponseHandler<Name>
) {
console.log(`🔁 on ${name}`);
return defaultOn(name, handler);
console.log(`[on] waiting for event '${name}'`);
const cancelOn = defaultOn(name, (data: Event[Name]['response']) => {
handler(data);
});

return () => {
console.log(`[on] cancelling event '${name}'`);
cancelOn();
};
}

export function once<Name extends EventName>(
name: Name,
handler: ResponseHandler<Name>
) {
console.log(`🔁 once ${name}`);
return defaultOnce(name, handler);
console.log(`[once] waiting for event '${name}'`);
const cancelOnce = defaultOnce(name, (data: Event[Name]['response']) => {
handler(data);
});

return () => {
console.log(`[once] cancelling event '${name}'`);
cancelOnce();
};
}

export function emit<Name extends EventName>(
name: Name,
data: Event[Name]['response']
) {
console.log(`🚀 emit ${name}`);
console.log(`[emit] sending event '${name}'`);
return defaultEmit(name, data);
}

export async function request<Name extends EventName>(
export function request<Name extends EventName>(
name: Name,
data: Event[Name]['request']
data: Event[Name]['request'],
handler: ResponseHandler<Name>,
options: { timeout: number; onCanceled?: () => void } = {
timeout: DEFAULT_EVENT_TIMEOUT,
}
) {
console.log(`✈️ request ${name}`);
const response = new Promise<Event[Name]['response']>((resolve, _reject) => {
console.log(`✈️ request 🚀 emit ${name}`);
defaultEmit(name, data);

console.log(`✈️ request ⏰ wait for ${name}`);
defaultOn(name, resolve);
console.log(`[request] requesting event '${name}'`);
defaultEmit(name, data);

console.log(`[request] waiting for event '${name}'`);
const cancelOnce = defaultOnce(name, handler);

const timeout = setTimeout(() => {
console.log(`[request] event '${name}' timeout`);
cancelOnce();
options.onCanceled?.();
}, options.timeout);

return () => {
console.log(`[request] cancelling event '${name}'`);
clearTimeout(timeout);
cancelOnce();
options.onCanceled?.();
};
}

// TODO: handle timeout to reject the promise
export function requestAsync<Name extends EventName>(
name: Name,
data: Event[Name]['request'],
options: { timeout: number } = { timeout: DEFAULT_EVENT_TIMEOUT }
): Promise<Event[Name]['response']> {
return new Promise((resolve, reject) => {
request(name, data, resolve, {
...options,
onCanceled: reject,
});
});
return response;
}

export async function handle<Name extends EventName>(
export function handle<Name extends EventName>(
name: Name,
handler: (
data: Event[Name]['request']
) => Promise<Event[Name]['response']> | Event[Name]['response']
): Promise<void> {
const response = await new Promise<Event[Name]['response']>(
(resolve, _reject) => {
console.log(`✈️ handle 🔁 on ${name}`);
defaultOn(name, (data: Event[Name]['request']) => {
console.log(`✈️ handle 🚀 emit ${name}`);
resolve(handler(data));
) => Promise<Event[Name]['response']> | Event[Name]['response'],
options: { timeout: number } = { timeout: DEFAULT_EVENT_TIMEOUT }
): () => void {
console.log(`[handle] waiting for event '${name}'`);
const cancelOnce = defaultOnce(name, (data: Event[Name]['request']) => {
Promise.resolve(handler(data))
.then((response) => {
console.log(`[handle] responding to event '${name}'`);
defaultEmit(name, response);
})
.catch(() => {
console.error(`[handle] error handling event '${name}'`);
});
});

// TODO: handle timeout to reject the promise
}
);
const timeout = setTimeout(() => {
console.log(`[handle] event '${name}' timeout`);
cancelOnce();
}, options.timeout);

return defaultEmit(name, response);
return () => {
console.log(`[handle] canceling event '${name}'`);
clearTimeout(timeout);
cancelOnce();
};
}
2 changes: 1 addition & 1 deletion packages/figma-widget/src/ui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Container } from './components/container';
export function App() {
useEffect(() => {
// Announce to the plugin that the UI is ready to receive messages
emit('ui-is-ready', undefined);
return emit('ui-is-ready', undefined);
}, []);

return (
Expand Down
2 changes: 1 addition & 1 deletion packages/figma-widget/src/ui/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const featureFlags = {

export const config = {
API_HOST: import.meta.env.VITE_API_HOST,
READ_INTERVAL: 1 * 1000, // 1 seconds
API_KEY_READ_INTERVAL: 2 * 1000, // 2 seconds
CREDENTIALS_KEY: 'ds-pro__credentials',
PROJECT_ID_KEY: 'ds-pro__id',
features: featureFlags,
Expand Down
Loading

0 comments on commit 76b0b49

Please sign in to comment.