feat: add gmail-hook monorepo with watcher, api, and web

Ship the receipt pipeline (Livin/Grab/BRI), PocketBase spendings, PM2 ecosystem, and a proper .gitignore that excludes secrets, SQLite data, and media dumps.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-13 22:58:47 +07:00
co-authored by Cursor
parent 8fa9f4350e
commit 46a2e2a7b0
67 changed files with 12153 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
import Database from "better-sqlite3";
import { SQLITE_PATH } from "./config.js";
mkdirSync(dirname(SQLITE_PATH), { recursive: true });
const db = new Database(SQLITE_PATH);
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
db.pragma("busy_timeout = 5000");
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
google_sub TEXT NOT NULL UNIQUE,
email TEXT NOT NULL,
name TEXT,
picture TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS oauth_tokens (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
access_token TEXT NOT NULL,
refresh_token TEXT,
expiry INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS watch_state (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
history_id TEXT NOT NULL,
expiration INTEGER NOT NULL
);
`);
const getUserByIdStmt = db.prepare(`SELECT * FROM users WHERE id = ?`);
const getUserByEmailStmt = db.prepare(
`SELECT * FROM users WHERE LOWER(email) = LOWER(?)`,
);
const getTokensStmt = db.prepare(`SELECT * FROM oauth_tokens WHERE user_id = ?`);
const listUsersWithTokensStmt = db.prepare(`
SELECT u.*
FROM users u
INNER JOIN oauth_tokens t ON t.user_id = u.id
`);
const saveTokensStmt = db.prepare(`
INSERT INTO oauth_tokens (user_id, access_token, refresh_token, expiry)
VALUES (@userId, @accessToken, @refreshToken, @expiry)
ON CONFLICT(user_id) DO UPDATE SET
access_token = excluded.access_token,
refresh_token = COALESCE(excluded.refresh_token, oauth_tokens.refresh_token),
expiry = excluded.expiry
`);
const getWatchStateStmt = db.prepare(
`SELECT * FROM watch_state WHERE user_id = ?`,
);
const saveWatchStateStmt = db.prepare(`
INSERT INTO watch_state (user_id, history_id, expiration)
VALUES (@userId, @historyId, @expiration)
ON CONFLICT(user_id) DO UPDATE SET
history_id = excluded.history_id,
expiration = excluded.expiration
`);
export function getUserById(id) {
return getUserByIdStmt.get(id) || null;
}
export function getUserByEmail(email) {
if (!email) return null;
return getUserByEmailStmt.get(email) || null;
}
export function getTokens(userId) {
return getTokensStmt.get(userId) || null;
}
export function listUsersWithTokens() {
return listUsersWithTokensStmt.all();
}
export function saveTokens(userId, { accessToken, refreshToken, expiry }) {
saveTokensStmt.run({
userId,
accessToken,
refreshToken: refreshToken || null,
expiry,
});
}
export function getWatchState(userId) {
return getWatchStateStmt.get(userId) || null;
}
export function saveWatchState(userId, { historyId, expiration }) {
saveWatchStateStmt.run({
userId,
historyId: String(historyId),
expiration: Number(expiration),
});
}