Refactor environment variable handling and improve API key management

- Introduced `client.ts` and `server.ts` for structured environment variable management using `@t3-oss/env-nextjs`.
- Updated references to environment variables in various files to use the new `clientEnv` and `serverEnv` objects.
- Enhanced the `next.config.mjs` file to validate environment variables during build.
- Added new dependencies: `@t3-oss/env-nextjs` and `jiti` for improved environment handling.
- Cleaned up imports and ensured consistent usage of environment variables across the application.
This commit is contained in:
simplr-sh 2025-01-06 10:28:54 +05:30
parent adf96e516f
commit 186bd3c2cc
11 changed files with 5017 additions and 3943 deletions

View File

@ -1,9 +1,10 @@
// app/actions.ts
'use server';
import { serverEnv } from '@/env/server';
import { xai } from '@ai-sdk/xai';
import { generateObject } from 'ai';
import { z } from 'zod';
import { xai } from '@ai-sdk/xai';
export async function suggestQuestions(history: any[]) {
'use server';
@ -37,7 +38,7 @@ Do not use pronouns like he, she, him, his, her, etc. in the questions as they b
};
}
const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY;
const ELEVENLABS_API_KEY = serverEnv.ELEVENLABS_API_KEY;
export async function generateSpeech(text: string, voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer' = "alloy") {

View File

@ -1,21 +1,21 @@
// /app/api/chat/route.ts
import { z } from "zod";
import { xai } from '@ai-sdk/xai'
import Exa from 'exa-js'
import {
convertToCoreMessages,
streamText,
tool,
smoothStream
} from "ai";
import { BlobRequestAbortedError, put } from '@vercel/blob';
import { getGroupConfig } from "@/app/actions";
import { serverEnv } from "@/env/server";
import { xai } from '@ai-sdk/xai';
import CodeInterpreter from "@e2b/code-interpreter";
import FirecrawlApp from '@mendable/firecrawl-js';
import { tavily } from '@tavily/core'
import { getGroupConfig } from "@/app/actions";
import { geolocation, ipAddress } from '@vercel/functions'
import { tavily } from '@tavily/core';
import { Ratelimit } from "@upstash/ratelimit"; // for deno: see above
import { Redis } from "@upstash/redis"; // see below for cloudflare and fastly adapters
import { BlobRequestAbortedError, put } from '@vercel/blob';
import {
convertToCoreMessages,
smoothStream,
streamText,
tool
} from "ai";
import Exa from 'exa-js';
import { z } from "zod";
// Allow streaming responses up to 60 seconds
export const maxDuration = 120;
@ -182,7 +182,7 @@ export async function POST(req: Request) {
searchDepth: ("basic" | "advanced")[];
exclude_domains?: string[];
}) => {
const apiKey = process.env.TAVILY_API_KEY;
const apiKey = serverEnv.TAVILY_API_KEY;
const tvly = tavily({ apiKey });
const includeImageDescriptions = true;
@ -258,7 +258,7 @@ export async function POST(req: Request) {
}),
execute: async ({ query }: { query: string }) => {
try {
const exa = new Exa(process.env.EXA_API_KEY as string);
const exa = new Exa(serverEnv.EXA_API_KEY as string);
const result = await exa.searchAndContents(
query,
@ -304,7 +304,7 @@ export async function POST(req: Request) {
query: z.string().describe("The search query for movies/TV shows"),
}),
execute: async ({ query }: { query: string }) => {
const TMDB_API_KEY = process.env.TMDB_API_KEY;
const TMDB_API_KEY = serverEnv.TMDB_API_KEY;
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
try {
@ -389,7 +389,7 @@ export async function POST(req: Request) {
description: "Get trending movies from TMDB",
parameters: z.object({}),
execute: async () => {
const TMDB_API_KEY = process.env.TMDB_API_KEY;
const TMDB_API_KEY = serverEnv.TMDB_API_KEY;
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
try {
@ -423,7 +423,7 @@ export async function POST(req: Request) {
description: "Get trending TV shows from TMDB",
parameters: z.object({}),
execute: async () => {
const TMDB_API_KEY = process.env.TMDB_API_KEY;
const TMDB_API_KEY = serverEnv.TMDB_API_KEY;
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
try {
@ -460,7 +460,7 @@ export async function POST(req: Request) {
}),
execute: async ({ query }: { query: string }) => {
try {
const exa = new Exa(process.env.EXA_API_KEY as string);
const exa = new Exa(serverEnv.EXA_API_KEY as string);
// Search academic papers with content summary
const result = await exa.searchAndContents(
@ -516,7 +516,7 @@ export async function POST(req: Request) {
}),
execute: async ({ query, no_of_results }: { query: string, no_of_results: number }) => {
try {
const exa = new Exa(process.env.EXA_API_KEY as string);
const exa = new Exa(serverEnv.EXA_API_KEY as string);
// Simple search to get YouTube URLs only
const searchResult = await exa.search(
@ -545,17 +545,17 @@ export async function POST(req: Request) {
try {
// Fetch detailed info from our endpoints
const [detailsResponse, captionsResponse, timestampsResponse] = await Promise.all([
fetch(`${process.env.YT_ENDPOINT}/video-data`, {
fetch(`${serverEnv.YT_ENDPOINT}/video-data`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: result.url })
}).then(res => res.ok ? res.json() : null),
fetch(`${process.env.YT_ENDPOINT}/video-captions`, {
fetch(`${serverEnv.YT_ENDPOINT}/video-captions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: result.url })
}).then(res => res.ok ? res.text() : null),
fetch(`${process.env.YT_ENDPOINT}/video-timestamps`, {
fetch(`${serverEnv.YT_ENDPOINT}/video-timestamps`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: result.url })
@ -595,7 +595,7 @@ export async function POST(req: Request) {
url: z.string().describe("The URL to retrieve the information from."),
}),
execute: async ({ url }: { url: string }) => {
const app = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY });
const app = new FirecrawlApp({ apiKey: serverEnv.FIRECRAWL_API_KEY });
try {
const content = await app.scrapeUrl(url);
if (!content.success || !content.metadata) {
@ -625,7 +625,7 @@ export async function POST(req: Request) {
lon: z.number().describe("The longitude of the location."),
}),
execute: async ({ lat, lon }: { lat: number; lon: number }) => {
const apiKey = process.env.OPENWEATHER_API_KEY;
const apiKey = serverEnv.OPENWEATHER_API_KEY;
const response = await fetch(
`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${apiKey}`,
);
@ -645,7 +645,7 @@ export async function POST(req: Request) {
console.log("Title:", title);
console.log("Icon:", icon);
const sandbox = await CodeInterpreter.create(process.env.SANDBOX_TEMPLATE_ID!);
const sandbox = await CodeInterpreter.create(serverEnv.SANDBOX_TEMPLATE_ID!);
const execution = await sandbox.runCode(code);
let message = "";
let images = [];
@ -729,14 +729,14 @@ export async function POST(req: Request) {
execute: async ({ query, coordinates }: { query: string; coordinates: number[] }) => {
try {
// Forward geocoding with Google Maps API
const googleApiKey = process.env.GOOGLE_MAPS_API_KEY;
const googleApiKey = serverEnv.GOOGLE_MAPS_API_KEY;
const googleResponse = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(query)}&key=${googleApiKey}`
);
const googleData = await googleResponse.json();
// Reverse geocoding with Mapbox
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
const mapboxToken = serverEnv.MAPBOX_ACCESS_TOKEN;
const [lat, lng] = coordinates;
const mapboxResponse = await fetch(
`https://api.mapbox.com/search/geocode/v6/reverse?longitude=${lng}&latitude=${lat}&access_token=${mapboxToken}`
@ -805,7 +805,7 @@ export async function POST(req: Request) {
location?: string;
radius?: number;
}) => {
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
const mapboxToken = serverEnv.MAPBOX_ACCESS_TOKEN;
let proximity = '';
if (location) {
@ -854,9 +854,9 @@ export async function POST(req: Request) {
from: z.string().describe("The source language (optional, will be auto-detected if not provided)."),
}),
execute: async ({ text, to, from }: { text: string; to: string; from?: string }) => {
const key = process.env.AZURE_TRANSLATOR_KEY;
const key = serverEnv.AZURE_TRANSLATOR_KEY;
const endpoint = "https://api.cognitive.microsofttranslator.com";
const location = process.env.AZURE_TRANSLATOR_LOCATION;
const location = serverEnv.AZURE_TRANSLATOR_LOCATION;
const url = `${endpoint}/translate?api-version=3.0&to=${to}${from ? `&from=${from}` : ''}`;
@ -893,14 +893,14 @@ export async function POST(req: Request) {
type: string;
radius: number;
}) => {
const apiKey = process.env.TRIPADVISOR_API_KEY;
const apiKey = serverEnv.TRIPADVISOR_API_KEY;
let finalLat = latitude;
let finalLng = longitude;
try {
// Try geocoding first
const geocodingData = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(location)}&key=${process.env.GOOGLE_MAPS_API_KEY}`
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(location)}&key=${serverEnv.GOOGLE_MAPS_API_KEY}`
);
const geocoding = await geocodingData.json();
@ -1007,7 +1007,7 @@ export async function POST(req: Request) {
// Get timezone for the location
const tzResponse = await fetch(
`https://maps.googleapis.com/maps/api/timezone/json?location=${details.latitude},${details.longitude}&timestamp=${Math.floor(Date.now() / 1000)}&key=${process.env.GOOGLE_MAPS_API_KEY}`
`https://maps.googleapis.com/maps/api/timezone/json?location=${details.latitude},${details.longitude}&timestamp=${Math.floor(Date.now() / 1000)}&key=${serverEnv.GOOGLE_MAPS_API_KEY}`
);
const tzData = await tzResponse.json();
const timezone = tzData.timeZoneId || 'UTC';
@ -1131,7 +1131,7 @@ export async function POST(req: Request) {
execute: async ({ flight_number }: { flight_number: string }) => {
try {
const response = await fetch(
`https://api.aviationstack.com/v1/flights?access_key=${process.env.AVIATION_STACK_API_KEY}&flight_iata=${flight_number}`
`https://api.aviationstack.com/v1/flights?access_key=${serverEnv.AVIATION_STACK_API_KEY}&flight_iata=${flight_number}`
);
return await response.json();
} catch (error) {

View File

@ -1,10 +1,10 @@
import { list, del, ListBlobResult } from '@vercel/blob';
import { del, list, ListBlobResult } from '@vercel/blob';
import { NextRequest, NextResponse } from 'next/server';
export const runtime = 'edge';
export async function GET(req: NextRequest) {
if (req.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
if (req.headers.get('Authorization') !== `Bearer ${serverEnv.CRON_SECRET}`) {
return new NextResponse('Unauthorized', { status: 401 });
}

View File

@ -1,13 +1,14 @@
"use client";
import { ThemeProvider } from "next-themes"
import { ReactNode } from "react"
import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
import { clientEnv } from "@/env/client";
import { ThemeProvider } from "next-themes";
import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';
import { ReactNode } from "react";
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
posthog.init(clientEnv.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: clientEnv.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'always',
})
}

View File

@ -1,7 +1,8 @@
import React, { useEffect, useRef, useCallback } from 'react';
import { clientEnv } from "@/env/client";
import { cn } from "@/lib/utils";
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { cn } from "@/lib/utils";
import React, { useCallback, useEffect, useRef } from 'react';
interface Location {
lat: number;
@ -38,7 +39,7 @@ interface Place {
timezone?: string;
}
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || '';
mapboxgl.accessToken = clientEnv.NEXT_PUBLIC_MAPBOX_TOKEN || '';
interface InteractiveMapProps {
center: Location;

View File

@ -1,10 +1,11 @@
// /app/components/map-components.tsx
import React, { useEffect, useRef } from 'react';
import { Skeleton } from "@/components/ui/skeleton";
import { clientEnv } from "@/env/client";
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { Skeleton } from "@/components/ui/skeleton";
import React, { useEffect, useRef } from 'react';
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || '';
mapboxgl.accessToken = clientEnv.NEXT_PUBLIC_MAPBOX_TOKEN || '';
interface Location {
lat: number;
@ -171,4 +172,4 @@ const MapContainer: React.FC<MapContainerProps> = ({
);
};
export { MapComponent, MapSkeleton, MapContainer };
export { MapComponent, MapContainer, MapSkeleton };

16
env/client.ts vendored Normal file
View File

@ -0,0 +1,16 @@
// https://env.t3.gg/docs/nextjs#create-your-schema
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'
export const clientEnv = createEnv({
client: {
NEXT_PUBLIC_MAPBOX_TOKEN: z.string().min(1),
NEXT_PUBLIC_POSTHOG_KEY: z.string().min(1),
NEXT_PUBLIC_POSTHOG_HOST: z.string().min(1).url(),
},
runtimeEnv: {
NEXT_PUBLIC_MAPBOX_TOKEN: process.env.NEXT_PUBLIC_MAPBOX_TOKEN,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
},
})

24
env/server.ts vendored Normal file
View File

@ -0,0 +1,24 @@
// https://env.t3.gg/docs/nextjs#create-your-schema
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'
export const serverEnv = createEnv({
server: {
ELEVENLABS_API_KEY: z.string().min(1),
TAVILY_API_KEY: z.string().min(1),
EXA_API_KEY: z.string().min(1),
TMDB_API_KEY: z.string().min(1),
YT_ENDPOINT: z.string().min(1),
FIRECRAWL_API_KEY: z.string().min(1),
OPENWEATHER_API_KEY: z.string().min(1),
SANDBOX_TEMPLATE_ID: z.string().min(1),
GOOGLE_MAPS_API_KEY: z.string().min(1),
MAPBOX_ACCESS_TOKEN: z.string().min(1),
AZURE_TRANSLATOR_KEY: z.string().min(1),
AZURE_TRANSLATOR_LOCATION: z.string().min(1),
TRIPADVISOR_API_KEY: z.string().min(1),
AVIATION_STACK_API_KEY: z.string().min(1),
CRON_SECRET: z.string().min(1),
},
experimental__runtimeEnv: process.env,
})

View File

@ -1,85 +1,94 @@
// https://env.t3.gg/docs/nextjs#validate-schema-on-build-(recommended)
import { createJiti } from 'jiti'
import { fileURLToPath } from 'node:url'
const jiti = createJiti(fileURLToPath(import.meta.url))
// Import env here to validate during build. Using jiti we can import .ts files :)
jiti.import('./env/server')
jiti.import('./env/client')
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["geist"],
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
],
},
]
},
images: {
dangerouslyAllowSVG: true,
remotePatterns: [
{
protocol: 'https',
hostname: 'www.google.com',
port: '',
pathname: '/s2/favicons',
},
{
protocol: 'https',
hostname: 'api.producthunt.com',
port: '',
pathname: '/widgets/embed-image/v1/featured.svg',
},
{
protocol: 'https',
hostname: 'metwm7frkvew6tn1.public.blob.vercel-storage.com',
port: '',
pathname: "**"
},
// upload.wikimedia.org
{
protocol: 'https',
hostname: 'upload.wikimedia.org',
port: '',
pathname: '**'
},
// media.theresanaiforthat.com
{
protocol: 'https',
hostname: 'media.theresanaiforthat.com',
port: '',
pathname: '**'
},
// www.uneed.best
{
protocol: 'https',
hostname: 'www.uneed.best',
port: '',
pathname: '**'
},
// image.tmdb.org
{
protocol: 'https',
hostname: 'image.tmdb.org',
port: '',
pathname: '/t/p/original/**'
},
// image.tmdb.org
{
protocol: 'https',
hostname: 'image.tmdb.org',
port: '',
pathname: '/**'
},
]
},
};
transpilePackages: ['geist'],
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
],
},
]
},
images: {
dangerouslyAllowSVG: true,
remotePatterns: [
{
protocol: 'https',
hostname: 'www.google.com',
port: '',
pathname: '/s2/favicons',
},
{
protocol: 'https',
hostname: 'api.producthunt.com',
port: '',
pathname: '/widgets/embed-image/v1/featured.svg',
},
{
protocol: 'https',
hostname: 'metwm7frkvew6tn1.public.blob.vercel-storage.com',
port: '',
pathname: '**',
},
// upload.wikimedia.org
{
protocol: 'https',
hostname: 'upload.wikimedia.org',
port: '',
pathname: '**',
},
// media.theresanaiforthat.com
{
protocol: 'https',
hostname: 'media.theresanaiforthat.com',
port: '',
pathname: '**',
},
// www.uneed.best
{
protocol: 'https',
hostname: 'www.uneed.best',
port: '',
pathname: '**',
},
// image.tmdb.org
{
protocol: 'https',
hostname: 'image.tmdb.org',
port: '',
pathname: '/t/p/original/**',
},
// image.tmdb.org
{
protocol: 'https',
hostname: 'image.tmdb.org',
port: '',
pathname: '/**',
},
],
},
}
export default nextConfig;
export default nextConfig

View File

@ -29,6 +29,7 @@
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@t3-oss/env-nextjs": "^0.11.1",
"@tailwindcss/typography": "^0.5.13",
"@tavily/core": "^0.0.2",
"@types/katex": "^0.16.7",
@ -54,6 +55,7 @@
"geist": "^1.3.1",
"google-auth-library": "^9.14.1",
"highlight.js": "^11.10.0",
"jiti": "^2.4.2",
"katex": "^0.16.11",
"lucide-react": "^0.424.0",
"luxon": "^3.5.0",

File diff suppressed because it is too large Load Diff