first commit
This commit is contained in:
@@ -0,0 +1,104 @@
|
|||||||
|
# Gmail Reader
|
||||||
|
|
||||||
|
Monorepo with a React + Vite + DaisyUI client, a Fastify API, and a Gmail push watcher. The API owns Google OAuth, stores tokens in SQLite, and calls the Gmail API with `gmail.readonly`. The browser only receives an HTTP-only session cookie. The watcher reuses those tokens, calls `users.watch`, and runs each new INBOX message through a handler pipeline.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 20+
|
||||||
|
- A Google Cloud OAuth **Web application** client with:
|
||||||
|
- Scope: `https://www.googleapis.com/auth/gmail.readonly`
|
||||||
|
- Authorized JavaScript origin: `https://oauth.0dev.web.id` (and `http://localhost:5173` for local)
|
||||||
|
- Authorized redirect URIs:
|
||||||
|
- `https://oauth.0dev.web.id/callback`
|
||||||
|
- `http://localhost:5173/callback` (local; Vite proxies this to Fastify)
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Fill in `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, and a long `SESSION_SECRET`. Keep:
|
||||||
|
|
||||||
|
```
|
||||||
|
GOOGLE_REDIRECT_URI=http://localhost:5173/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- UI: http://localhost:5173
|
||||||
|
- API: http://localhost:3000
|
||||||
|
- Watcher (optional): `npm run dev:watcher` → http://localhost:3001
|
||||||
|
|
||||||
|
Vite proxies `/auth`, `/callback`, and `/api` to Fastify so the Google redirect stays on the same origin as the SPA.
|
||||||
|
|
||||||
|
Sign in via the UI at least once so SQLite has a refresh token before starting the watcher.
|
||||||
|
|
||||||
|
## Production (one process)
|
||||||
|
|
||||||
|
Build the frontend, then start Fastify. Fastify serves `apps/web/dist` and handles `/callback` itself.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
NODE_ENV=production GOOGLE_REDIRECT_URI=https://oauth.0dev.web.id/callback npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Point your reverse proxy at the Fastify port (`PORT`, default 3000). Send the whole host to that one process — SPA, `/api`, `/auth`, and `/callback` must share `https://oauth.0dev.web.id`.
|
||||||
|
|
||||||
|
Example Caddy:
|
||||||
|
|
||||||
|
```
|
||||||
|
oauth.0dev.web.id {
|
||||||
|
reverse_proxy 127.0.0.1:3000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
SQLite is stored at `apps/api/data/app.db`. Keep that directory writable and backed up.
|
||||||
|
|
||||||
|
## Gmail push watcher
|
||||||
|
|
||||||
|
Gmail does not POST to you directly. It publishes to **Cloud Pub/Sub**, which push-delivers to `POST /pubsub/gmail`.
|
||||||
|
|
||||||
|
### GCP setup
|
||||||
|
|
||||||
|
1. Enable **Gmail API** and **Cloud Pub/Sub** in the same Google Cloud project as the OAuth client.
|
||||||
|
2. Create a topic, e.g. `projects/YOUR_PROJECT/topics/gmail-push`.
|
||||||
|
3. Grant **Pub/Sub Publisher** on that topic to `serviceAccount:gmail-api-push@system.gserviceaccount.com`.
|
||||||
|
4. Create a **push subscription** whose endpoint is publicly reachable HTTPS, e.g. `https://oauth.0dev.web.id/pubsub/gmail?token=YOUR_TOKEN`.
|
||||||
|
5. Put the topic name and token in `.env` (`GOOGLE_PUBSUB_TOPIC`, `PUBSUB_VERIFICATION_TOKEN`).
|
||||||
|
|
||||||
|
Start after a user has signed in through the web app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start:watcher
|
||||||
|
```
|
||||||
|
|
||||||
|
Example Caddy (API on 3000, watcher on 3001):
|
||||||
|
|
||||||
|
```
|
||||||
|
oauth.0dev.web.id {
|
||||||
|
handle /pubsub/gmail* {
|
||||||
|
reverse_proxy 127.0.0.1:3001
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
reverse_proxy 127.0.0.1:3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The watcher renews `users.watch` on startup and every 12 hours. Handlers run in sequence per new message: log the event, then fetch and log the message body. A handler may pass fields to the next one or `break` the chain.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- Sign in with Google (server-side authorization code flow)
|
||||||
|
- List latest mail
|
||||||
|
- Search with Gmail query syntax (`from:`, `subject:`, `newer_than:`, …)
|
||||||
|
- Open a message and read decoded plain-text content
|
||||||
|
- Watch INBOX via Gmail push (Pub/Sub) and run a sequential handler pipeline (log event, then fetch and log the message)
|
||||||
|
- Watch INBOX via Gmail push (Pub/Sub) and run a sequential handler pipeline (log event, then fetch and log the message)
|
||||||
Reference in New Issue
Block a user