diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index 60bffd2..5b1c5d6 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -29,11 +29,33 @@ const nextConfig = { }, { key: 'Access-Control-Allow-Origin', - value: '*', // TODO: Perhaps set figma origin instead? + value: '*', // TODO: Set specific origin instead of '*' for production }, { key: 'Access-Control-Allow-Methods', - value: 'POST, PUT, DELETE, OPTIONS', + value: 'GET, POST, PUT, DELETE, OPTIONS', + }, + { + key: 'Access-Control-Allow-Headers', + value: 'Content-Type, Authorization', + }, + ], + }, + // Add this new object to cover /api/auth paths + { + source: '/api/auth/:path*', + headers: [ + { + key: 'Access-Control-Allow-Credentials', + value: 'true', + }, + { + key: 'Access-Control-Allow-Origin', + value: '*', // TODO: Set specific origin instead of '*' for production + }, + { + key: 'Access-Control-Allow-Methods', + value: 'GET, POST, PUT, DELETE, OPTIONS', }, { key: 'Access-Control-Allow-Headers', diff --git a/apps/dashboard/src/app/api/auth/refresh/route.ts b/apps/dashboard/src/app/api/auth/refresh/route.ts deleted file mode 100644 index 8bb28fc..0000000 --- a/apps/dashboard/src/app/api/auth/refresh/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createServerClient } from '@ds-project/auth/server'; -import type { NextRequest } from 'next/server'; - -export async function POST(request: NextRequest) { - const result = (await request.json()) as { refreshToken: string }; - - const supabase = createServerClient(); - const { - data: { session }, - } = await supabase.auth.refreshSession({ - refresh_token: result.refreshToken, - }); - - const accessToken = session?.access_token; - const refreshToken = session?.refresh_token; - const expiresAt = session?.expires_at; - - return new Response( - JSON.stringify({ accessToken, refreshToken, expiresAt }), - { status: 200 } - ); -} diff --git a/apps/dashboard/src/lib/middleware/figma/middleware.ts b/apps/dashboard/src/lib/middleware/figma/middleware.ts index 61f4b8d..60ee079 100644 --- a/apps/dashboard/src/lib/middleware/figma/middleware.ts +++ b/apps/dashboard/src/lib/middleware/figma/middleware.ts @@ -7,6 +7,7 @@ import type { MiddlewareFactory } from '../compose'; import { createMiddlewareClient } from '@ds-project/auth/middleware'; import { clientEnv } from '@/env/client'; import { config } from '@/config'; +import { createApiKey } from '@ds-project/api/operations'; export const figmaMiddleware: MiddlewareFactory = (middleware) => @@ -31,24 +32,25 @@ export const figmaMiddleware: MiddlewareFactory = supabaseUrl: clientEnv.NEXT_PUBLIC_SUPABASE_URL, }); const { - data: { session }, - } = await supabase.auth.getSession(); - const accessToken = session?.access_token; - const refreshToken = session?.refresh_token; - const expiresAt = session?.expires_at; - - if (accessToken) { - console.log('🔐 Figma: User is authenticated. Exchanging key...'); + data: { user }, + } = await supabase.auth.getUser(); + + if (user) { + console.log('🔐 Figma: User is authenticated. Exchanging api key...'); const keyValue = await kv.getdel(figmaKey); if (keyValue) { + const apiKey = await createApiKey({ + supabase, + userId: user.id, + description: `Figma API Key - ${new Date().toISOString()}`, + }); if ( - accessToken && - refreshToken && - expiresAt && + apiKey.status === 'success' && + apiKey.apiKey && (await kv.set( keyValue.readKey, - { accessToken, refreshToken, expiresAt }, + { apiKey: apiKey.apiKey }, { px: 5 * 60 * 1000, // Set the 5 minutes expire time, in milliseconds (a positive integer). nx: true, // Only set the key if it does not already exist. @@ -88,13 +90,10 @@ export const figmaMiddleware: MiddlewareFactory = supabaseUrl: clientEnv.NEXT_PUBLIC_SUPABASE_URL, }); const { - data: { session }, - } = await supabase.auth.getSession(); - const accessToken = session?.access_token; - const refreshToken = session?.refresh_token; - const expiresAt = session?.expires_at; + data: { user }, + } = await supabase.auth.getUser(); - if (!accessToken) { + if (!user) { console.log('🔐 Figma: User is not authenticated. Skipping.'); return middleware(request, event, response); } @@ -102,13 +101,17 @@ export const figmaMiddleware: MiddlewareFactory = const keyValue = await kv.getdel(figmaKey); if (keyValue) { + const apiKey = await createApiKey({ + supabase, + userId: user.id, + description: `Figma API Key - ${new Date().toISOString()}`, + }); if ( - accessToken && - refreshToken && - expiresAt && + apiKey.status === 'success' && + apiKey.apiKey && (await kv.set( keyValue.readKey, - { accessToken, refreshToken, expiresAt }, + { apiKey: apiKey.apiKey }, { px: 5 * 60 * 1000, // Set the 5 minutes expire time, in milliseconds (a positive integer). nx: true, // Only set the key if it does not already exist. diff --git a/apps/dashboard/src/types/kv-types.ts b/apps/dashboard/src/types/kv-types.ts index 58718d4..e76df53 100644 --- a/apps/dashboard/src/types/kv-types.ts +++ b/apps/dashboard/src/types/kv-types.ts @@ -3,9 +3,7 @@ export interface KVCredentialsRead { } export interface KVCredentials { - accessToken: string; - refreshToken: string; - expiresAt: number; + apiKey: string; } export interface KVOAuthState { diff --git a/packages/api/package.json b/packages/api/package.json index e27c463..328ee84 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,9 @@ { "name": "@ds-project/api", "private": true, + "keywords": [], + "license": "ISC", + "author": "", "type": "module", "exports": { ".": { @@ -14,17 +17,18 @@ "./react": { "types": "./dist/react.d.ts", "default": "./src/react.tsx" + }, + "./operations": { + "types": "./dist/operations.d.ts", + "default": "./src/operations/index.ts" } }, - "prettier": "@ds-project/prettier", "scripts": { - "lint": "eslint", "format": "prettier --check . --ignore-path ../../.gitignore --ignore-path ../../.prettierignore", + "lint": "eslint", "type-check": "tsc --noEmit --emitDeclarationOnly false" }, - "keywords": [], - "author": "", - "license": "ISC", + "prettier": "@ds-project/prettier", "dependencies": { "@ds-project/auth": "workspace:*", "@ds-project/database": "workspace:*", diff --git a/packages/api/src/operations/create-api-key.ts b/packages/api/src/operations/create-api-key.ts new file mode 100644 index 0000000..06fcc35 --- /dev/null +++ b/packages/api/src/operations/create-api-key.ts @@ -0,0 +1,16 @@ +import type { createServerClient } from '@ds-project/auth/server'; +import type { Database } from '@ds-project/database'; +import { KeyHippo } from 'keyhippo'; + +export async function createApiKey({ + supabase, + userId, + description, +}: { + supabase: ReturnType>; + userId: string; + description: string; +}): Promise>> { + const keyHippo = new KeyHippo(supabase); + return await keyHippo.createApiKey(userId, description); +} diff --git a/packages/api/src/operations/index.ts b/packages/api/src/operations/index.ts new file mode 100644 index 0000000..a4118eb --- /dev/null +++ b/packages/api/src/operations/index.ts @@ -0,0 +1,2 @@ +export * from './create-api-key'; +export * from './release'; diff --git a/packages/api/src/router/api-keys.ts b/packages/api/src/router/api-keys.ts index 1819144..8f55b71 100644 --- a/packages/api/src/router/api-keys.ts +++ b/packages/api/src/router/api-keys.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createTRPCRouter, protectedProcedure } from '../trpc'; import { KeyHippo } from 'keyhippo'; +import { createApiKey } from '../operations/create-api-key'; export const apiKeysRouter = createTRPCRouter({ list: protectedProcedure @@ -27,8 +28,11 @@ export const apiKeysRouter = createTRPCRouter({ }) ) .mutation(async ({ ctx, input }) => { - const keyHippo = new KeyHippo(ctx.supabase); - return await keyHippo.createApiKey(ctx.account.userId, input.description); + return createApiKey({ + supabase: ctx.supabase, + userId: ctx.account.userId, + description: input.description, + }); }), revoke: protectedProcedure diff --git a/packages/api/src/router/projects.ts b/packages/api/src/router/projects.ts index cce2d50..677a6d9 100644 --- a/packages/api/src/router/projects.ts +++ b/packages/api/src/router/projects.ts @@ -1,6 +1,6 @@ import { eq } from '@ds-project/database'; -import { createTRPCRouter, protectedProcedure } from '../trpc'; +import { apiProcedure, createTRPCRouter, protectedProcedure } from '../trpc'; import { AccountsToProjects, Projects } from '@ds-project/database/schema'; export const projectsRouter = createTRPCRouter({ @@ -15,7 +15,7 @@ export const projectsRouter = createTRPCRouter({ }); }), - account: protectedProcedure.query(async ({ ctx }) => { + account: apiProcedure.query(async ({ ctx }) => { return ctx.database .select({ id: Projects.id, diff --git a/packages/api/src/router/resources.ts b/packages/api/src/router/resources.ts index df50518..d0d3ee9 100644 --- a/packages/api/src/router/resources.ts +++ b/packages/api/src/router/resources.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { eq } from '@ds-project/database'; -import { createTRPCRouter, protectedProcedure } from '../trpc'; +import { apiProcedure, createTRPCRouter, protectedProcedure } from '../trpc'; import { InsertResourcesSchema, PreprocessedTokensSchema, @@ -47,7 +47,7 @@ export const resourcesRouter = createTRPCRouter({ return ctx.database.insert(Resources).values(input); }), - updateDesignTokens: protectedProcedure + updateDesignTokens: apiProcedure .input( z.object({ name: z.string(), diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index e3e875a..cdf0fba 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -9,7 +9,7 @@ import { initTRPC, TRPCError } from '@trpc/server'; import SuperJSON from 'superjson'; import { ZodError } from 'zod'; -import { createServerClient, validateToken } from '@ds-project/auth/server'; +import { createServerClient } from '@ds-project/auth/server'; // import type { Session } from '@acme/auth'; // import { auth, validateToken } from '@acme/auth'; @@ -18,21 +18,7 @@ import { eq } from '@ds-project/database'; import type { Account } from '@ds-project/database/schema'; import type { Database } from '@ds-project/database'; import type { DSContext } from './types/context'; - -/** - * Isomorphic Session getter for API requests - * - Expo requests will have a session token in the Authorization header - * - Next.js requests will have a session token in cookies - */ -async function isomorphicGetUser(authToken: string | null) { - if (authToken) return validateToken(authToken); - - const authClient = createServerClient(); - const { - data: { user }, - } = await authClient.auth.getUser(); - return user; -} +import { KeyHippo } from 'keyhippo'; /** * 1. CONTEXT @@ -51,23 +37,28 @@ export const createTRPCContext = async (opts: { account: Account | null; }) => { const supabase = createServerClient(); - const token = opts.headers.get('Authorization') ?? null; - const user = await isomorphicGetUser(token); + const keyHippo = new KeyHippo(supabase); + const { userId } = await keyHippo.authenticate({ + headers: opts.headers, + } as Request); const source = opts.headers.get('x-trpc-source') ?? 'unknown'; - console.log(`>>> tRPC Request from ${source} by ${user?.id}`); + console.log(`>>> tRPC Request from ${source} by ${userId}`); - const account = user?.id + const account = userId ? ((await database.query.Accounts.findFirst({ - where: (accounts) => eq(accounts.userId, user.id), + where: (accounts) => eq(accounts.userId, userId), })) ?? null) : null; + const { + data: { user }, + } = await supabase.auth.getUser(); + return { supabase, - user, database, - token, + user, account, }; }; @@ -163,3 +154,18 @@ export const protectedProcedure = t.procedure } as DSContext, }); }); + +export const apiProcedure = t.procedure + .use(timingMiddleware) + .use(({ ctx, next }) => { + if (!ctx.account) { + throw new TRPCError({ code: 'UNAUTHORIZED' }); + } + + return next({ + ctx: { + ...ctx, + account: ctx.account, + } as DSContext, + }); + }); diff --git a/packages/auth/package.json b/packages/auth/package.json index 9bd5a79..5bf18dc 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,9 @@ { "name": "@ds-project/auth", "private": true, + "keywords": [], + "license": "ISC", + "author": "", "type": "module", "exports": { "./client": { @@ -16,18 +19,12 @@ "default": "./src/server/index.ts" } }, - "prettier": "@ds-project/prettier", "scripts": { - "lint": "eslint", "format": "prettier --check . --ignore-path ../../.gitignore --ignore-path ../../.prettierignore", + "lint": "eslint", "type-check": "tsc --noEmit --emitDeclarationOnly false" }, - "keywords": [], - "author": "", - "license": "ISC", - "dependecies": { - "@ds-project/database": "workspace:*" - }, + "prettier": "@ds-project/prettier", "dependencies": { "@next/env": "^14.2.5", "@supabase/ssr": "^0.4.0", @@ -44,5 +41,8 @@ "eslint": "catalog:", "prettier": "catalog:", "typescript": "catalog:" + }, + "dependecies": { + "@ds-project/database": "workspace:*" } } diff --git a/packages/figma-utilities/src/credentials.ts b/packages/figma-utilities/src/credentials.ts index 2f7bfa2..1232f92 100644 --- a/packages/figma-utilities/src/credentials.ts +++ b/packages/figma-utilities/src/credentials.ts @@ -1,13 +1,9 @@ import z from 'zod'; export interface Credentials { - accessToken: string; - refreshToken: string; - expiresAt: number; + apiKey: string; } export const CredentialsSchema = z.object({ - accessToken: z.string(), - refreshToken: z.string(), - expiresAt: z.number(), + apiKey: z.string().min(1), }); diff --git a/packages/figma-widget/package.json b/packages/figma-widget/package.json index 9fd3187..cc99cd8 100644 --- a/packages/figma-widget/package.json +++ b/packages/figma-widget/package.json @@ -31,7 +31,6 @@ "react-dom": "catalog:", "style-dictionary": "^4.1.0", "superjson": "^2.2.1", - "trpc-token-refresh-link": "^0.5.0", "zod": "^3.23.8" }, "devDependencies": { diff --git a/packages/figma-widget/src/ui/modules/providers/api-provider.tsx b/packages/figma-widget/src/ui/modules/providers/api-provider.tsx index 1251223..e486e77 100644 --- a/packages/figma-widget/src/ui/modules/providers/api-provider.tsx +++ b/packages/figma-widget/src/ui/modules/providers/api-provider.tsx @@ -1,39 +1,11 @@ -import { useMemo } from 'react'; import { TRPCReactProvider } from '@ds-project/api/react'; import { useAuth } from './auth-provider'; -import { tokenRefreshLink } from 'trpc-token-refresh-link'; -import type { AppRouter } from '../../../../../api/src/app-router'; export function ApiProvider({ children }: { children: React.ReactNode }) { - const { credentials, refreshAccessToken, logout } = useAuth(); - - const authTrpcLink = useMemo( - () => - tokenRefreshLink({ - tokenRefreshNeeded: () => { - if (!credentials) { - return false; - } - - return credentials.expiresAt < Math.floor(Date.now() / 1000); - }, - fetchAccessToken: async () => { - try { - void refreshAccessToken(); - } catch (error) { - await logout(); - } - }, - }), - [credentials, logout, refreshAccessToken] - ); + const { credentials } = useAuth(); return ( - + {children} ); diff --git a/packages/figma-widget/src/ui/modules/providers/auth-provider.tsx b/packages/figma-widget/src/ui/modules/providers/auth-provider.tsx index 3e378cc..2911dfb 100644 --- a/packages/figma-widget/src/ui/modules/providers/auth-provider.tsx +++ b/packages/figma-widget/src/ui/modules/providers/auth-provider.tsx @@ -24,7 +24,6 @@ interface ContextType { | 'authorized' | 'unauthorized' | 'failed'; - refreshAccessToken: () => Promise; login: () => Promise; logout: () => void; } @@ -32,7 +31,6 @@ interface ContextType { const Context = createContext({ credentials: null, state: 'initializing', - refreshAccessToken: () => Promise.resolve(), login: () => Promise.resolve(null), // eslint-disable-next-line @typescript-eslint/no-empty-function logout: () => {}, @@ -77,40 +75,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setShouldUpdatePlugin(false); }, [credentials, shouldUpdatePlugin, state]); - const refreshAccessToken = useCallback(async () => { - if (!credentials) { - setCredentials(null); - setState('unauthorized'); - setShouldUpdatePlugin(true); - return; - } - - const response = await fetch(`${config.AUTH_API_HOST}/api/auth/refresh`, { - method: 'POST', - body: JSON.stringify({ refreshToken: credentials.refreshToken }), - }); - - if (!response.ok) { - setCredentials(null); - setState('unauthorized'); - setShouldUpdatePlugin(true); - return; - } - - const { data: _credentials, success: areCredentialsValid } = - CredentialsSchema.safeParse(await response.json()); - - if (areCredentialsValid) { - setCredentials(_credentials); - setState('authorized'); - setShouldUpdatePlugin(true); - } else { - setCredentials(null); - setState('unauthorized'); - setShouldUpdatePlugin(true); - } - }, [credentials]); - const logout = useCallback(() => { setCredentials(null); setState('unauthorized'); @@ -176,11 +140,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { () => ({ state, credentials, - refreshAccessToken, login, logout, }), - [credentials, login, logout, refreshAccessToken, state] + [credentials, login, logout, state] ); return {children}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6cec8c..5e2706b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,57 +12,27 @@ catalogs: '@figma/widget-typings': specifier: ^1.9.1 version: 1.9.1 - '@trpc/react-query': - specifier: 11.0.0-rc.482 - version: 11.0.0-rc.482 - '@types/node': - specifier: ^22.4.1 - version: 22.4.1 '@types/react': specifier: ^18.3.3 version: 18.3.3 '@types/react-dom': specifier: ^18.3.0 version: 18.3.0 - '@vitejs/plugin-react': - specifier: ^4.3.1 - version: 4.3.1 - concurrently: - specifier: ^8.2.2 - version: 8.2.2 eslint: specifier: ^8.57.0 version: 8.57.0 - next: - specifier: ^14.2.6 - version: 14.2.6 - postcss: - specifier: ^8.4.39 - version: 8.4.41 - prettier: - specifier: ^3.3.3 - version: 3.3.3 react: specifier: ^18.3.1 version: 18.3.1 react-dom: specifier: ^18.3.1 version: 18.3.1 - style-dictionary: - specifier: ^4.0.1 - version: 4.1.0 - tailwindcss: - specifier: ^3.4.7 - version: 3.4.7 typescript: specifier: ^5.5.4 version: 5.5.4 vite: specifier: ^5.3.1 version: 5.3.3 - zod: - specifier: ^3.23.8 - version: 3.23.8 overrides: '@trpc/client': 11.0.0-rc.482 @@ -697,9 +667,6 @@ importers: superjson: specifier: ^2.2.1 version: 2.2.1 - trpc-token-refresh-link: - specifier: ^0.5.0 - version: 0.5.0 zod: specifier: ^3.23.8 version: 3.23.8 @@ -5254,9 +5221,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -6509,14 +6473,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-queue@7.4.1: - resolution: {integrity: sha512-vRpMXmIkYF2/1hLBKisKeVYJZ8S2tZ0zEAmIJgdVKP2nq0nh4qCdf8bgw+ZgKrkh71AOCaqzwbJJk1WtdcF3VA==} - engines: {node: '>=12'} - - p-timeout@5.1.0: - resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==} - engines: {node: '>=12'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -7534,9 +7490,6 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - trpc-token-refresh-link@0.5.0: - resolution: {integrity: sha512-yXPrGjYnXafDfjOYV29bcHYlfMmCnpCFhrDSI5fxJJlYamClHhyTgHjd+VHov/EJvwVu4Z+cp7FgIkL7Q6rKuQ==} - ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -13275,8 +13228,6 @@ snapshots: etag@1.8.1: {} - eventemitter3@5.0.1: {} - events@3.3.0: {} execa@5.1.1: @@ -14573,13 +14524,6 @@ snapshots: dependencies: p-limit: 3.1.0 - p-queue@7.4.1: - dependencies: - eventemitter3: 5.0.1 - p-timeout: 5.1.0 - - p-timeout@5.1.0: {} - p-try@2.2.0: {} package-json-from-dist@1.0.0: {} @@ -15768,13 +15712,6 @@ snapshots: tree-kill@1.2.2: {} - trpc-token-refresh-link@0.5.0: - dependencies: - '@trpc/client': 11.0.0-rc.482(@trpc/server@11.0.0-rc.482) - '@trpc/server': 11.0.0-rc.482 - date-fns: 2.30.0 - p-queue: 7.4.1 - ts-api-utils@1.3.0(typescript@5.5.4): dependencies: typescript: 5.5.4