From 9dbd7ddd2ee5c432c8f438da70bd084a36694c7d Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Sat, 24 May 2025 22:50:17 +0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth.ts | 11 ++-- apps/api/src/db/schema/auth-data.ts | 17 ++++++ apps/api/src/db/schema/index.ts | 1 + apps/api/src/lib/drizzle-auth-storage.ts | 66 ++++++++++++++++++++++++ apps/api/src/server.ts | 2 + packages/cli/src/index.ts | 7 +-- readme.md | 18 +------ 7 files changed, 96 insertions(+), 26 deletions(-) create mode 100644 apps/api/src/db/schema/auth-data.ts create mode 100644 apps/api/src/lib/drizzle-auth-storage.ts diff --git a/apps/api/src/auth.ts b/apps/api/src/auth.ts index 4e729da9..31119a39 100644 --- a/apps/api/src/auth.ts +++ b/apps/api/src/auth.ts @@ -2,20 +2,19 @@ import { assert, pick } from '@agentic/platform-core' import { issuer } from '@openauthjs/openauth' import { GithubProvider } from '@openauthjs/openauth/provider/github' import { PasswordProvider } from '@openauthjs/openauth/provider/password' -import { MemoryStorage } from '@openauthjs/openauth/storage/memory' import { PasswordUI } from '@openauthjs/openauth/ui/password' import { type RawUser } from '@/db' import { subjects } from '@/lib/auth/subjects' import { upsertOrLinkUserAccount } from '@/lib/auth/upsert-or-link-user-account' +import { DrizzleAuthStorage } from '@/lib/drizzle-auth-storage' import { env } from '@/lib/env' import { getGitHubClient } from '@/lib/external/github' +// Initialize OpenAuth issuer which is a Hono app for all auth routes. export const authRouter = issuer({ subjects, - storage: MemoryStorage({ - persist: './auth-db-temp.json' - }), + storage: DrizzleAuthStorage(), providers: { github: GithubProvider({ clientID: env.GITHUB_CLIENT_ID, @@ -25,6 +24,7 @@ export const authRouter = issuer({ password: PasswordProvider( PasswordUI({ sendCode: async (email, code) => { + // TODO: Send email code to user // eslint-disable-next-line no-console console.log({ email, code }) } @@ -97,7 +97,8 @@ export const authRouter = issuer({ accountId: value.email }, partialUser: { - email: value.email + email: value.email, + emailVerified: true } }) } else { diff --git a/apps/api/src/db/schema/auth-data.ts b/apps/api/src/db/schema/auth-data.ts new file mode 100644 index 00000000..ea62c8d6 --- /dev/null +++ b/apps/api/src/db/schema/auth-data.ts @@ -0,0 +1,17 @@ +import { jsonb, pgTable, text, timestamp } from '@fisch0920/drizzle-orm/pg-core' + +import { timestamps } from './common' + +// Simple key-value store of JSON data for OpenAuth-related state. +export const authData = pgTable('auth_data', { + // Example ID keys: + // "oauth:refresh\u001fuser:f99d3004946f9abb\u001f2cae301e-3fdc-40c4-8cda-83b25a616d06" + // "signing:key\u001ff001a516-838d-4c88-aa9e-719d8fc9d5a3" + // "email\u001ft@t.com\u001fpassword" + // "encryption:key\u001f14d3c324-f9c7-4867-81a9-b0b77b0db0be" + id: text().primaryKey(), + ...timestamps, + + value: jsonb().$type>().notNull(), + expiry: timestamp() +}) diff --git a/apps/api/src/db/schema/index.ts b/apps/api/src/db/schema/index.ts index 27c8bd4d..8d57233c 100644 --- a/apps/api/src/db/schema/index.ts +++ b/apps/api/src/db/schema/index.ts @@ -1,4 +1,5 @@ export * from './account' +export * from './auth-data' export * from './common' export * from './consumer' export * from './deployment' diff --git a/apps/api/src/lib/drizzle-auth-storage.ts b/apps/api/src/lib/drizzle-auth-storage.ts new file mode 100644 index 00000000..f82dfba9 --- /dev/null +++ b/apps/api/src/lib/drizzle-auth-storage.ts @@ -0,0 +1,66 @@ +import { + joinKey, + splitKey, + type StorageAdapter +} from '@openauthjs/openauth/storage/storage' + +import { and, db, eq, isNull, like, lt, or, schema } from '@/db' + +export function DrizzleAuthStorage(): StorageAdapter { + return { + async get(key: string[]) { + const id = joinKey(key) + const entry = await db.query.authData.findFirst({ + where: eq(schema.authData.id, id) + }) + if (!entry) return undefined + + if (entry.expiry && Date.now() >= entry.expiry.getTime()) { + await db.delete(schema.authData).where(eq(schema.authData.id, id)) + return undefined + } + + return entry.value + }, + + async set(key: string[], value: Record, expiry?: Date) { + const id = joinKey(key) + + await db + .insert(schema.authData) + .values({ + id, + value, + expiry + }) + .onConflictDoUpdate({ + target: schema.authData.id, + set: { + value, + expiry: expiry ?? null + } + }) + }, + + async remove(key: string[]) { + const id = joinKey(key) + await db.delete(schema.authData).where(eq(schema.authData.id, id)) + }, + + async *scan(prefix: string[]) { + const now = new Date() + const idPrefix = joinKey(prefix) + + const entries = await db.query.authData.findMany({ + where: and( + like(schema.authData.id, `${idPrefix}%`), + or(isNull(schema.authData.expiry), lt(schema.authData.expiry, now)) + ) + }) + + for (const entry of entries) { + yield [splitKey(entry.id), entry.value] + } + } + } +} diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 47fd2539..876cd267 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -32,8 +32,10 @@ app.use(middleware.accessLogger) app.use(middleware.responseTime) app.use(middleware.errorHandler) +// Mount all auth routes which are handled by OpenAuth app.route('', authRouter) +// Mount all v1 API routes app.route('/v1', apiV1) app.doc31('/docs', { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c50bcca2..83a63ab9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -31,11 +31,8 @@ async function main() { if (authSession) { try { await client.setRefreshAuthToken(authSession.refreshToken) - } catch (err: any) { - console.warn( - 'Existing auth session is invalid; logging out...', - err.message - ) + } catch { + console.warn('Existing auth session is invalid; logging out.\n') AuthStore.clearAuth() } } diff --git a/readme.md b/readme.md index 0ae4e390..03972aeb 100644 --- a/readme.md +++ b/readme.md @@ -13,24 +13,10 @@ - like https://github.com/tierrun/tier and Saasify - https://github.com/tierrun/tier/blob/main/pricing/schema.json - https://blog.tier.run/tier-hello-world-demo -- auth - - decide on approach for auth - - built-in, first-party, tight coupling - - https://www.better-auth.com - - issues - - doesn't allow dynamic social provider config - - awkward schema cli generation and constraints - - awkward cookie-only session support (need JWTs for CLI and SDK) - - client uses dynamic proxy for methods which makes DX awkward - - should be able to use custom `ky`-based client - - drizzle pg adapter requires `Date` timestamps instead of default strings - - https://github.com/toolbeam/openauth - - https://github.com/aipotheosis-labs/aci/tree/main/backend/apps - - https://github.com/NangoHQ/nango - - https://github.com/transitive-bullshit?submit=Search&q=oauth&tab=stars&type=&sort=&direction=&submit=Search - - clerk / workos / auth0 - consider switching to [consola](https://github.com/unjs/consola) for logging? - consider switching to `bun` (for `--hot` reloading!!) +- transactional emails + - openauth password emails and `sendCode` ## License