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
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ npm install

3. To expose your app server using an HTTP tunnel, install [ngrok](https://www.npmjs.com/package/ngrok#usage) globally, then start the ngrok service.

Starting a local HTTP tunnel with ngrok requires you to create an [ngrok account](https://dashboard.ngrok.com/signup) and add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) to the ngrok config file.

```shell
ngrok config add-authtoken $YOUR_AUTHTOKEN
```

You can use `npm` to install ngrok:

```shell
Expand Down Expand Up @@ -57,29 +63,29 @@ cp .env-sample .env

6. In the `.env` file, replace the `CLIENT_ID` and `CLIENT_SECRET` variables with the API account credentials in the app profile. To locate the credentials, find the app's profile in the [Developer Portal](https://devtools.bigcommerce.com/my/apps), then click **View Client ID**.

7. In the `.env` file, update the `AUTH_CALLBACK` variable with the `ngrok_url` from step 4.
7. In the `.env` file, update the `AUTH_CALLBACK` variable with the auth callback URL from step 4.

8. In the `.env` file, enter a secret `JWT_KEY`. To support HS256 encryption, the JWT key must be at least 32 random characters (256 bits).

9. **Configure the data store.** In the `.env` file, specify the `DB_TYPE`.
9. **Configure the data store.** This project was written to use [Firebase](https://firebase.google.com/) or [MySQL](https://www.mysql.com/)

> The DB type must be either `firebase` or `mysql`.
In the `.env` file, specify the `DB_TYPE`.

If using Firebase, supply the `FIRE_` config keys listed in the `.env` file. See the [Firebase quickstart (Google)](https://firebase.google.com/docs/firestore/quickstart).
If using Firebase, copy the contents of your Service Account JSON key file into the `sample-firebase-keys.json` file. This file can be generated by:
1. Creating a new project in Firebase
2. Adding a Cloud Firestore
3. And generating a new Private Key under Project Settings > Service Accounts
See the [Firebase quickstart (Google)](https://firebase.google.com/docs/firestore/quickstart) for more detailed information.

If using MySQL, supply the `MYSQL_` config keys listed in the `.env` file, then do the initial database migration by running the following npm script:

```shell
npm run db:setup
```
If using MySQL, supply the `MYSQL_` config keys listed in the `.env` file, then do the initial database migration by running the following npm script: `npm run db:setup`

10. Start your dev environment in a dedicated terminal session, **separate from `ngrok`**.

```shell
npm run dev
```

> If `ngrok` expires, update the callbacks in steps 4 and 7 with the new `ngrok_url`. You can learn more about [persisting ngrok tunnels longer (ngrok)](https://ngrok.com/docs/getting-started/#step-3-connect-your-agent-to-your-ngrok-account).
> If you relaunch `ngrok`, update the callbacks in steps 4 and 7 with the new `ngrok_url`. You can learn more about [persisting ngrok tunnels longer (ngrok)](https://ngrok.com/docs/getting-started/#step-3-connect-your-agent-to-your-ngrok-account).

11. Consult our developer documentation to [install and launch the app](https://developer.bigcommerce.com/api-docs/apps/quick-start#install-the-app).

Expand Down
4 changes: 2 additions & 2 deletions lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export function setSession(session: SessionProps) {

export async function getSession({ query: { context = '' } }: NextApiRequest) {
if (typeof context !== 'string') return;
const { context: storeHash, user } = decodePayload(context);
const hasUser = await db.hasStoreUser(storeHash, user?.id);
const { context: storeHash, user } = decodePayload(context) as SessionProps;
const hasUser = await db.hasStoreUser(storeHash, String(user?.id));

// Before retrieving session/ hitting APIs, check user
if (!hasUser) {
Expand Down
53 changes: 24 additions & 29 deletions lib/dbs/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { initializeApp } from 'firebase/app';
import { deleteDoc, doc, getDoc, getFirestore, setDoc, updateDoc } from 'firebase/firestore';
import { cert, getApp, getApps, initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import serviceAccount from '../../sample-firebase-keys.json';
import { SessionProps, UserData } from '../../types';

// Firebase config and initialization
// Prod applications might use config file
const { FIRE_API_KEY, FIRE_DOMAIN, FIRE_PROJECT_ID } = process.env;
const firebaseConfig = {
apiKey: FIRE_API_KEY,
authDomain: FIRE_DOMAIN,
projectId: FIRE_PROJECT_ID,
};
const app = initializeApp(firebaseConfig);
const app = getApps().length
? getApp()
: initializeApp({ credential: cert(serviceAccount as any) });
const db = getFirestore(app);

// Firestore data management functions
Expand All @@ -20,14 +15,14 @@ export async function setUser({ user }: SessionProps) {
if (!user) return null;

const { email, id, username } = user;
const ref = doc(db, 'users', String(id));
const ref = db.collection('user').doc(String(id));
const data: UserData = { email };

if (username) {
data.username = username;
}

await setDoc(ref, data, { merge: true });
await ref.set(data, {merge: true });
}

export async function setStore(session: SessionProps) {
Expand All @@ -41,10 +36,10 @@ export async function setStore(session: SessionProps) {
if (!accessToken || !scope) return null;

const storeHash = context?.split('/')[1] || '';
const ref = doc(db, 'store', storeHash);
const ref = db.collection('store').doc(storeHash);
const data = { accessToken, adminId: id, scope };

await setDoc(ref, data);
await ref.set(data);
}

// User management for multi-user apps
Expand All @@ -62,22 +57,22 @@ export async function setStoreUser(session: SessionProps) {
const contextString = context ?? sub;
const storeHash = contextString?.split('/')[1] || '';
const documentId = `${userId}_${storeHash}`; // users can belong to multiple stores
const ref = doc(db, 'storeUsers', documentId);
const storeUser = await getDoc(ref);
const ref = db.collection('storeUsers').doc(documentId);
const storeUser = await ref.get();

// Set admin (store owner) if installing/ updating the app
// https://developer.bigcommerce.com/api-docs/apps/guide/users
if (accessToken) {
// Create a new admin user if none exists
if (!storeUser.exists()) {
await setDoc(ref, { storeHash, isAdmin: true });
if (!storeUser.exists) {
await ref.set({ storeHash, isAdmin: true });
} else if (!storeUser.data()?.isAdmin) {
await updateDoc(ref, { isAdmin: true });
await ref.update({ isAdmin: true });
}
} else {
// Create a new user if it doesn't exist
if (!storeUser.exists()) {
await setDoc(ref, { storeHash, isAdmin: owner.id === userId }); // isAdmin true if owner == user
if (!storeUser.exists) {
await ref.set({ storeHash, isAdmin: owner.id === userId });
}
}
}
Expand All @@ -86,29 +81,29 @@ export async function deleteUser({ context, user, sub }: SessionProps) {
const contextString = context ?? sub;
const storeHash = contextString?.split('/')[1] || '';
const docId = `${user?.id}_${storeHash}`;
const ref = doc(db, 'storeUsers', docId);
const ref = db.collection('storeUsers').doc(docId);

await deleteDoc(ref);
await ref.delete();
}

export async function hasStoreUser(storeHash: string, userId: string) {
if (!storeHash || !userId) return false;

const docId = `${userId}_${storeHash}`;
const userDoc = await getDoc(doc(db, 'storeUsers', docId));
const userDoc = await db.collection('storeUsers').doc(docId).get();

return userDoc.exists();
return userDoc.exists;
}

export async function getStoreToken(storeHash: string) {
if (!storeHash) return null;
const storeDoc = await getDoc(doc(db, 'store', storeHash));
const storeDoc = await db.collection('store').doc(storeHash).get();

return storeDoc.data()?.accessToken ?? null;
}

export async function deleteStore({ store_hash: storeHash }: SessionProps) {
const ref = doc(db, 'store', storeHash);
const ref = db.collection('store').doc(storeHash);

await deleteDoc(ref);
await ref.delete();
}
Loading