idimitrov.dev/_content/cases/stepsy.wiki.md

147 lines
5.0 KiB
Markdown
Raw Normal View History

2023-11-11 15:21:23 +01:00
---
title: Multi-tenant knowledge base website based on Google APIs
goal: Create a modern multi-tenant web app that lets users use their Google Drive as a knowledge base
role: Design and implement the web app
date: Jul 29, 2023 - Nov 5, 2023
z: 3
2023-11-11 15:21:23 +01:00
---
2023-11-19 09:36:10 +01:00
This project aims to be a Google Drive frontend. It uses the Google APIs to fetch document data and display that data in
a wiki-style web page.
2023-11-17 11:17:37 +01:00
### [Demo page](https://ivan.stepsy.wiki/space/spc)
(website not live yet.. waiting for client)
![thumbnail](/thumbnail.png)
It supports Google Docs, Google Sheets, Google Slides, PDFs and regular files.
---
### Technical overview
2023-11-11 15:21:23 +01:00
2023-11-19 09:36:10 +01:00
I chose NextJS as the backbone for this project as it offers the greatest amount of flexibility while still being very
powerful both on the client as well as on the server with an active community and thriving ecosystem.
2023-11-11 15:21:23 +01:00
2023-11-19 09:36:10 +01:00
For styles I chose TailwindCSS with DaisyUI for the optimizations and development speed that come out of using them.
Tailwind uses purgecss to minimize the final bundle making the page load and feel faster.
2023-11-11 15:21:23 +01:00
The database is PostgreSQL with Prisma ORM running on Vercel's cloud infrastructure.
For authentication I chose NextAuth with JWT as it's the preferred way to handle auth in a NextJS project.
2023-11-19 09:36:10 +01:00
The actual implementation is a lengthy process involving many moving parts and lots of code. I'll go over the three most
challenging problems in no particular order.
2023-11-11 15:21:23 +01:00
2023-11-19 09:36:10 +01:00
Interfacing with Google Drive is done to read the content there and almost never used for writing except for setting and
removing permissions. To read the content the reader must have appropriate permissions and that's determined by the auth
system with a JWT. For each request we can get the JWT and use it in the google client to auth unless it's an anonymous
user, in which case we must use a google service account JWT. This JWT holds a google client access token used by google
in determining permissions. Once the client is set up we can start making drive requests on behalf of the user getting
their drive content inside the web app including folders, files, documents, pictures, shared drives and so on, which can
later be rendered on a page. These requests are a bottleneck, which required many optimizations and concurrency tricks
to make the site considerably faster than the competition.
2023-11-11 15:21:23 +01:00
2023-11-19 09:36:10 +01:00
The storage API uses Prisma ORM for storing and getting all the user info including wikis and spaces. When a user logs
in they can see their wiki as well as all the wikis they are allowed to manage. It's used to handle authorized requests
like changing the wiki/space name, url, permissions and more. Storage is an integral part of any web application.
2023-11-11 15:21:23 +01:00
2023-11-19 09:36:10 +01:00
The UI/UX uses TailwindCSS and DaisyUI to make everything a fast, modern, optimized and intuitive experience with extra
features like dozens of themes as well as a custom theme builder. React was used with TypeScript to provide a nice
modern client-side experience between transitions and interactions. This setup supports maximum optimization as you can
see in the screenshots below allowing the app to reach a lighthouse score of 100 on all but one (it has 99) pages. Both
mobile and desktop is supported.
2023-11-11 15:21:23 +01:00
---
2023-11-11 15:21:23 +01:00
### Google API details
Configure NextAuth for Google:
```ts
export default NextAuth({
...
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorization: {
params: {
access_type: "offline",
prompt: "consent",
scope: "openid profile email https://www.googleapis.com/auth/drive",
},
},
}),
],
...
```
Create an auth client for logged in users
```ts
2023-11-19 09:36:10 +01:00
let authClient = new google.auth.OAuth2(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET);
authClient.setCredentials({
access_token: accessToken, // this comes from the logged in user info
refresh_token: refreshToken, // same for this
});
```
or one for anonymous users using a Google service account
```ts
authClient = new google.auth.JWT({
email: serviceAccount.client_email,
key: serviceAccount.private_key,
scopes: ["https://www.googleapis.com/auth/drive"],
});
```
Create the drive client
```ts
const drive = google.drive({
2023-11-19 09:36:10 +01:00
version: "v3",
auth: authClient,
});
```
You can now use this client to query the API
```ts
2023-11-19 09:36:10 +01:00
const file = (await drive.files.get({ fileId })).data;
```
```ts
2023-11-19 09:36:10 +01:00
const folderContents = (await drive.files.list({ q: `'${folderId}' in parents` })).data.files;
```
```ts
2023-11-19 09:36:10 +01:00
const googleDocHtml = (
await drive.files.export({
fileId: googleDocId,
mimeType: "text/html",
})
).data;
```
```ts
const shortcutTarget = await drive.files.get({
2023-11-19 09:36:10 +01:00
fileId,
fields: "shortcutDetails/targetId",
});
2023-11-19 09:36:10 +01:00
const targetId = shortcutTarget.data.shortcutDetails?.targetId;
```
Google doesn't export everything to HTML. They provide document renderers as iFrames.
```tsx
<iframe src={`https://docs.google.com/{{"spreadsheets" or "presentation"}}/d/${docId}/preview`}></iframe>
```
```tsx
// This is used for PDFs or regular text files
<iframe src={`https://drive.google.com/file/d/${docId}/preview`}></iframe>
```