Skip to content

Commit a264f2f

Browse files
authored
Merge pull request #71 from bigcommerce/DATA-12840
refactor: DATA-12840 Avoid passing auth poken as a part of request query params
2 parents 963b748 + 18d9d2c commit a264f2f

5 files changed

Lines changed: 84 additions & 23 deletions

File tree

src/app/api/app/load/route.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import jwt from 'jsonwebtoken';
22
import { z } from 'zod';
33
import { NextResponse, type NextRequest } from 'next/server';
44
import { env } from '~/env.mjs';
5+
import * as db from '~/lib/db';
56

67
const queryParamSchema = z.object({
78
signed_payload_jwt: z.string(),
@@ -28,11 +29,11 @@ const jwtSchema = z.object({
2829
channel_id: z.number().nullable(),
2930
});
3031

31-
export function GET(request: NextRequest) {
32-
function appendAuthToken(url: string, authToken: string): string {
32+
export async function GET(request: NextRequest) {
33+
function appendExchangeToken(url: string, token: string): string {
3334
const delimiter = new URL(url, env.APP_ORIGIN).search ? '&' : '?';
3435

35-
return `${url}${delimiter}authToken=${authToken}`;
36+
return `${url}${delimiter}exchangeToken=${token}`;
3637
}
3738

3839
const parsedParams = queryParamSchema.safeParse(
@@ -62,7 +63,9 @@ export function GET(request: NextRequest) {
6263
expiresIn: 3600,
6364
});
6465

65-
return NextResponse.redirect(new URL(appendAuthToken(path, clientToken), env.APP_ORIGIN), {
66+
const exchangeToken = await db.saveClientToken(clientToken);
67+
68+
return NextResponse.redirect(new URL(appendExchangeToken(path, exchangeToken), env.APP_ORIGIN), {
6669
status: 302,
6770
statusText: 'Found',
6871
});

src/app/api/generateDescription/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { aiSchema } from './schema';
44
import { authorize } from '~/lib/authorize';
55

66
export async function POST(req: NextRequest) {
7-
const authToken = req.nextUrl.searchParams.get('authToken') || 'missing';
7+
const authToken = req.headers.get('X-Auth-Token') || 'missing';
88

99
if (!authorize(authToken)) {
1010
return new NextResponse('Unauthorized', { status: 401 });

src/app/productDescription/[productId]/form.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ export default function Form({
4343

4444
const handleGenerateDescription = async () => {
4545
setIsLoading(true);
46-
const res = await fetch(`/api/generateDescription?authToken=${authToken}`, {
46+
const res = await fetch(`/api/generateDescription`, {
4747
method: 'POST',
4848
body: JSON.stringify(
4949
prepareAiPromptAttributes(currentAttributes, product)
5050
),
5151
headers: {
5252
'X-CSRF-Token': csrfToken,
53+
'X-Auth-Token': authToken,
5354
},
5455
});
5556

src/app/productDescription/[productId]/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { headers } from 'next/headers';
66

77
interface PageProps {
88
params: { productId: string };
9-
searchParams: { product_name: string; authToken: string };
9+
searchParams: { product_name: string; exchangeToken: string };
1010
}
1111

1212
export default async function Page(props: PageProps) {
1313
const { productId } = props.params;
14-
const { product_name: name, authToken } = props.searchParams;
14+
const { product_name: name, exchangeToken } = props.searchParams;
15+
16+
const authToken = await db.getClientTokenMaybeAndDelete(exchangeToken) || 'missing';
1517

1618
const authorized = authorize(authToken);
1719

src/lib/db.ts

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { initializeApp } from 'firebase/app';
1+
import { FirebaseApp, initializeApp } from 'firebase/app';
22
import {
33
deleteDoc,
44
doc,
55
getDoc,
66
getFirestore,
77
setDoc,
8+
Timestamp,
9+
Firestore
810
} from 'firebase/firestore';
911
import { env } from '~/env.mjs';
1012

@@ -25,16 +27,29 @@ export interface AuthProps {
2527
user: User;
2628
}
2729

28-
const { FIRE_API_KEY, FIRE_DOMAIN, FIRE_PROJECT_ID } = env;
30+
export interface ClientTokenData {
31+
token: string;
32+
expiresAt: Timestamp;
33+
}
34+
35+
let app: FirebaseApp;
36+
let db: Firestore;
37+
38+
function getDb() {
39+
if (!db) {
40+
const { FIRE_API_KEY, FIRE_DOMAIN, FIRE_PROJECT_ID } = env;
2941

30-
const firebaseConfig = {
31-
apiKey: FIRE_API_KEY,
32-
authDomain: FIRE_DOMAIN,
33-
projectId: FIRE_PROJECT_ID,
34-
};
42+
const firebaseConfig = {
43+
apiKey: FIRE_API_KEY,
44+
authDomain: FIRE_DOMAIN,
45+
projectId: FIRE_PROJECT_ID,
46+
};
3547

36-
const app = initializeApp(firebaseConfig);
37-
const db = getFirestore(app);
48+
app = initializeApp(firebaseConfig);
49+
db = getFirestore(app);
50+
}
51+
return db;
52+
}
3853

3954
// Firestore data management functions
4055

@@ -43,7 +58,7 @@ export async function setUser(user: User) {
4358
if (!user) return Promise.resolve();
4459

4560
const { email, id, username } = user;
46-
const ref = doc(db, 'users', String(id));
61+
const ref = doc(getDb(), 'users', String(id));
4762
const data: UserData = { email };
4863

4964
if (username) {
@@ -64,7 +79,7 @@ export async function setStore(props: AuthProps) {
6479
if (!accessToken || !scope) return null;
6580

6681
const storeHash = context?.split('/')[1] || '';
67-
const ref = doc(db, 'store', storeHash);
82+
const ref = doc(getDb(), 'store', storeHash);
6883
const data = { accessToken, adminId: id, scope };
6984

7085
await setDoc(ref, data);
@@ -82,26 +97,66 @@ export async function setStoreUser(session: AuthProps) {
8297

8398
const storeHash = context.split('/')[1];
8499
const documentId = `${userId}_${storeHash}`;
85-
const ref = doc(db, 'storeUsers', documentId);
100+
const ref = doc(getDb(), 'storeUsers', documentId);
86101

87102
await setDoc(ref, { storeHash });
88103
}
89104

90105
export async function deleteUser(storeHash: string, user: User) {
91106
const docId = `${user.id}_${storeHash}`;
92-
const ref = doc(db, 'storeUsers', docId);
107+
const ref = doc(getDb(), 'storeUsers', docId);
93108

94109
await deleteDoc(ref);
95110
}
96111

97112
export async function getStoreToken(storeHash: string): Promise<string | null> {
98113
if (!storeHash) return null;
99-
const storeDoc = await getDoc(doc(db, 'store', storeHash));
114+
const storeDoc = await getDoc(doc(getDb(), 'store', storeHash));
100115

101116
return storeDoc.data()?.accessToken;
102117
}
103118

104119
export async function deleteStore(storeHash: string) {
105-
const ref = doc(db, 'store', storeHash);
120+
const ref = doc(getDb(), 'store', storeHash);
106121
await deleteDoc(ref);
107122
}
123+
124+
export async function saveClientToken(clientToken: string): Promise<string> {
125+
if (!clientToken) {
126+
throw new Error('A clientToken is required to create an exchange token.');
127+
}
128+
129+
const exchangeToken = crypto.randomUUID();
130+
const expiresAt = Timestamp.fromMillis(Date.now() + 120 * 1000); // 2 minutes from now
131+
132+
const ref = doc(getDb(), 'exchangeTokens', exchangeToken);
133+
const data: Omit<ClientTokenData, 'expiresAt'> & { expiresAt: Timestamp } = {
134+
token: clientToken,
135+
expiresAt
136+
};
137+
138+
await setDoc(ref, data);
139+
140+
return exchangeToken;
141+
}
142+
143+
export async function getClientTokenMaybeAndDelete(exchangeToken: string): Promise<string | false> {
144+
const ref = doc(getDb(), 'exchangeTokens', exchangeToken);
145+
const docSnap = await getDoc(ref);
146+
147+
await deleteDoc(ref);
148+
149+
if (!docSnap.exists()) {
150+
return false;
151+
}
152+
153+
const data = docSnap.data() as ClientTokenData;
154+
const now = Timestamp.now();
155+
156+
if (now > data.expiresAt) {
157+
console.warn('Exchange token expired:', exchangeToken);
158+
return false;
159+
}
160+
161+
return data.token;
162+
}

0 commit comments

Comments
 (0)