Skip to content

Commit 7073275

Browse files
ariefgpevereq
andauthored
feat(config): add typed ConfigService with Zod validation (PR 1 of 4) (#425)
* feat(config): add typed ConfigService with Zod validation - Add lib/config/config-service.ts - centralized configuration singleton - Add lib/config/schemas/ with Zod schemas for all config sections: - core.schema.ts: NODE_ENV, APP_URL, DATABASE_URL, site info - auth.schema.ts: AUTH_SECRET, OAuth providers, JWT, cookies - email.schema.ts: SMTP, Resend, Novu providers - payment.schema.ts: Stripe, LemonSqueezy, Polar, trial amounts - analytics.schema.ts: PostHog, Sentry, Recaptcha, Vercel - integrations.schema.ts: Trigger.dev, Twenty CRM, cron - Add lib/config/utils/ with parsing and logging utilities - Add lib/config/types.ts with exported TypeScript types - Add lib/config/index.ts as module barrel export - ConfigService validates all 113 env vars at startup - Uses 'server-only' to prevent client-side usage - Fail-fast on critical errors, warnings for non-critical - Secrets automatically masked in logs - Tree-shakeable exports for each config section - No breaking changes - existing config files unchanged * feat(config): migrate auth and payment to typed ConfigService Migrate security-critical auth and payment code to use the new typed ConfigService, replacing direct process.env access with validated configuration. Changes: - lib/auth/providers.ts: Use authConfig for OAuth credentials - lib/auth/config.ts: Use authConfig.supabase for Supabase config - auth.config.ts: Use authConfig for OAuth provider setup - lib/payment/config/payment-provider-manager.ts: Use paymentConfig for Stripe, LemonSqueezy, Polar - lib/payment/lib/lemonsqueezy.ts: Use paymentConfig and coreConfig - lib/payment/lib/providers/stripe-provider.ts: Use paymentConfig - lib/payment/lib/providers/lemonsqueezy-provider.ts: Use paymentConfig and coreConfig - lib/payment/lib/providers/polar-provider.ts: Use paymentConfig and coreConfig - lib/types.ts: Use paymentConfig for pricing plan constants - app/api/stripe/webhook/route.ts: Use coreConfig and emailConfig - app/api/polar/webhook/route.ts: Use coreConfig for NODE_ENV - app/api/polar/webhook/utils.ts: Use coreConfig and emailConfig - app/api/polar/checkout/route.ts: Use coreConfig for NODE_ENV - app/api/polar/subscription/portal/route.ts: Use coreConfig for NODE_ENV - app/api/polar/subscription/portal/utils.ts: Use coreConfig for URLs - app/api/polar/subscription/[subscriptionId]/reactivate/route.ts: Use coreConfig - app/api/polar/subscription/[subscriptionId]/cancel/route.ts: Use coreConfig - lib/config/server-config.ts: Wrap with ConfigService, add @deprecated notices - lib/config.ts: Re-export ConfigService to fix module resolution * feat(config): migrate core services to use ConfigService - Migrate lib/db/ files (drizzle.ts, initialize.ts, seed.ts) to use coreConfig.DATABASE_URL and coreConfig.NODE_ENV - Migrate lib/mail/index.ts to use coreConfig.APP_URL and emailConfig (renamed import to avoid conflict) - Migrate lib/services/email-notification.service.ts with new getEmailServiceConfig() helper to reduce duplication - Migrate lib/background-jobs/ files (config.ts, job-factory.ts, local-job-manager.ts) to use integrationsConfig.triggerDev and coreConfig.NODE_ENV - Migrate lib/repositories/category.repository.ts to use coreConfig.content.dataRepository, ghToken, githubBranch - Migrate lib/repositories/tag.repository.ts to use coreConfig.content.* - Migrate lib/repositories/item.repository.ts to use coreConfig.content.* - Migrate lib/repositories/twenty-crm-config.repository.ts to use integrationsConfig.twentyCrm - Fix integrations.schema.ts TwentyCrmSyncMode to match actual types ('disabled' | 'platform' | 'direct_crm') - Skip drizzle.config.ts (CLI tool correctly uses dotenv) * feat(config): complete ConfigService migration across codebase - Migrate lib/auth/index.ts to use coreConfig.DATABASE_URL and coreConfig.NODE_ENV - Migrate lib/auth/supabase/server.ts and middleware.ts to use authConfig.supabase.url/anonKey - Migrate lib/auth/session-cache.ts and cached-session.ts to use coreConfig.NODE_ENV for debug logging - Migrate lib/auth/error-handler.ts to use authConfig for OAuth provider credentials - Migrate app/api/auth/[...nextauth]/error-handler.ts to use coreConfig.NODE_ENV - Migrate lib/payment/lib/utils/prices.ts to use paymentConfig.pricing.free/standard/premium - Migrate lib/payment/services/payment-email.service.ts to use paymentConfig.stripe.*PriceId - Migrate lib/services/sync-service.ts to use coreConfig.NODE_ENV - Migrate lib/services/twenty-crm-sync-factory.ts to use integrationsConfig.twentyCrm.baseUrl/apiKey - Migrate lib/repository.ts to use coreConfig.content.ghToken and dataRepository - Migrate lib/newsletter/config.ts to use coreConfig.APP_URL and emailConfig.resend/novu.apiKey - Migrate lib/constants.ts to use coreConfig.NODE_ENV and analyticsConfig.posthog.personalApiKey/projectId - Migrate app/api/cron/sync/route.ts to use integrationsConfig.cron.secret - Migrate app/api/verify-recaptcha/route.ts to use analyticsConfig.recaptcha.secretKey and coreConfig.NODE_ENV - Fix integrations.schema.ts TwentyCrmSyncMode to match actual types ('disabled' | 'platform' | 'direct_crm') * fix(config): correct AUTH_SECRET critical path from core to auth section * fix(config): implement graceful fallback for non-critical validation errors - Add .catch() handlers to URL/email fields to provide defaults on invalid format - Add .catch() handlers to enum fields to fallback on invalid values - Fix loadAndValidate() to use simplified logic (no more broken re-parse) - Invalid URLs/emails/enums now silently use defaults instead of crashing - Remaining validation errors after .catch() are truly unrecoverable * fix(config): resolve client/server boundary issues in config imports - Create lib/config/client.ts with client-safe siteConfig - Update client components to import from @/lib/config/client - Remove @/lib/config imports from payment providers (use process.env) - Update lib/types.ts to use NEXT_PUBLIC_ env vars for pricing defaults - Update lib/auth/config.ts to use NEXT_PUBLIC_SUPABASE_* directly - Add sandbox/apiUrl options to PolarConfig interface * fix(config): add safe default for EMAIL_SUPPORT to prevent webhook failures - Schema marks EMAIL_SUPPORT as optional but getEmailConfig() throws when absent - This breaks Stripe/LemonSqueezy webhooks for installs without EMAIL_SUPPORT set - Add fallback default '[email protected]' consistent with existing patterns - Polar handlers already handle this gracefully; align Stripe/Lemon behavior * fix(db): resolve server-only import breaking migration scripts Changes - Created lib/db/config.ts - Script-safe database configuration module that reads directly from process.env without server-only guard, allowing migration/seed scripts to run outside Next.js context - Updated lib/db/drizzle.ts - Replaced @/lib/config import with local ./config module; updated all coreConfig.DATABASE_URL and coreConfig.NODE_ENV usages to use getDatabaseUrl() and getNodeEnv() functions - Updated lib/db/seed.ts - Replaced config import with ./config; changed seed credential access to read directly from process.env (SEED_ADMIN_EMAIL, SEED_ADMIN_PASSWORD, SEED_FAKE_USER_COUNT) to avoid server-only issues - Updated lib/db/initialize.ts - Replaced @/lib/config import with ./config; updated all database URL and NODE_ENV checks to use script-safe functions - Fixed lib/config/schemas/integrations.schema.ts - Corrected TwentyCrmSyncMode enum values from 'disabled' | 'manual' | 'automatic' to 'disabled' | 'platform' | 'direct_crm' to match type * fix(db): resolve server-only import breaking migration scripts * fix(config): remove server-only imports from client-accessible modules * fix(payment): align FREE price env var name with config schema * fix(payment): handle NaN fallback for price env vars * feat(config): centralize client-side config access * feat(config): add DISABLE_AUTO_SYNC to core config schema * refactor(repository): remove dead githubBranch fallback * refactor(mail): remove dead APP_URL fallback * fix(types): correct trial env var names to match .env.example * fix: resolve config inconsistencies and OAuth env var mismatch * fix(config): respect explicit enabled=false in integration schemas * fix(config): handle NaN in payment pricing collection * fix(config): handle NaN in SMTP port collection * fix(config): handle NaN in seed user count collection * fix(polar): add support email fallback in webhook utils * style(auth): move import to top of session-cache.ts * docs(config): clarify client import path in barrel export * fix(config): resolve server-only import chain and missing exports * fix: remove unused sponsor hook and add APP_URL fallback - Comment out unused useActiveSponsorAds hook in item-detail.tsx The sponsor ads rendering was disabled but hook was still called, causing unnecessary API calls on every render - Add fallback for undefined APP_URL in email config Prevents malformed email links (password reset, verification) when APP_URL is not configured * fix(payment): remove dead APP_URL fallback in payment-provider-manager * fix(config): restore EMAIL_PROVIDER env var support - Add EMAIL_PROVIDER to email schema (string, default: 'resend') - Collect EMAIL_PROVIDER from environment variables - Add EMAIL_PROVIDER to .env.example with available options - Fix regression from config service migration that hardcoded 'resend' * fix(config): use EMAIL_PROVIDER in email-notification service * fix(mail): use production-safe APP_URL fallback --------- Co-authored-by: Ruslan Konviser <[email protected]>
1 parent 3c60240 commit 7073275

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+2601
-864
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ RECAPTCHA_SECRET_KEY=x-demo.ever.works
134134
# Email Templates and Service Configurations
135135
COMPANY_NAME=Ever Works
136136

137+
## Email provider: resend, novu
138+
EMAIL_PROVIDER=resend
139+
137140
RESEND_API_KEY=
138141
NOVU_API_KEY=
139142

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ jobs:
102102
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET || '' }}
103103
GITHUB_CLIENT_ID: ${{ secrets.GITHUB_CLIENT_ID || '' }}
104104
GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET || '' }}
105-
FACEBOOK_CLIENT_ID: ${{ secrets.FACEBOOK_CLIENT_ID || '' }}
106-
FACEBOOK_CLIENT_SECRET: ${{ secrets.FACEBOOK_CLIENT_SECRET || '' }}
105+
FB_CLIENT_ID: ${{ secrets.FACEBOOK_CLIENT_ID || '' }}
106+
FB_CLIENT_SECRET: ${{ secrets.FACEBOOK_CLIENT_SECRET || '' }}
107107
TWITTER_CLIENT_ID: ${{ secrets.TWITTER_CLIENT_ID || '' }}
108108
TWITTER_CLIENT_SECRET: ${{ secrets.TWITTER_CLIENT_SECRET || '' }}
109109

app/api/auth/[...nextauth]/error-handler.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
22
import crypto from 'crypto';
33
import { logError, ErrorType, createAppError } from '@/lib/utils/error-handler';
4+
import { coreConfig, authConfig } from '@/lib/config/config-service';
45

56
/**
67
* Handles NextAuth specific errors and provides appropriate responses
@@ -35,8 +36,8 @@ export function handleNextAuthError(
3536
// Send an appropriate response
3637
res.status(statusCode).json({
3738
error: {
38-
message: process.env.NODE_ENV === 'production'
39-
? 'An authentication error occurred'
39+
message: coreConfig.NODE_ENV === 'production'
40+
? 'An authentication error occurred'
4041
: error.message,
4142
status: statusCode
4243
}
@@ -57,11 +58,11 @@ export function checkNextAuthEnvironment(): string | null {
5758

5859
if (missingVars.length > 0) {
5960
const warningMessage = `Missing NextAuth environment variables: ${missingVars.join(', ')}`;
60-
61+
6162
// Suppress warnings during CI/linting
62-
const shouldSuppress =
63+
const shouldSuppress =
6364
process.env.CI === 'true' ||
64-
process.env.NODE_ENV === 'test' ||
65+
coreConfig.NODE_ENV === 'test' ||
6566
process.argv.some(arg => /(?:^|[/\\])(eslint|lint(?:-staged)?)(?:\.[jt]s)?$/.test(arg));
6667

6768
if (!shouldSuppress) {
@@ -100,7 +101,7 @@ export function isOAuthProviderConfigured(provider: string): boolean {
100101
const providerEnvVars: Record<string, string[]> = {
101102
google: ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'],
102103
github: ['GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET'],
103-
facebook: ['FACEBOOK_CLIENT_ID', 'FACEBOOK_CLIENT_SECRET'],
104+
facebook: ['FB_CLIENT_ID', 'FB_CLIENT_SECRET'],
104105
twitter: ['TWITTER_CLIENT_ID', 'TWITTER_CLIENT_SECRET'],
105106
microsoft: ['MICROSOFT_CLIENT_ID', 'MICROSOFT_CLIENT_SECRET']
106107
};

app/api/auth/[...nextauth]/error-wrapper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function isOAuthProviderConfigured(provider: string): boolean {
1111
const providerEnvVars: Record<string, string[]> = {
1212
google: ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'],
1313
github: ['GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET'],
14-
facebook: ['FACEBOOK_CLIENT_ID', 'FACEBOOK_CLIENT_SECRET'],
14+
facebook: ['FB_CLIENT_ID', 'FB_CLIENT_SECRET'],
1515
twitter: ['TWITTER_CLIENT_ID', 'TWITTER_CLIENT_SECRET'],
1616
microsoft: ['MICROSOFT_CLIENT_ID', 'MICROSOFT_CLIENT_SECRET']
1717
};

app/api/cron/sync/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextResponse } from "next/server";
22
import { triggerManualSync } from "@/lib/services/sync-service";
3+
import { integrationsConfig } from "@/lib/config/config-service";
34

45
/**
56
* Vercel Cron endpoint for automatic content synchronization.
@@ -26,7 +27,7 @@ export async function GET(request: Request): Promise<NextResponse<CronSyncRespon
2627

2728
// Verify cron secret for authentication
2829
const authHeader = request.headers.get("authorization");
29-
const cronSecret = process.env.CRON_SECRET;
30+
const cronSecret = integrationsConfig.cron.secret;
3031

3132
if (cronSecret && authHeader !== `Bearer ${cronSecret}`) {
3233
console.warn("[CRON_SYNC] Unauthorized request - invalid or missing CRON_SECRET");

app/api/polar/checkout/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import { auth, getOrCreatePolarProvider } from '@/lib/auth';
3+
import { coreConfig } from '@/lib/config/config-service';
34

45
/**
56
* @swagger
@@ -314,7 +315,7 @@ export async function POST(request: NextRequest) {
314315
{
315316
error: errorMessage,
316317
message: errorMessage,
317-
details: process.env.NODE_ENV === 'development' && error instanceof Error ? error.stack : undefined
318+
details: coreConfig.NODE_ENV === 'development' && error instanceof Error ? error.stack : undefined
318319
},
319320
{ status: statusCode }
320321
);
@@ -460,7 +461,7 @@ export async function GET(request: NextRequest) {
460461
return NextResponse.json(
461462
{
462463
error: errorMessage,
463-
details: process.env.NODE_ENV === 'development' && error instanceof Error ? error.stack : undefined
464+
details: coreConfig.NODE_ENV === 'development' && error instanceof Error ? error.stack : undefined
464465
},
465466
{ status: 500 }
466467
);

app/api/polar/subscription/[subscriptionId]/cancel/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import { auth, getOrCreatePolarProvider } from '@/lib/auth';
3+
import { coreConfig } from '@/lib/config/config-service';
34
import { getPolarSubscription } from '@/lib/payment/lib/utils/polar-subscription-helpers';
45

56
/**
@@ -302,7 +303,7 @@ export async function POST(
302303
return NextResponse.json(
303304
{
304305
error: errorMessage,
305-
...(process.env.NODE_ENV === 'development' && {
306+
...(coreConfig.NODE_ENV === 'development' && {
306307
details: error instanceof Error ? error.stack : String(error)
307308
})
308309
},

app/api/polar/subscription/[subscriptionId]/reactivate/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import { auth, getOrCreatePolarProvider } from '@/lib/auth';
3+
import { coreConfig } from '@/lib/config/config-service';
34
import { getPolarSubscription } from '@/lib/payment/lib/utils/polar-subscription-helpers';
45

56
/**
@@ -240,7 +241,7 @@ export async function POST(
240241
return NextResponse.json(
241242
{
242243
error: errorMessage,
243-
...(process.env.NODE_ENV === 'development' && {
244+
...(coreConfig.NODE_ENV === 'development' && {
244245
details: error instanceof Error ? error.stack : String(error)
245246
})
246247
},

app/api/polar/subscription/portal/route.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import { auth, getOrCreatePolarProvider } from '@/lib/auth';
3+
import { coreConfig } from '@/lib/config/config-service';
34
import { extractReturnUrl, buildAbsoluteUrl } from './utils';
45

56
/**
@@ -165,7 +166,7 @@ export async function POST(request: NextRequest) {
165166
errorDetails = {
166167
name: error.name,
167168
message: error.message,
168-
...(process.env.NODE_ENV === 'development' && {
169+
...(coreConfig.NODE_ENV === 'development' && {
169170
stack: error.stack
170171
})
171172
};
@@ -175,17 +176,17 @@ export async function POST(request: NextRequest) {
175176
}
176177

177178
// Log full error for debugging
178-
if (process.env.NODE_ENV === 'development') {
179+
if (coreConfig.NODE_ENV === 'development') {
179180
console.error('Full error object:', JSON.stringify(error, null, 2));
180181
}
181182

182183
return NextResponse.json(
183184
{
184185
error: 'Failed to create customer portal session',
185-
message: process.env.NODE_ENV === 'development'
186+
message: coreConfig.NODE_ENV === 'development'
186187
? errorMessage
187188
: 'Failed to create customer portal session',
188-
...(process.env.NODE_ENV === 'development' && {
189+
...(coreConfig.NODE_ENV === 'development' && {
189190
details: errorDetails
190191
})
191192
},

app/api/polar/subscription/portal/utils.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NextRequest } from 'next/server';
2+
import { coreConfig } from '@/lib/config/config-service';
23

34
/**
45
* Configuration constants for return URL handling
@@ -64,10 +65,7 @@ export function isValidPathLength(path: string): boolean {
6465
* @returns Application base URL (absolute URI)
6566
*/
6667
export function getAppUrl(): string {
67-
return (
68-
process.env.NEXT_PUBLIC_APP_URL ??
69-
(process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'https://demo.ever.works')
70-
);
68+
return coreConfig.APP_URL || 'https://demo.ever.works';
7169
}
7270

7371
/**
@@ -162,7 +160,7 @@ export async function extractReturnUrl(request: NextRequest): Promise<string> {
162160
return normalizedPath;
163161
} catch (error) {
164162
// Log error in development for debugging
165-
if (process.env.NODE_ENV === 'development') {
163+
if (coreConfig.NODE_ENV === 'development') {
166164
console.debug('[extractReturnUrl] Error parsing request body:', error);
167165
}
168166

0 commit comments

Comments
 (0)