diff --git a/scripts/start-server.mjs b/scripts/start-server.mjs new file mode 100644 index 0000000..8b08e19 --- /dev/null +++ b/scripts/start-server.mjs @@ -0,0 +1,104 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import path from 'node:path'; +import { randomBytes } from 'node:crypto'; +import { spawn } from 'node:child_process'; + +const DEFAULT_DB = 'mysql://root@localhost:3306/thoth'; +const SECRET_FILENAME = 'secret'; + +/** + * Resolves the Thoth home directory path. + * Uses THOTH_HOME_DIR env var if set, otherwise defaults to ~/.thoth + */ +function resolveHomeDirectory() { + const customHomeDirectory = process.env.THOTH_HOME_DIR; + if (customHomeDirectory) { + return customHomeDirectory; + } + return path.join(homedir(), '.thoth'); +} + +/** + * Ensures the home directory exists, creating it if necessary. + */ +function ensureHomeDirectory(homeDirectory) { + if (!existsSync(homeDirectory)) { + console.log(`Creating Thoth home directory: ${homeDirectory}`); + mkdirSync(homeDirectory, { recursive: true }); + } +} + +/** + * Resolves the database connection string. + * Uses DB env var if set, otherwise returns default MySQL connection. + */ +function resolveDatabase() { + const database = process.env.DB; + if (database) { + return database; + } + console.log(`DB not set, using default: ${DEFAULT_DB}`); + return DEFAULT_DB; +} + +/** + * Resolves the Better Auth secret. + * Priority: + * 1. BETTER_AUTH_SECRET env var if set + * 2. Read from {homeDirectory}/secret if file exists + * 3. Generate new secret and save to {homeDirectory}/secret + */ +function resolveSecret(homeDirectory) { + const environmentSecret = process.env.BETTER_AUTH_SECRET; + if (environmentSecret) { + return environmentSecret; + } + + const secretPath = path.join(homeDirectory, SECRET_FILENAME); + + if (existsSync(secretPath)) { + console.log(`Reading auth secret from: ${secretPath}`); + return readFileSync(secretPath, 'utf8').trim(); + } + + console.log(`Generating new auth secret and saving to: ${secretPath}`); + const newSecret = randomBytes(32).toString('hex'); + writeFileSync(secretPath, newSecret, { mode: 0o600 }); + return newSecret; +} + +/** + * Starts the Next.js server with the resolved environment variables. + */ +function startServer(environment) { + console.log('Starting server with pnpm start...'); + + const child = spawn('pnpm', ['start'], { + stdio: 'inherit', + env: { ...process.env, ...environment }, + shell: true, + }); + + child.on('error', (error) => { + console.error('Failed to start server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); + }); + + child.on('exit', (code) => { + // eslint-disable-next-line unicorn/no-process-exit + process.exit(code ?? 0); + }); +} + +// Main execution +const homeDirectory = resolveHomeDirectory(); +ensureHomeDirectory(homeDirectory); + +const environment = { + DB: resolveDatabase(), + BETTER_AUTH_SECRET: resolveSecret(homeDirectory), +}; + +startServer(environment); diff --git a/src/lib/auth/config.ts b/src/lib/auth/config.ts index f470172..0f1304f 100755 --- a/src/lib/auth/config.ts +++ b/src/lib/auth/config.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/prefer-ternary */ import { betterAuth } from 'better-auth'; import { genericOAuth } from 'better-auth/plugins'; import { createPool } from 'mysql2/promise'; @@ -8,61 +9,93 @@ import { connection } from 'next/server'; let authInstance: ReturnType | null = null; +/** + * Checks if all OIDC environment variables are configured. + * If all are present, OIDC authentication will be used. + * If any are missing, credentials (email/password) authentication will be used. + */ +function hasOidcConfig(environment: Awaited>): boolean { + return Boolean( + environment.OIDC_CLIENT_ID && + environment.OIDC_CLIENT_SECRET && + environment.OIDC_DISCOVERY_URL && + environment.OIDC_AUTHORIZATION_URL + ); +} + async function initializeAuth() { if (authInstance === null) { await connection().then(getDatabase); const environment = await getEnvironment(); + const useOidc = hasOidcConfig(environment); - authInstance = betterAuth({ - database: createPool(environment.DB), - plugins: [ - genericOAuth({ - config: [ - { - providerId: 'oidc', - clientId: environment.OIDC_CLIENT_ID, - clientSecret: environment.OIDC_CLIENT_SECRET, - authorizationUrl: environment.OIDC_AUTHORIZATION_URL, - discoveryUrl: environment.OIDC_DISCOVERY_URL, - scopes: ['openid', 'profile', 'email'], - }, - ], - }), - ], - trustedOrigins: environment.NODE_ENV === 'development' ? ['http://localhost:3000'] : [], - secret: environment.BETTER_AUTH_SECRET, - hooks: {}, - databaseHooks: { - user: { - create: { - after: async (user) => { - const workspaceRepository = await getWorkspaceRepository(); - const workspace = await workspaceRepository.create({ - name: 'Default Workspace', - userId: user.id, - createdAt: new Date().toISOString(), - lastUpdated: new Date().toISOString(), - } satisfies WorkspaceCreate); + const databaseHooks = { + user: { + create: { + after: async (user: { id: string }) => { + const workspaceRepository = await getWorkspaceRepository(); + const workspace = await workspaceRepository.create({ + name: 'Default Workspace', + userId: user.id, + createdAt: new Date().toISOString(), + lastUpdated: new Date().toISOString(), + } satisfies WorkspaceCreate); - const containerRepository = await getContainerRepository(); - const pageData: PageContainerCreate = { - name: 'Welcome', - type: 'page', - userId: user.id, - createdAt: new Date().toISOString(), - lastUpdated: new Date().toISOString(), - workspaceId: workspace.id, - emoji: '👋', - parentId: null, - }; + const containerRepository = await getContainerRepository(); + const pageData: PageContainerCreate = { + name: 'Welcome', + type: 'page', + userId: user.id, + createdAt: new Date().toISOString(), + lastUpdated: new Date().toISOString(), + workspaceId: workspace.id, + emoji: '👋', + parentId: null, + }; - await containerRepository.create(pageData); - }, + await containerRepository.create(pageData); }, }, }, - }); + }; + + if (useOidc) { + // OIDC authentication mode + authInstance = betterAuth({ + database: createPool(environment.DB), + plugins: [ + genericOAuth({ + config: [ + { + providerId: 'oidc', + clientId: environment.OIDC_CLIENT_ID!, + clientSecret: environment.OIDC_CLIENT_SECRET!, + authorizationUrl: environment.OIDC_AUTHORIZATION_URL!, + discoveryUrl: environment.OIDC_DISCOVERY_URL!, + scopes: ['openid', 'profile', 'email'], + }, + ], + }), + ], + trustedOrigins: environment.NODE_ENV === 'development' ? ['http://localhost:3000'] : [], + secret: environment.BETTER_AUTH_SECRET, + hooks: {}, + databaseHooks, + }); + } else { + // Credentials (email/password) authentication mode + authInstance = betterAuth({ + database: createPool(environment.DB), + emailAndPassword: { + enabled: true, + }, + trustedOrigins: environment.NODE_ENV === 'development' ? ['http://localhost:3000'] : [], + secret: environment.BETTER_AUTH_SECRET, + hooks: {}, + databaseHooks, + }); + } } return authInstance; } diff --git a/src/lib/environment.ts b/src/lib/environment.ts index c589e60..69b2c4e 100755 --- a/src/lib/environment.ts +++ b/src/lib/environment.ts @@ -8,10 +8,11 @@ const environmentSchema = { default: 'info', }), BETTER_AUTH_SECRET: str(), - OIDC_CLIENT_ID: str(), - OIDC_CLIENT_SECRET: str(), - OIDC_DISCOVERY_URL: url(), - OIDC_AUTHORIZATION_URL: url(), + // OIDC variables are optional - if not set, credentials auth will be used + OIDC_CLIENT_ID: str({ default: undefined }), + OIDC_CLIENT_SECRET: str({ default: undefined }), + OIDC_DISCOVERY_URL: url({ default: undefined }), + OIDC_AUTHORIZATION_URL: url({ default: undefined }), } as const; type Environment = ReturnType>;