Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/app/api/app/load/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(
Expand Down Expand Up @@ -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',
});
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/generateDescription/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
3 changes: 2 additions & 1 deletion src/app/productDescription/[productId]/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});

Expand Down
6 changes: 4 additions & 2 deletions src/app/productDescription/[productId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
85 changes: 70 additions & 15 deletions src/lib/db.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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

Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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<string | null> {
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<string> {
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<ClientTokenData, 'expiresAt'> & { expiresAt: Timestamp } = {
token: clientToken,
expiresAt
};

await setDoc(ref, data);

return exchangeToken;
}

export async function getClientTokenMaybeAndDelete(exchangeToken: string): Promise<string | false> {
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;
}
Loading