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:
@@ -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),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user