diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts index bd14422e..1a0a8af2 100644 --- a/apps/backend/src/api/routes/auth.controller.ts +++ b/apps/backend/src/api/routes/auth.controller.ts @@ -30,7 +30,9 @@ export class AuthController { getOrgFromCookie ); - if (body.provider === 'LOCAL') { + const activationRequired = body.provider === 'LOCAL' && !!process.env.RESEND_API_KEY; + + if (activationRequired) { response.header('activate', 'true'); response.status(200).json({ activate: true }); return; diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 7cc0a962..30f1be83 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -8,6 +8,7 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { SubscriptionExceptionFilter } from '@gitroom/backend/services/auth/permissions/subscription.exception'; import { HttpExceptionFilter } from '@gitroom/nestjs-libraries/services/exception.filter'; +import { ConfigurationChecker } from '@gitroom/helpers/configuration/configuration.checker'; async function bootstrap() { const app = await NestFactory.create(AppModule, { @@ -38,6 +39,8 @@ async function bootstrap() { try { await app.listen(port); + + checkConfiguration() // Do this last, so that users will see obvious issues at the end of the startup log without having to scroll up. Logger.log(`🚀 Backend is running on: http://localhost:${port}`); } catch (e) { @@ -45,4 +48,20 @@ async function bootstrap() { } } +function checkConfiguration() { + const checker = new ConfigurationChecker(); + checker.readEnvFromProcess() + checker.check() + + if (checker.hasIssues()) { + for (const issue of checker.getIssues()) { + Logger.warn(issue, 'Configuration issue') + } + + Logger.warn("Configuration issues found: " + checker.getIssuesCount()) + } else { + Logger.log("Configuration check completed without any issues.") + } +} + bootstrap(); diff --git a/apps/commands/src/command.module.ts b/apps/commands/src/command.module.ts index dfa6acaa..9430ef56 100644 --- a/apps/commands/src/command.module.ts +++ b/apps/commands/src/command.module.ts @@ -4,11 +4,12 @@ import { CheckStars } from './tasks/check.stars'; import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module'; import { RefreshTokens } from './tasks/refresh.tokens'; import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module'; +import { ConfigurationTask } from './tasks/configuration'; @Module({ imports: [ExternalCommandModule, DatabaseModule, BullMqModule], controllers: [], - providers: [CheckStars, RefreshTokens], + providers: [CheckStars, RefreshTokens, ConfigurationTask], get exports() { return [...this.imports, ...this.providers]; }, diff --git a/apps/commands/src/tasks/configuration.ts b/apps/commands/src/tasks/configuration.ts new file mode 100644 index 00000000..7fd6f287 --- /dev/null +++ b/apps/commands/src/tasks/configuration.ts @@ -0,0 +1,30 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { ConfigurationChecker } from '@gitroom/helpers/configuration/configuration.checker'; + +@Injectable() +export class ConfigurationTask { + @Command({ + command: 'config:check', + describe: 'Checks your configuration (.env) file for issues.', + }) + create() { + const checker = new ConfigurationChecker(); + checker.readEnvFromProcess() + checker.check() + + if (checker.hasIssues()) { + for (const issue of checker.getIssues()) { + console.warn("Configuration issue:", issue) + } + + console.error("Configuration check complete, issues: ", checker.getIssuesCount()) + } else { + console.log("Configuration check complete, no issues found.") + } + + console.log("Press Ctrl+C to exit."); + return true + } +} + diff --git a/libraries/helpers/src/configuration/configuration.checker.ts b/libraries/helpers/src/configuration/configuration.checker.ts new file mode 100644 index 00000000..b6a1ca6d --- /dev/null +++ b/libraries/helpers/src/configuration/configuration.checker.ts @@ -0,0 +1,116 @@ +import { readFileSync, existsSync } from 'fs' +import * as dotenv from 'dotenv' +import { resolve } from 'path' + +export class ConfigurationChecker { + cfg: dotenv.DotenvParseOutput + issues: string[] = [] + + readEnvFromFile () { + const envFile = resolve(__dirname, '../../../.env') + + if (!existsSync(envFile)) { + console.error('Env file not found!: ', envFile) + return + } + + const handle = readFileSync(envFile, 'utf-8') + + this.cfg = dotenv.parse(handle) + } + + readEnvFromProcess () { + this.cfg = process.env + } + + check () { + this.checkDatabaseServers() + this.checkNonEmpty('JWT_SECRET') + this.checkIsValidUrl('MAIN_URL') + this.checkIsValidUrl('FRONTEND_URL') + this.checkIsValidUrl('NEXT_PUBLIC_BACKEND_URL') + this.checkIsValidUrl('BACKEND_INTERNAL_URL') + this.checkNonEmpty('RESEND_API_KEY', 'Needed to send user activation emails.') + this.checkNonEmpty('CLOUDFLARE_ACCOUNT_ID', 'Needed to setup providers.') + this.checkNonEmpty('CLOUDFLARE_ACCESS_KEY', 'Needed to setup providers.') + this.checkNonEmpty('CLOUDFLARE_SECRET_ACCESS_KEY', 'Needed to setup providers.') + this.checkNonEmpty('CLOUDFLARE_BUCKETNAME', 'Needed to setup providers.') + this.checkNonEmpty('CLOUDFLARE_BUCKET_URL', 'Needed to setup providers.') + this.checkNonEmpty('CLOUDFLARE_REGION', 'Needed to setup providers.') + } + + checkNonEmpty (key: string, description?: string): boolean { + const v = this.get(key) + + if (!description) { + description = '' + } + + if (!v) { + this.issues.push(key + ' not set. ' + description) + return false + } + + if (v.length === 0) { + this.issues.push(key + ' is empty.' + description) + return false + } + + return true + } + + get(key: string): string | undefined { + return this.cfg[key as keyof typeof this.cfg] + } + + checkDatabaseServers () { + this.checkRedis() + this.checkIsValidUrl('DATABASE_URL') + } + + checkRedis () { + if (!this.cfg.REDIS_URL) { + this.issues.push('REDIS_URL not set') + } + + try { + const redisUrl = new URL(this.cfg.REDIS_URL) + + if (redisUrl.protocol !== 'redis:') { + this.issues.push('REDIS_URL must start with redis://') + } + } catch (error) { + this.issues.push('REDIS_URL is not a valid URL') + } + } + + checkIsValidUrl (key: string) { + if (!this.checkNonEmpty(key)) { + return + } + + const urlString = this.get(key) + + try { + new URL(urlString) + } catch (error) { + this.issues.push(key + ' is not a valid URL') + } + + if (urlString.endsWith('/')) { + this.issues.push(key + ' should not end with /') + } + } + + hasIssues() { + return this.issues.length > 0 + } + + getIssues() { + return this.issues + } + + getIssuesCount() { + return this.issues.length + } +}