env vars
This commit is contained in:
25
apps/website/env.d.ts
vendored
25
apps/website/env.d.ts
vendored
@@ -47,8 +47,33 @@ declare module 'react/compiler-runtime' {
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV?: 'development' | 'production' | 'test';
|
||||
|
||||
// Website (public, exposed to browser)
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE?: 'pre-launch' | 'alpha';
|
||||
NEXT_PUBLIC_SITE_URL?: string;
|
||||
NEXT_PUBLIC_API_BASE_URL?: string;
|
||||
NEXT_PUBLIC_SITE_NAME?: string;
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL?: string;
|
||||
NEXT_PUBLIC_SPONSOR_EMAIL?: string;
|
||||
NEXT_PUBLIC_LEGAL_COMPANY_NAME?: string;
|
||||
NEXT_PUBLIC_LEGAL_VAT_ID?: string;
|
||||
NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY?: string;
|
||||
NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS?: string;
|
||||
NEXT_PUBLIC_DISCORD_URL?: string;
|
||||
NEXT_PUBLIC_X_URL?: string;
|
||||
NEXT_TELEMETRY_DISABLED?: string;
|
||||
|
||||
// Website (server-only)
|
||||
API_BASE_URL?: string;
|
||||
|
||||
// Vercel KV (server-only)
|
||||
KV_REST_API_URL?: string;
|
||||
KV_REST_API_TOKEN?: string;
|
||||
|
||||
// Build/CI toggles (server-only)
|
||||
CI?: string;
|
||||
DOCKER?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
apps/website/lib/config/env.ts
Normal file
96
apps/website/lib/config/env.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const urlOptional = z.string().url().optional();
|
||||
const stringOptional = z.string().optional();
|
||||
|
||||
const publicEnvSchema = z.object({
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE: z.enum(['pre-launch', 'alpha']).optional(),
|
||||
NEXT_PUBLIC_SITE_URL: urlOptional,
|
||||
NEXT_PUBLIC_API_BASE_URL: urlOptional,
|
||||
|
||||
NEXT_PUBLIC_SITE_NAME: stringOptional,
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: stringOptional,
|
||||
NEXT_PUBLIC_SPONSOR_EMAIL: stringOptional,
|
||||
|
||||
NEXT_PUBLIC_LEGAL_COMPANY_NAME: stringOptional,
|
||||
NEXT_PUBLIC_LEGAL_VAT_ID: stringOptional,
|
||||
NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY: stringOptional,
|
||||
NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS: stringOptional,
|
||||
|
||||
NEXT_PUBLIC_DISCORD_URL: stringOptional,
|
||||
NEXT_PUBLIC_X_URL: stringOptional,
|
||||
});
|
||||
|
||||
const serverEnvSchema = z.object({
|
||||
API_BASE_URL: urlOptional,
|
||||
KV_REST_API_URL: urlOptional,
|
||||
KV_REST_API_TOKEN: stringOptional,
|
||||
|
||||
CI: stringOptional,
|
||||
DOCKER: stringOptional,
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
|
||||
});
|
||||
|
||||
export type WebsitePublicEnv = z.infer<typeof publicEnvSchema>;
|
||||
export type WebsiteServerEnv = z.infer<typeof serverEnvSchema>;
|
||||
|
||||
function formatZodIssues(issues: z.ZodIssue[]): string {
|
||||
return issues
|
||||
.map((issue) => {
|
||||
const path = issue.path.join('.') || '(root)';
|
||||
return `${path}: ${issue.message}`;
|
||||
})
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Website env vars (server-side safe).
|
||||
* Only validates the variables we explicitly support.
|
||||
*/
|
||||
export function getWebsiteServerEnv(): WebsiteServerEnv {
|
||||
const result = serverEnvSchema.safeParse(process.env);
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid website server env: ${formatZodIssues(result.error.issues)}`);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Website public env vars (safe on both server + client).
|
||||
* Note: on the client, only `NEXT_PUBLIC_*` vars are actually present.
|
||||
*/
|
||||
export function getWebsitePublicEnv(): WebsitePublicEnv {
|
||||
const result = publicEnvSchema.safeParse(process.env);
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid website public env: ${formatZodIssues(result.error.issues)}`);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export function isTruthyEnv(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
return value !== '0' && value.toLowerCase() !== 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the semantics used in [`getWebsiteApiBaseUrl()`](apps/website/lib/config/apiBaseUrl.ts:6).
|
||||
*/
|
||||
export function isTestLikeEnvironment(): boolean {
|
||||
const { NODE_ENV, CI, DOCKER } = getWebsiteServerEnv();
|
||||
return NODE_ENV === 'test' || CI === 'true' || DOCKER === 'true';
|
||||
}
|
||||
|
||||
export function isProductionEnvironment(): boolean {
|
||||
return getWebsiteServerEnv().NODE_ENV === 'production';
|
||||
}
|
||||
|
||||
export function isKvConfigured(): boolean {
|
||||
const { KV_REST_API_URL, KV_REST_API_TOKEN } = getWebsiteServerEnv();
|
||||
return Boolean(KV_REST_API_URL && KV_REST_API_TOKEN);
|
||||
}
|
||||
|
||||
export function assertKvConfiguredInProduction(): void {
|
||||
if (isProductionEnvironment() && !isKvConfigured()) {
|
||||
throw new Error('Missing KV_REST_API_URL/KV_REST_API_TOKEN in production environment');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { assertKvConfiguredInProduction, isKvConfigured, isProductionEnvironment } from './config/env';
|
||||
|
||||
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour in milliseconds
|
||||
const MAX_REQUESTS_PER_WINDOW = 5;
|
||||
const RATE_LIMIT_PREFIX = 'ratelimit:signup:';
|
||||
|
||||
const isDev = !process.env.KV_REST_API_URL;
|
||||
// Dev fallback: only allowed outside production.
|
||||
const isDev = !isProductionEnvironment() && !isKvConfigured();
|
||||
|
||||
// In-memory fallback for development
|
||||
const devRateLimits = new Map<string, { count: number; resetAt: number }>();
|
||||
@@ -49,6 +52,8 @@ export async function checkRateLimit(identifier: string): Promise<{
|
||||
}
|
||||
|
||||
// Production: Use Vercel KV
|
||||
assertKvConfiguredInProduction();
|
||||
|
||||
const { kv } = await import('@vercel/kv');
|
||||
const key = `${RATE_LIMIT_PREFIX}${identifier}`;
|
||||
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import { apiClient } from '@/lib/apiClient';
|
||||
import { ApiClient } from '@/lib/api';
|
||||
import type { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { LeagueMemberViewModel } from '@/lib/view-models/LeagueMemberViewModel';
|
||||
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
|
||||
let cachedLeaguesApiClient: LeaguesApiClient | undefined;
|
||||
|
||||
function getDefaultLeaguesApiClient(): LeaguesApiClient {
|
||||
if (cachedLeaguesApiClient) return cachedLeaguesApiClient;
|
||||
|
||||
const api = new ApiClient(getWebsiteApiBaseUrl());
|
||||
cachedLeaguesApiClient = (api as any).leagues as LeaguesApiClient;
|
||||
return cachedLeaguesApiClient;
|
||||
}
|
||||
|
||||
export class LeagueMembershipService {
|
||||
// In-memory cache for memberships (populated via API calls)
|
||||
private static leagueMemberships = new Map<string, LeagueMembership[]>();
|
||||
@@ -11,7 +22,7 @@ export class LeagueMembershipService {
|
||||
constructor(private readonly leaguesApiClient?: LeaguesApiClient) {}
|
||||
|
||||
private getClient(): LeaguesApiClient {
|
||||
return (this.leaguesApiClient ?? (apiClient as any).leagues) as LeaguesApiClient;
|
||||
return this.leaguesApiClient ?? getDefaultLeaguesApiClient();
|
||||
}
|
||||
|
||||
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMemberViewModel[]> {
|
||||
@@ -45,7 +56,7 @@ export class LeagueMembershipService {
|
||||
*/
|
||||
static async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
|
||||
try {
|
||||
const result = await apiClient.leagues.getMemberships(leagueId);
|
||||
const result = await getDefaultLeaguesApiClient().getMemberships(leagueId);
|
||||
const memberships: LeagueMembership[] = ((result as any)?.members ?? []).map((member: any) => ({
|
||||
id: `${member.driverId}-${leagueId}`, // Generate ID since API doesn't provide it
|
||||
leagueId,
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"hooks/",
|
||||
"lib/",
|
||||
"next-env.d.ts",
|
||||
"env.d.ts",
|
||||
"types/",
|
||||
"utilities/",
|
||||
".next/types/**/*.ts"
|
||||
|
||||
Reference in New Issue
Block a user