diff --git a/src/app/api/app/load/route.ts b/src/app/api/app/load/route.ts index b66f5ff..11bbb6d 100644 --- a/src/app/api/app/load/route.ts +++ b/src/app/api/app/load/route.ts @@ -2,6 +2,7 @@ import jwt from 'jsonwebtoken'; import { z } from 'zod'; import { NextResponse, type NextRequest } from 'next/server'; import { env } from '~/env.mjs'; +import * as db from '~/lib/db'; const queryParamSchema = z.object({ signed_payload_jwt: z.string(), @@ -28,11 +29,11 @@ const jwtSchema = z.object({ channel_id: z.number().nullable(), }); -export function GET(request: NextRequest) { - function appendAuthToken(url: string, authToken: string): string { +export async function GET(request: NextRequest) { + function appendExchangeToken(url: string, token: string): string { const delimiter = new URL(url, env.APP_ORIGIN).search ? '&' : '?'; - return `${url}${delimiter}authToken=${authToken}`; + return `${url}${delimiter}exchangeToken=${token}`; } const parsedParams = queryParamSchema.safeParse( @@ -62,7 +63,9 @@ export function GET(request: NextRequest) { expiresIn: 3600, }); - return NextResponse.redirect(new URL(appendAuthToken(path, clientToken), env.APP_ORIGIN), { + const exchangeToken = await db.saveClientToken(clientToken); + + return NextResponse.redirect(new URL(appendExchangeToken(path, exchangeToken), env.APP_ORIGIN), { status: 302, statusText: 'Found', }); diff --git a/src/app/api/generateDescription/route.ts b/src/app/api/generateDescription/route.ts index 655a21e..4414fa8 100644 --- a/src/app/api/generateDescription/route.ts +++ b/src/app/api/generateDescription/route.ts @@ -4,7 +4,7 @@ import { aiSchema } from './schema'; import { authorize } from '~/lib/authorize'; export async function POST(req: NextRequest) { - const authToken = req.nextUrl.searchParams.get('authToken') || 'missing'; + const authToken = req.headers.get('X-Auth-Token') || 'missing'; if (!authorize(authToken)) { return new NextResponse('Unauthorized', { status: 401 }); diff --git a/src/app/productDescription/[productId]/form.tsx b/src/app/productDescription/[productId]/form.tsx index a6b87d0..ee53227 100644 --- a/src/app/productDescription/[productId]/form.tsx +++ b/src/app/productDescription/[productId]/form.tsx @@ -43,13 +43,14 @@ export default function Form({ const handleGenerateDescription = async () => { setIsLoading(true); - const res = await fetch(`/api/generateDescription?authToken=${authToken}`, { + const res = await fetch(`/api/generateDescription`, { method: 'POST', body: JSON.stringify( prepareAiPromptAttributes(currentAttributes, product) ), headers: { 'X-CSRF-Token': csrfToken, + 'X-Auth-Token': authToken, }, }); diff --git a/src/app/productDescription/[productId]/page.tsx b/src/app/productDescription/[productId]/page.tsx index 8e77777..d5c2cfa 100644 --- a/src/app/productDescription/[productId]/page.tsx +++ b/src/app/productDescription/[productId]/page.tsx @@ -6,12 +6,14 @@ import { headers } from 'next/headers'; interface PageProps { params: { productId: string }; - searchParams: { product_name: string; authToken: string }; + searchParams: { product_name: string; exchangeToken: string }; } export default async function Page(props: PageProps) { const { productId } = props.params; - const { product_name: name, authToken } = props.searchParams; + const { product_name: name, exchangeToken } = props.searchParams; + + const authToken = await db.getClientTokenMaybeAndDelete(exchangeToken) || 'missing'; const authorized = authorize(authToken); diff --git a/src/lib/db.ts b/src/lib/db.ts index 10f2d00..4fa9a7b 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,10 +1,12 @@ -import { initializeApp } from 'firebase/app'; +import { FirebaseApp, initializeApp } from 'firebase/app'; import { deleteDoc, doc, getDoc, getFirestore, setDoc, + Timestamp, + Firestore } from 'firebase/firestore'; import { env } from '~/env.mjs'; @@ -25,16 +27,29 @@ export interface AuthProps { user: User; } -const { FIRE_API_KEY, FIRE_DOMAIN, FIRE_PROJECT_ID } = env; +export interface ClientTokenData { + token: string; + expiresAt: Timestamp; +} + +let app: FirebaseApp; +let db: Firestore; + +function getDb() { + if (!db) { + const { FIRE_API_KEY, FIRE_DOMAIN, FIRE_PROJECT_ID } = env; -const firebaseConfig = { - apiKey: FIRE_API_KEY, - authDomain: FIRE_DOMAIN, - projectId: FIRE_PROJECT_ID, -}; + const firebaseConfig = { + apiKey: FIRE_API_KEY, + authDomain: FIRE_DOMAIN, + projectId: FIRE_PROJECT_ID, + }; -const app = initializeApp(firebaseConfig); -const db = getFirestore(app); + app = initializeApp(firebaseConfig); + db = getFirestore(app); + } + return db; +} // Firestore data management functions @@ -43,7 +58,7 @@ export async function setUser(user: User) { if (!user) return Promise.resolve(); const { email, id, username } = user; - const ref = doc(db, 'users', String(id)); + const ref = doc(getDb(), 'users', String(id)); const data: UserData = { email }; if (username) { @@ -64,7 +79,7 @@ export async function setStore(props: AuthProps) { if (!accessToken || !scope) return null; const storeHash = context?.split('/')[1] || ''; - const ref = doc(db, 'store', storeHash); + const ref = doc(getDb(), 'store', storeHash); const data = { accessToken, adminId: id, scope }; await setDoc(ref, data); @@ -82,26 +97,66 @@ export async function setStoreUser(session: AuthProps) { const storeHash = context.split('/')[1]; const documentId = `${userId}_${storeHash}`; - const ref = doc(db, 'storeUsers', documentId); + const ref = doc(getDb(), 'storeUsers', documentId); await setDoc(ref, { storeHash }); } export async function deleteUser(storeHash: string, user: User) { const docId = `${user.id}_${storeHash}`; - const ref = doc(db, 'storeUsers', docId); + const ref = doc(getDb(), 'storeUsers', docId); await deleteDoc(ref); } export async function getStoreToken(storeHash: string): Promise { if (!storeHash) return null; - const storeDoc = await getDoc(doc(db, 'store', storeHash)); + const storeDoc = await getDoc(doc(getDb(), 'store', storeHash)); return storeDoc.data()?.accessToken; } export async function deleteStore(storeHash: string) { - const ref = doc(db, 'store', storeHash); + const ref = doc(getDb(), 'store', storeHash); await deleteDoc(ref); } + +export async function saveClientToken(clientToken: string): Promise { + if (!clientToken) { + throw new Error('A clientToken is required to create an exchange token.'); + } + + const exchangeToken = crypto.randomUUID(); + const expiresAt = Timestamp.fromMillis(Date.now() + 120 * 1000); // 2 minutes from now + + const ref = doc(getDb(), 'exchangeTokens', exchangeToken); + const data: Omit & { expiresAt: Timestamp } = { + token: clientToken, + expiresAt + }; + + await setDoc(ref, data); + + return exchangeToken; +} + +export async function getClientTokenMaybeAndDelete(exchangeToken: string): Promise { + const ref = doc(getDb(), 'exchangeTokens', exchangeToken); + const docSnap = await getDoc(ref); + + await deleteDoc(ref); + + if (!docSnap.exists()) { + return false; + } + + const data = docSnap.data() as ClientTokenData; + const now = Timestamp.now(); + + if (now > data.expiresAt) { + console.warn('Exchange token expired:', exchangeToken); + return false; + } + + return data.token; +}