diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..078bacf --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,16 @@ +name: Release Package + +on: + push: + branches: + - main + +permissions: + contents: read + packages: write + id-token: write + +jobs: + release: + uses: rubriclab/package/.github/workflows/release-package.yml@main + secrets: inherit diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..29e89bf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +- [2024-10-02] [init](https://github.com/RubricLab/auth/commit/40145b017976d9a7393063a4640e6965af45eac4) +# Changelog + diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..94dc280 --- /dev/null +++ b/index.ts @@ -0,0 +1,4 @@ +export type { + AuthProvider, + AuthProviderConfig, +} from "./lib/types"; diff --git a/lib/route.ts b/lib/route.ts new file mode 100644 index 0000000..554bb40 --- /dev/null +++ b/lib/route.ts @@ -0,0 +1,93 @@ +import { type NextRequest, NextResponse } from "next/server"; +import type { + AuthProviders, + DB, + UserInfo, + AuthProvider, + AuthTokens, +} from "./types"; + +async function connectAuthProvider({ + userId, + userInfo, + provider, + authTokens, + db, +}: { + userId: string; + userInfo: UserInfo; + provider: AuthProvider; + authTokens: AuthTokens; + db: DB; +}) { + await db.authProvider.upsert({ + where: { + userId_provider_accountId: { + provider: provider.config.provider, + userId, + accountId: userInfo.accountId, + }, + }, + update: { + accessToken: authTokens.accessToken, + refreshToken: authTokens.refreshToken, + expiresAt: authTokens.expiresAt ? new Date(authTokens.expiresAt) : null, + }, + create: { + provider: provider.config.provider, + accountId: userInfo.accountId, + label: userInfo.label, + scopes: provider.config.scopes, + accessToken: authTokens.accessToken, + refreshToken: authTokens.refreshToken, + expiresAt: authTokens.expiresAt ? new Date(authTokens.expiresAt) : null, + userId, + }, + }); +} + +export function createAuthCallbackHandler({ + authProviders, + db, + url, +}: { + authProviders: AuthProviders; + db: DB; + url: string; +}) { + return async ( + request: NextRequest, + { params }: { params: { provider: keyof AuthProviders } }, + ): Promise => { + const provider = authProviders[params.provider]; + if (!provider) { + return NextResponse.redirect(`${url}?error=InvalidProvider`); + } + const code = request.nextUrl.searchParams.get("code"); + if (!code) { + return NextResponse.redirect(`${url}?error=NoCodeProvided`); + } + + const userId = request.nextUrl.searchParams.get("state"); + if (!userId) { + return NextResponse.redirect(`${url}?error=NoUserIdProvided`); + } + + try { + const authTokens = await provider.getTokensFromCode({ code }); + const userInfo = await provider.getUserInfo({ + token: authTokens.accessToken, + }); + + await connectAuthProvider({ userId, provider, authTokens, userInfo, db }); + + return NextResponse.redirect(`${url}?success=true`); + } catch (error) { + console.error( + `Error during ${provider.config.provider} auth callback:`, + error, + ); + return NextResponse.redirect(`${url}?error=AuthFailed`); + } + }; +} diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..7cf8fe6 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,49 @@ +import type { PrismaClient } from "@prisma/client"; + +export interface UserInfo { + accountId: string; + label: string; +} + +export interface AuthProviderConfig { + provider: string; + scopes: string[]; + clientId: string; + clientSecret: string; +} + +export interface AuthTokens { + accessToken: string; + refreshToken: string | null; + expiresAt: number | null; +} + +export interface AuthProvider { + config: AuthProviderConfig; + getAuthUrl: ({ userId }: { userId: string }) => string; + getTokensFromCode({ code }: { code: string }): Promise; + getAccessToken({ + userId, + accountId, + }: { + userId: string; + accountId: string; + }): Promise<{ accessToken: string }>; + getUserInfo({ token }: { token: string }): Promise; +} + +export type AuthProviders = { + [provider in string]?: AuthProvider; +}; + +export interface User { + id: string; +} + +export interface WebhookInfo { + id: string; + type: string; + enabled: boolean; +} + +export type DB = PrismaClient; diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..fd43f66 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,59 @@ +import type { Prisma } from "@prisma/client"; + +import type { AuthProviders } from "./types"; + +export function createAuthActions({ + authProviders, + db, +}: { + authProviders: AuthProviders; + db: { + authProvider: Prisma.AuthProviderDelegate; + }; +}) { + return { + async getAuthUrl({ + userId, + provider, + }: { + userId: string; + provider: string; + }) { + const authProvider = authProviders[provider]; + + if (!authProvider) { + throw new Error(`Auth provider not found for ${provider}`); + } + + return authProvider.getAuthUrl({ userId }); + }, + + async getConnectedAccounts({ userId }: { userId: string }) { + const connectedAccounts = await db.authProvider.findMany({ + where: { userId }, + }); + + return connectedAccounts; + }, + + async disconnectAuthProvider({ + userId, + provider, + accountId, + }: { + userId: string; + provider: string; + accountId: string; + }) { + await db.authProvider.delete({ + where: { + userId_provider_accountId: { + userId, + provider, + accountId, + }, + }, + }); + }, + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..859207a --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "scripts": { + "generate": "bun scripts/generate.ts", + "watch": "bun scripts/watch.ts", + "prepare": "bun x simple-git-hooks", + "bleed": "bun x npm-check-updates -u && bun i", + "clean": "rm -rf .next && rm -rf node_modules", + "format": "bun x biome format --write .", + "lint": "bun x biome check .", + "lint:fix": "bun x biome lint . --write --unsafe" + }, + "name": "@rubriclab/auth", + "version": "0.0.1", + "main": "index.ts", + "dependencies": { + "@rubriclab/package": "*" + }, + "simple-git-hooks": { + "post-commit": "bun run rubriclab-postcommit" + }, + "publishConfig": { + "access": "public" + } +}