161 lines
4.3 KiB
TypeScript
161 lines
4.3 KiB
TypeScript
import { betterAuth } from 'better-auth';
|
|
import { getMigrations } from 'better-auth/db';
|
|
import { nextCookies } from 'better-auth/next-js';
|
|
import { admin, magicLink, organization } from 'better-auth/plugins';
|
|
import { Pool } from 'pg';
|
|
|
|
declare global {
|
|
// eslint-disable-next-line no-var
|
|
var __fiscalAuthPgPool: Pool | undefined;
|
|
}
|
|
|
|
type BetterAuthInstance = ReturnType<typeof betterAuth>;
|
|
|
|
let authInstance: BetterAuthInstance | null = null;
|
|
let migrationPromise: Promise<void> | null = null;
|
|
|
|
function parseCsvList(value: string | undefined) {
|
|
return (value ?? '')
|
|
.split(',')
|
|
.map((entry) => entry.trim())
|
|
.filter((entry) => entry.length > 0);
|
|
}
|
|
|
|
function isPostgresConnectionString(value: string | undefined) {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const protocol = new URL(value).protocol.toLowerCase();
|
|
return protocol === 'postgres:' || protocol === 'postgresql:';
|
|
} catch {
|
|
return /^postgres(?:ql)?:\/\//i.test(value);
|
|
}
|
|
}
|
|
|
|
function splitSqlStatements(sqlText: string) {
|
|
return sqlText
|
|
.split(';')
|
|
.map((statement) => statement.trim())
|
|
.filter((statement) => statement.length > 0);
|
|
}
|
|
|
|
function buildPostgresMigrationPlan(sqlText: string) {
|
|
const immediateStatements: string[] = [];
|
|
const deferredIndexStatements: string[] = [];
|
|
const addIndexPattern = /^alter table\s+([^\s]+)\s+add\s+index\s+([^\s]+)\s+\((.+)\)$/i;
|
|
|
|
for (const rawStatement of splitSqlStatements(sqlText)) {
|
|
const statement = rawStatement.replace(/\s+/g, ' ').trim();
|
|
const match = statement.match(addIndexPattern);
|
|
if (!match) {
|
|
immediateStatements.push(statement);
|
|
continue;
|
|
}
|
|
|
|
const [, tableName, indexName, columns] = match;
|
|
deferredIndexStatements.push(`create index if not exists ${indexName} on ${tableName} (${columns})`);
|
|
}
|
|
|
|
return [...immediateStatements, ...deferredIndexStatements];
|
|
}
|
|
|
|
async function runPostgresMigrations(pool: Pool, sqlText: string) {
|
|
const statements = buildPostgresMigrationPlan(sqlText);
|
|
if (statements.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
for (const statement of statements) {
|
|
await client.query(statement);
|
|
}
|
|
await client.query('COMMIT');
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
function getPool() {
|
|
const connectionString = process.env.DATABASE_URL?.trim();
|
|
if (!connectionString) {
|
|
throw new Error('DATABASE_URL is required for Better Auth PostgreSQL adapter.');
|
|
}
|
|
|
|
if (!globalThis.__fiscalAuthPgPool) {
|
|
globalThis.__fiscalAuthPgPool = new Pool({ connectionString });
|
|
}
|
|
|
|
return globalThis.__fiscalAuthPgPool;
|
|
}
|
|
|
|
function buildAuth() {
|
|
const adminUserIds = parseCsvList(process.env.BETTER_AUTH_ADMIN_USER_IDS);
|
|
const trustedOrigins = parseCsvList(process.env.BETTER_AUTH_TRUSTED_ORIGINS);
|
|
const baseURL = process.env.BETTER_AUTH_BASE_URL?.trim()
|
|
|| process.env.BETTER_AUTH_URL?.trim()
|
|
|| undefined;
|
|
const secret = process.env.BETTER_AUTH_SECRET?.trim() || undefined;
|
|
|
|
return betterAuth({
|
|
database: getPool(),
|
|
baseURL,
|
|
secret,
|
|
emailAndPassword: {
|
|
enabled: true
|
|
},
|
|
trustedOrigins: trustedOrigins.length > 0 ? trustedOrigins : undefined,
|
|
plugins: [
|
|
admin(adminUserIds.length > 0 ? { adminUserIds } : undefined),
|
|
magicLink({
|
|
sendMagicLink: async ({ email, url }) => {
|
|
console.info(`[better-auth] Magic link requested for ${email}: ${url}`);
|
|
}
|
|
}),
|
|
organization(),
|
|
nextCookies()
|
|
]
|
|
});
|
|
}
|
|
|
|
export function getAuth() {
|
|
if (!authInstance) {
|
|
authInstance = buildAuth();
|
|
}
|
|
|
|
return authInstance;
|
|
}
|
|
|
|
export async function ensureAuthSchema() {
|
|
const auth = getAuth();
|
|
const connectionString = process.env.DATABASE_URL?.trim();
|
|
|
|
if (!migrationPromise) {
|
|
migrationPromise = (async () => {
|
|
const migrations = await getMigrations(auth.options);
|
|
if (isPostgresConnectionString(connectionString)) {
|
|
const sql = await migrations.compileMigrations();
|
|
await runPostgresMigrations(getPool(), sql);
|
|
return;
|
|
}
|
|
|
|
await migrations.runMigrations();
|
|
})();
|
|
}
|
|
|
|
try {
|
|
await migrationPromise;
|
|
} catch (error) {
|
|
migrationPromise = null;
|
|
throw error;
|
|
}
|
|
|
|
return auth;
|
|
}
|