mirror of https://github.com/buster-so/buster.git
typesafe env variables
This commit is contained in:
parent
e70cd9d4a9
commit
a89d701795
|
@ -1 +0,0 @@
|
|||
NODE_OPTIONS='--no-deprecation'
|
|
@ -0,0 +1,9 @@
|
|||
NEXT_PUBLIC_API_URL="http://127.0.0.1:3001"
|
||||
NEXT_PUBLIC_API2_URL="http://127.0.0.1:3002"
|
||||
NEXT_PUBLIC_WEB_SOCKET_URL="ws://127.0.0.1:3001"
|
||||
NEXT_PUBLIC_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_SUPABASE_URL="http://127.0.0.1:54321"
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=""
|
||||
NEXT_SLACK_APP_SUPPORT_URL=""
|
||||
NEXT_PUBLIC_POSTHOG_KEY=""
|
||||
NEXT_PUBLIC_POSTHOG_HOST=""
|
|
@ -1,20 +1,15 @@
|
|||
import path, { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import env from './src/config/env.mjs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL
|
||||
? new URL(process.env.NEXT_PUBLIC_API_URL).origin
|
||||
: '';
|
||||
const wsUrl = process.env.NEXT_PUBLIC_WEB_SOCKET_URL
|
||||
? new URL(process.env.NEXT_PUBLIC_WEB_SOCKET_URL).origin
|
||||
.replace('https', 'wss')
|
||||
.replace('http', 'ws')
|
||||
: '';
|
||||
const api2Url = process.env.NEXT_PUBLIC_API2_URL
|
||||
? new URL(process.env.NEXT_PUBLIC_API2_URL).origin
|
||||
: '';
|
||||
const apiUrl = new URL(env.NEXT_PUBLIC_API_URL).origin;
|
||||
const api2Url = new URL(env.NEXT_PUBLIC_API2_URL).origin;
|
||||
const wsUrl = new URL(env.NEXT_PUBLIC_WEB_SOCKET_URL).origin
|
||||
.replace('https', 'wss')
|
||||
.replace('http', 'ws');
|
||||
|
||||
// Function to create CSP header with dynamic API URLs
|
||||
const createCspHeader = (isEmbed = false) => {
|
||||
|
|
|
@ -60,9 +60,6 @@ export async function login(page: Page) {
|
|||
await page.goto('http://localhost:3000/auth/login');
|
||||
|
||||
// Add your login logic here, for example:
|
||||
// await page.fill('input[name="email"]', process.env.TEST_USER_EMAIL || 'test@example.com');
|
||||
// await page.fill('input[name="password"]', process.env.TEST_USER_PASSWORD || 'password123');
|
||||
// await page.click('button[type="submit"]');
|
||||
|
||||
await page.getByText('Sign in').click();
|
||||
await page.getByText(`Don't already have an account?`).click();
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export const BASE_API_URL = `${process.env.NEXT_PUBLIC_API_URL}`;
|
||||
export const BASE_API_URL_V2 = `${process.env.NEXT_PUBLIC_API2_URL}`;
|
||||
import env from '@/config/envClient';
|
||||
|
||||
export const BASE_API_URL = `${env.NEXT_PUBLIC_API_URL}`;
|
||||
export const BASE_API_URL_V2 = `${env.NEXT_PUBLIC_API2_URL}`;
|
||||
export const BASE_URL = `${BASE_API_URL}/api/v1`;
|
||||
export const BASE_URL_V2 = `${BASE_API_URL_V2}/api/v2`;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createAxiosInstance } from '../createAxiosInstance';
|
||||
import env from '@/config/envClient';
|
||||
|
||||
const nextApi = createAxiosInstance(process.env.NEXT_PUBLIC_URL || '');
|
||||
const nextApi = createAxiosInstance(env.NEXT_PUBLIC_API_URL || '');
|
||||
|
||||
export default nextApi;
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
const SUPABASE_CONNECTION_URL = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/connect-supabase/login`;
|
||||
import env from '@/config/envClient';
|
||||
|
||||
const SUPABASE_CONNECTION_URL = `${env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/connect-supabase/login`;
|
||||
|
||||
export const connectSupabaseToBuster = async () => {
|
||||
window.location.href = SUPABASE_CONNECTION_URL;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
'use server';
|
||||
|
||||
import env from '@/config/env';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import type { AppSupportRequest } from '@/api/buster_rest/nextjs/support';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
|
||||
const slackHookURL = process.env.NEXT_SLACK_APP_SUPPORT_URL || '';
|
||||
const slackHookURL = env.NEXT_SLACK_APP_SUPPORT_URL || '';
|
||||
const STORAGE_BUCKET = 'support-screenshots'; // Using the default public bucket that usually exists
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
|
|
|
@ -25,10 +25,11 @@ import {
|
|||
import { inputHasText } from '@/lib/text';
|
||||
import { BusterRoutes, createBusterRoute } from '@/routes/busterRoutes';
|
||||
import { PolicyCheck } from './PolicyCheck';
|
||||
import env from '@/config/envClient';
|
||||
|
||||
const DEFAULT_CREDENTIALS = {
|
||||
email: process.env.NEXT_PUBLIC_USER || '',
|
||||
password: process.env.NEXT_PUBLIC_USER_PASSWORD || ''
|
||||
email: env.NEXT_PUBLIC_USER || '',
|
||||
password: env.NEXT_PUBLIC_USER_PASSWORD || ''
|
||||
};
|
||||
|
||||
export const LoginForm: React.FC = () => {
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
import env from './envClient';
|
||||
|
||||
export const isDev = env.NODE_ENV === 'development';
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { ClientEnv } from './envClient';
|
||||
|
||||
export interface ServerEnv {
|
||||
NEXT_SLACK_APP_SUPPORT_URL?: string;
|
||||
}
|
||||
|
||||
export interface Env extends ClientEnv, ServerEnv {}
|
||||
|
||||
// Main env (client + server)
|
||||
declare const env: Env;
|
||||
export default env;
|
|
@ -0,0 +1,57 @@
|
|||
import { z } from 'zod';
|
||||
import clientEnv from './envClient.mjs';
|
||||
import { isServer } from '@tanstack/react-query';
|
||||
|
||||
if (!isServer) {
|
||||
throw new Error('env.mjs is only meant to be used on the server');
|
||||
}
|
||||
|
||||
const serverEnvSchema = z.object({
|
||||
// Slack integration (private)
|
||||
NEXT_SLACK_APP_SUPPORT_URL: z
|
||||
.string()
|
||||
.url({ message: 'NEXT_SLACK_APP_SUPPORT_URL must be a valid URL' })
|
||||
.optional()
|
||||
});
|
||||
|
||||
// Parse and validate server-only environment variables
|
||||
let serverEnv;
|
||||
|
||||
try {
|
||||
serverEnv = serverEnvSchema.parse(process.env);
|
||||
} catch (error) {
|
||||
console.error('❌ Server environment validation failed!');
|
||||
console.error('');
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error('The following private environment variables are invalid or missing:');
|
||||
console.error('');
|
||||
|
||||
error.errors.forEach((err) => {
|
||||
const path = err.path.join('.');
|
||||
console.error(` • ${path}: ${err.message}`);
|
||||
});
|
||||
|
||||
console.error('');
|
||||
console.error(
|
||||
'Please check your .env file and ensure all required private variables are set correctly.'
|
||||
);
|
||||
} else {
|
||||
console.error('Unexpected error during server environment validation:', error);
|
||||
}
|
||||
|
||||
console.error('');
|
||||
console.error('Build cannot continue with invalid server environment configuration.');
|
||||
|
||||
// Throw error to prevent build from continuing
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Combine client and server environment variables
|
||||
const env = {
|
||||
...clientEnv,
|
||||
...serverEnv
|
||||
};
|
||||
|
||||
export { env };
|
||||
export default env;
|
|
@ -0,0 +1,17 @@
|
|||
export interface ClientEnv {
|
||||
NODE_ENV: 'development' | 'production' | 'test';
|
||||
NEXT_PUBLIC_API_URL: string;
|
||||
NEXT_PUBLIC_API2_URL: string;
|
||||
NEXT_PUBLIC_WEB_SOCKET_URL: string;
|
||||
NEXT_PUBLIC_URL: string;
|
||||
NEXT_PUBLIC_SUPABASE_URL: string;
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: string;
|
||||
NEXT_PUBLIC_POSTHOG_KEY?: string;
|
||||
NEXT_PUBLIC_POSTHOG_HOST?: string;
|
||||
NEXT_PUBLIC_USER?: string;
|
||||
NEXT_PUBLIC_USER_PASSWORD?: string;
|
||||
}
|
||||
|
||||
// Client-only env for frontend usage
|
||||
declare const clientEnv: ClientEnv;
|
||||
export default clientEnv;
|
|
@ -0,0 +1,83 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
const clientEnvSchema = z.object({
|
||||
// Node environment
|
||||
NODE_ENV: z
|
||||
.enum(['development', 'production', 'test'], {
|
||||
errorMap: () => ({ message: 'NODE_ENV must be development, production, or test' })
|
||||
})
|
||||
.default('development'),
|
||||
|
||||
// API URLs
|
||||
NEXT_PUBLIC_API_URL: z
|
||||
.string()
|
||||
.min(1, { message: 'NEXT_PUBLIC_API_URL is required' })
|
||||
.url({ message: 'NEXT_PUBLIC_API_URL must be a valid URL' }),
|
||||
NEXT_PUBLIC_API2_URL: z
|
||||
.string()
|
||||
.min(1, { message: 'NEXT_PUBLIC_API2_URL is required' })
|
||||
.url({ message: 'NEXT_PUBLIC_API2_URL must be a valid URL' }),
|
||||
NEXT_PUBLIC_WEB_SOCKET_URL: z
|
||||
.string()
|
||||
.min(1, { message: 'NEXT_PUBLIC_WEB_SOCKET_URL is required' })
|
||||
.url({ message: 'NEXT_PUBLIC_WEB_SOCKET_URL must be a valid URL' }),
|
||||
NEXT_PUBLIC_URL: z
|
||||
.string()
|
||||
.min(1, { message: 'NEXT_PUBLIC_URL is required' })
|
||||
.url({ message: 'NEXT_PUBLIC_URL must be a valid URL' }),
|
||||
|
||||
// Supabase configuration
|
||||
NEXT_PUBLIC_SUPABASE_URL: z
|
||||
.string()
|
||||
.min(1, { message: 'NEXT_PUBLIC_SUPABASE_URL is required' })
|
||||
.url({ message: 'NEXT_PUBLIC_SUPABASE_URL must be a valid URL' }),
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: z
|
||||
.string()
|
||||
.min(1, { message: 'NEXT_PUBLIC_SUPABASE_ANON_KEY is required' }),
|
||||
|
||||
// PostHog analytics
|
||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_POSTHOG_HOST: z
|
||||
.string()
|
||||
.url({ message: 'NEXT_PUBLIC_POSTHOG_HOST must be a valid URL' })
|
||||
.optional(),
|
||||
|
||||
// Development/Testing credentials
|
||||
NEXT_PUBLIC_USER: z.string().optional(),
|
||||
NEXT_PUBLIC_USER_PASSWORD: z.string().optional()
|
||||
});
|
||||
|
||||
// Parse and validate client environment variables
|
||||
let clientEnv;
|
||||
|
||||
try {
|
||||
clientEnv = clientEnvSchema.parse(process.env);
|
||||
} catch (error) {
|
||||
console.error('❌ Client environment validation failed!');
|
||||
console.error('');
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error('The following public environment variables are invalid or missing:');
|
||||
console.error('');
|
||||
|
||||
error.errors.forEach((err) => {
|
||||
const path = err.path.join('.');
|
||||
console.error(` • ${path}: ${err.message}`);
|
||||
});
|
||||
|
||||
console.error('');
|
||||
console.error(
|
||||
'Please check your .env file and ensure all required public variables are set correctly.'
|
||||
);
|
||||
} else {
|
||||
console.error('Unexpected error during client environment validation:', error);
|
||||
}
|
||||
|
||||
console.error('');
|
||||
console.error('Build cannot continue with invalid client environment configuration.');
|
||||
|
||||
// Throw error to prevent build from continuing
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export default clientEnv;
|
|
@ -1 +1,3 @@
|
|||
export const BUSTER_WS_URL = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/ws`;
|
||||
import env from '@/config/envClient';
|
||||
|
||||
export const BUSTER_WS_URL = `${env.NEXT_PUBLIC_WEB_SOCKET_URL}/api/v1/ws`;
|
||||
|
|
|
@ -13,8 +13,9 @@ import { useMemoizedFn } from '@/hooks';
|
|||
import { useWebSocket } from '@/hooks/useWebSocket';
|
||||
import type { SupabaseContextReturnType } from '../../Supabase';
|
||||
import { useSupabaseContext } from '../../Supabase';
|
||||
import env from '@/config/envClient';
|
||||
|
||||
const BUSTER_WS_URL = `${process.env.NEXT_PUBLIC_WEB_SOCKET_URL}/api/v1/ws`;
|
||||
const BUSTER_WS_URL = `${env.NEXT_PUBLIC_WEB_SOCKET_URL}/api/v1/ws`;
|
||||
|
||||
export type BusterOnCallback = {
|
||||
callback: BusterSocketResponse['callback'];
|
||||
|
|
|
@ -8,8 +8,9 @@ import React, { type PropsWithChildren, useEffect } from 'react';
|
|||
import type { BusterUserTeam } from '@/api/asset_interfaces';
|
||||
import { isDev } from '@/config';
|
||||
import { useUserConfigContextSelector } from '../Users';
|
||||
import env from '@/config/envClient';
|
||||
|
||||
const POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||
const POSTHOG_KEY = env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||
|
||||
export const BusterPosthogProvider: React.FC<PropsWithChildren> = React.memo(({ children }) => {
|
||||
if (isDev || !POSTHOG_KEY) {
|
||||
|
@ -21,7 +22,7 @@ export const BusterPosthogProvider: React.FC<PropsWithChildren> = React.memo(({
|
|||
BusterPosthogProvider.displayName = 'BusterPosthogProvider';
|
||||
|
||||
const options: Partial<PostHogConfig> = {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
api_host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
person_profiles: 'always',
|
||||
session_recording: {
|
||||
recordBody: true
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { createBrowserClient } from '@supabase/ssr';
|
||||
import env from '@/config/envClient';
|
||||
|
||||
export function createClient() {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
const supabaseUrl = env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error('Missing Supabase environment variables');
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
import { BusterRoutes, createBusterRoute } from '@/routes';
|
||||
import { createClient } from './server';
|
||||
import env from '@/config/env';
|
||||
|
||||
const authURLFull = `${process.env.NEXT_PUBLIC_URL}${createBusterRoute({
|
||||
const authURLFull = `${env.NEXT_PUBLIC_URL}${createBusterRoute({
|
||||
route: BusterRoutes.AUTH_CALLBACK
|
||||
})}`;
|
||||
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
'use server';
|
||||
|
||||
import { type CookieOptions, createServerClient } from '@supabase/ssr';
|
||||
import { cookies } from 'next/headers';
|
||||
import env from '@/config/env';
|
||||
|
||||
export const COOKIE_OPTIONS: CookieOptions = {
|
||||
path: '/',
|
||||
secure: process.env.NODE_ENV === 'production', // Only use secure in production
|
||||
secure: env.NODE_ENV === 'production', // Only use secure in production
|
||||
sameSite: 'lax', // Type assertion to fix the error
|
||||
httpOnly: true, // Make cookies HttpOnly
|
||||
maxAge: 60 * 60 * 24 * 7 // 1 week
|
||||
};
|
||||
|
||||
export async function createClient() {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
const supabaseUrl = env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error('Missing Supabase environment variables');
|
||||
|
|
|
@ -4,8 +4,9 @@ import { revalidatePath } from 'next/cache';
|
|||
import { redirect } from 'next/navigation';
|
||||
import { BusterRoutes, createBusterRoute } from '@/routes';
|
||||
import { createClient } from './server';
|
||||
import env from '@/config/env';
|
||||
|
||||
const authURLFull = `${process.env.NEXT_PUBLIC_URL}${createBusterRoute({
|
||||
const authURLFull = `${env.NEXT_PUBLIC_URL}${createBusterRoute({
|
||||
route: BusterRoutes.AUTH_CALLBACK
|
||||
})}`;
|
||||
|
||||
|
@ -102,7 +103,7 @@ export const signUp = async ({ email, password }: { email: string; password: str
|
|||
const authURL = createBusterRoute({
|
||||
route: BusterRoutes.AUTH_CONFIRM
|
||||
});
|
||||
const authURLFull = `${process.env.NEXT_PUBLIC_URL}${authURL}`;
|
||||
const authURLFull = `${env.NEXT_PUBLIC_URL}${authURL}`;
|
||||
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createServerClient } from '@supabase/ssr';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import env from '@/config/env';
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({
|
||||
|
@ -8,8 +9,8 @@ export async function updateSession(request: NextRequest) {
|
|||
});
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL || '',
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '',
|
||||
env.NEXT_PUBLIC_SUPABASE_URL || '',
|
||||
env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '',
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
|
|
Loading…
Reference in New Issue