diff --git a/.env.example b/.env.example index 5481ab55..2e558225 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,34 @@ +# ─── API ─────────────────────────────────────────── +NEXT_PUBLIC_API_URL=http://localhost:5000 +# Base URL of the dnb-backend REST API + +NEXT_PUBLIC_AI_API_URL=http://localhost:8000 +# Base URL of the dnb-ai FastAPI service for the AI assistant + +# ─── Stellar ─────────────────────────────────────── +NEXT_PUBLIC_STELLAR_NETWORK=testnet +# Stellar network to use: "testnet" or "mainnet" + +# ─── Cloudinary ──────────────────────────────────── NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name -NEXT_PUBLIC_CLOUDINARY_API_KEY=your_cloudinary_api_key -NEXT_PUBLIC_CLOUDINARY_API_SECRET=your_cloudinary_api_secret -NEXT_PUBLIC_CLOUDINARY_URL=your_cloudinary_url +# Cloudinary cloud name for unsigned uploads (required) + +# ─── Jitsi ───────────────────────────────────────── NEXT_PUBLIC_JITSI_DOMAIN=https://meet.jit.si +# Jitsi Meet domain for video spaces + +NEXT_PUBLIC_JITSI_REQUIRE_JWT=false +# Set to "true" if the Jitsi deployment requires a signed JWT token + +# ─── Firebase ───────────────────────────────────── +NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyC8LlmtlWXvbcbyVbdyv4r-tDsGhhukdag +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=deen-bridge-22195.firebaseapp.com +NEXT_PUBLIC_FIREBASE_PROJECT_ID=deen-bridge-22195 +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=deen-bridge-22195.firebasestorage.app +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=368531944242 +NEXT_PUBLIC_FIREBASE_APP_ID=1:368531944242:web:7994b11820741a69d35d2b +NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=G-ZZ81THLVCC +# Firebase Web SDK config values (not secrets, but environment-specific) # NODE_ENV=production DNB_API_URL=https://dnb-backend-api.onrender.com NEXT_PUBLIC_API_URL=https://dnb-backend-api.onrender.com diff --git a/README.md b/README.md index 7c91322b..f61604c7 100644 --- a/README.md +++ b/README.md @@ -86,12 +86,17 @@ The app runs at `http://localhost:3000`. ### Environment Variables -| Variable | Description | -|----------|-------------| -| `NEXT_PUBLIC_API_URL` | Base URL of the [dnb-backend](https://github.com/Deen-Bridge/dnb-backend) API | -| `NEXT_PUBLIC_STELLAR_NETWORK` | `testnet` or `public` (mainnet) | - -See `.env.example` for the full list. +| Variable | Required | Description | +|----------|----------|-------------| +| `NEXT_PUBLIC_API_URL` | No (default `http://localhost:5000`) | Base URL of the [dnb-backend](https://github.com/Deen-Bridge/dnb-backend) API | +| `NEXT_PUBLIC_AI_API_URL` | No (default `http://localhost:8000`) | Base URL of the [dnb-ai](https://github.com/Deen-Bridge/dnb-ai) AI assistant service | +| `NEXT_PUBLIC_STELLAR_NETWORK` | No (default `testnet`) | Stellar network — `testnet` or `mainnet` | +| `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME` | **Yes** | Cloudinary cloud name for unsigned image/video uploads | +| `NEXT_PUBLIC_JITSI_DOMAIN` | No (default `meet.jit.si`) | Jitsi Meet domain for live video spaces | +| `NEXT_PUBLIC_JITSI_REQUIRE_JWT` | No (default `false`) | Whether the Jitsi deployment requires a signed JWT token | +| `NEXT_PUBLIC_FIREBASE_*` | No | Firebase Web SDK config values (apiKey, authDomain, projectId, etc.) — defaults to the project's current values | + +See `.env.example` for the full variable list with example values. ### Scripts diff --git a/app/api/ai/chat/route.js b/app/api/ai/chat/route.js index 9339170a..994cf8f3 100644 --- a/app/api/ai/chat/route.js +++ b/app/api/ai/chat/route.js @@ -1,4 +1,5 @@ import axios from "axios"; +import { config } from "@/lib/config/env"; export async function POST(req) { try { @@ -6,9 +7,7 @@ export async function POST(req) { console.log("Sending message to AI:", message); console.log("Chat ID:", chat_id, "User ID:", user_id); - // Use environment variable or fallback to localhost - const AI_API_URL = - process.env.NEXT_PUBLIC_AI_API_URL || "http://localhost:8000"; + const AI_API_URL = config.aiApiUrl; const endpoint = `${AI_API_URL}/chat`; console.log("Using AI API URL:", endpoint); diff --git a/app/api/ai/stream/route.js b/app/api/ai/stream/route.js index d13ae29d..540d048a 100644 --- a/app/api/ai/stream/route.js +++ b/app/api/ai/stream/route.js @@ -1,4 +1,5 @@ +import { config } from "@/lib/config/env"; export async function POST(req) { try { @@ -10,9 +11,7 @@ export async function POST(req) { user_id, }); - // Use environment variable or fallback to localhost - const AI_API_URL = - process.env.NEXT_PUBLIC_AI_API_URL || "http://localhost:8000"; + const AI_API_URL = config.aiApiUrl; const endpoint = `${AI_API_URL}/chat/stream`; // Make request to AI backend diff --git a/app/api/books/[bookId]/preview/route.js b/app/api/books/[bookId]/preview/route.js index 62aa7dfe..9e7490c9 100644 --- a/app/api/books/[bookId]/preview/route.js +++ b/app/api/books/[bookId]/preview/route.js @@ -1,9 +1,9 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; +import { config } from "@/lib/config/env"; const getBackendBaseUrl = () => { - const base = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000"; - return base.replace(/\/$/, ""); + return config.apiUrl.replace(/\/$/, ""); }; export async function GET(_request, { params }) { diff --git a/components/organisms/dashboard/JaasMeetingClientSection.jsx b/components/organisms/dashboard/JaasMeetingClientSection.jsx index f54646fc..51a2a7da 100644 --- a/components/organisms/dashboard/JaasMeetingClientSection.jsx +++ b/components/organisms/dashboard/JaasMeetingClientSection.jsx @@ -9,6 +9,7 @@ import JaasMeetingComponent from "@/components/organisms/jitsi/JitsiMeeting"; import { joinSpaceWaitlist } from "@/lib/actions/spaces/joinSpaceWaitlist"; import { updateSpace } from "@/lib/actions/spaces/updateSpace"; import { getSpaceMeetingToken } from "@/lib/actions/calls/get-space-meeting-token"; +import { config } from "@/lib/config/env"; const normalizeDomain = (domain = "meet.jit.si") => domain.replace(/^https?:\/\//i, "").replace(/\/+$/g, ""); @@ -39,9 +40,7 @@ export default function JaasMeetingClientButtons({ space }) { const [isCopying, setIsCopying] = useState(false); const [joinLoading, setJoinLoading] = useState(false); const [tokenLoading, setTokenLoading] = useState(false); - const envRequiresJwt = - typeof process !== "undefined" && - process.env.NEXT_PUBLIC_JITSI_REQUIRE_JWT === "true"; + const envRequiresJwt = config.jitsiRequireJwt; const [meetingToken, setMeetingToken] = useState(null); const [requiresJwt, setRequiresJwt] = useState( @@ -52,8 +51,7 @@ export default function JaasMeetingClientButtons({ space }) { [requiresJwt, envRequiresJwt] ); const [meetingMeta, setMeetingMeta] = useState(() => { - const domain = - process.env.NEXT_PUBLIC_JITSI_DOMAIN || "meet.jit.si"; + const domain = config.jitsiDomain; const normalizedDomain = normalizeDomain(domain); const fallbackRoom = space?.meetingRoom || (space?._id ? `deenbridge-space-${space._id}` : ""); @@ -82,7 +80,7 @@ export default function JaasMeetingClientButtons({ space }) { }, [waitListIds, user?._id]); const baseDomain = useMemo( - () => normalizeDomain(process.env.NEXT_PUBLIC_JITSI_DOMAIN || "meet.jit.si"), + () => normalizeDomain(config.jitsiDomain), [] ); diff --git a/components/organisms/dashboard/ai/Ai-Sidebar.jsx b/components/organisms/dashboard/ai/Ai-Sidebar.jsx index 3826c8f0..b9d9727f 100644 --- a/components/organisms/dashboard/ai/Ai-Sidebar.jsx +++ b/components/organisms/dashboard/ai/Ai-Sidebar.jsx @@ -5,13 +5,13 @@ import { Plus, Trash2, MessageSquare } from "lucide-react"; import { toast } from "sonner"; import { formatDistanceToNow } from "date-fns"; import Button from "@/components/atoms/form/Button"; +import { config } from "@/lib/config/env"; export function AiSidebar({ onChatSelect, currentChatId, onNewChat }) { const [chatHistory, setChatHistory] = useState([]); const [loadingHistory, setLoadingHistory] = useState(false); const [userId, setUserId] = useState(null); - const AI_API_URL = - process.env.NEXT_PUBLIC_AI_API_URL || "http://localhost:8000"; + const AI_API_URL = config.aiApiUrl; useEffect(() => { const loadUserChats = async () => { diff --git a/components/stellar/StellarProvider.jsx b/components/stellar/StellarProvider.jsx index bd90baf1..f1ff73ce 100644 --- a/components/stellar/StellarProvider.jsx +++ b/components/stellar/StellarProvider.jsx @@ -16,8 +16,9 @@ import { AlbedoModule } from "@creit.tech/stellar-wallets-kit/modules/albedo"; import { toast } from "sonner"; import useAuth from "@/hooks/useAuth"; import axiosInstance from "@/lib/config/axios.config"; +import { config } from "@/lib/config/env"; -const NETWORK = process.env.NEXT_PUBLIC_STELLAR_NETWORK || "testnet"; +const NETWORK = config.stellarNetwork; const StellarContext = createContext(null); diff --git a/lib/actions/ai/load-chat-history.js b/lib/actions/ai/load-chat-history.js index a571035b..6058950b 100644 --- a/lib/actions/ai/load-chat-history.js +++ b/lib/actions/ai/load-chat-history.js @@ -1,4 +1,5 @@ import { cachedFetch, cacheKeys, CACHE_CONFIG } from "@/lib/utils/cache"; +import { config } from "@/lib/config/env"; /** * Load chat history for a specific user @@ -8,8 +9,7 @@ import { cachedFetch, cacheKeys, CACHE_CONFIG } from "@/lib/utils/cache"; */ export const loadChatHistory = async (userId, forceRefresh = false) => { try { - const AI_API_URL = - process.env.NEXT_PUBLIC_AI_API_URL || "http://localhost:8000"; + const AI_API_URL = config.aiApiUrl; const data = await cachedFetch(`${AI_API_URL}/user/${userId}/chats`, { cacheKey: cacheKeys.chatHistory(userId), @@ -33,8 +33,7 @@ export const loadChatHistory = async (userId, forceRefresh = false) => { */ export const loadChatMessages = async (chatId, forceRefresh = false) => { try { - const AI_API_URL = - process.env.NEXT_PUBLIC_AI_API_URL || "http://localhost:8000"; + const AI_API_URL = config.aiApiUrl; const history = await cachedFetch(`${AI_API_URL}/chat/${chatId}/history`, { cacheKey: cacheKeys.chatMessages(chatId), diff --git a/lib/actions/cached-api.js b/lib/actions/cached-api.js index 57884e1f..4a82f74d 100644 --- a/lib/actions/cached-api.js +++ b/lib/actions/cached-api.js @@ -4,8 +4,9 @@ import { CACHE_CONFIG, smartCache, } from "@/lib/utils/cache"; +import { config } from "@/lib/config/env"; -const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000"; +const API_URL = config.apiUrl; /** * Courses API with caching diff --git a/lib/config/axios.config.js b/lib/config/axios.config.js index d45f9078..4870202e 100644 --- a/lib/config/axios.config.js +++ b/lib/config/axios.config.js @@ -1,15 +1,7 @@ import axios from "axios"; +import { config } from "@/lib/config/env"; -const isDev = process.env.NODE_ENV === "development"; - -// Use environment variable if set, otherwise fallback to defaults -const baseURL = - process.env.NEXT_PUBLIC_API_URL || - (isDev ? "http://localhost:5000" : "https://dnb-backend-api.onrender.com"); - -console.log( - `🌐 API Base URL: ${baseURL} (${isDev ? "Development" : "Production"} mode)` -); +const baseURL = config.apiUrl; const axiosInstance = axios.create({ baseURL, diff --git a/lib/config/env.js b/lib/config/env.js new file mode 100644 index 00000000..dd6fb9e9 --- /dev/null +++ b/lib/config/env.js @@ -0,0 +1,133 @@ +import { z } from "zod"; + +function warn(field, fallback) { + if (process.env.NODE_ENV !== "production") { + console.warn( + `\u26a0\ufe0f ${field} is not set. Using "${fallback}" as fallback.` + ); + } +} + +const dangerous = Object.keys(process.env).filter((k) => + /^NEXT_PUBLIC_.*SECRET/.test(k) +); + +if (dangerous.length > 0) { + throw new Error( + `\u274c Dangerous environment variable(s) detected: ${dangerous.join(", ")}.\n` + + `Variables prefixed with NEXT_PUBLIC_ are exposed to the browser bundle and must never contain secrets.\n` + + `Rename the variable(s) to remove the NEXT_PUBLIC_ prefix.` + ); +} + +const raw = { + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, + NEXT_PUBLIC_AI_API_URL: process.env.NEXT_PUBLIC_AI_API_URL, + NEXT_PUBLIC_STELLAR_NETWORK: process.env.NEXT_PUBLIC_STELLAR_NETWORK, + NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: + process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, + NEXT_PUBLIC_JITSI_DOMAIN: process.env.NEXT_PUBLIC_JITSI_DOMAIN, + NEXT_PUBLIC_JITSI_REQUIRE_JWT: process.env.NEXT_PUBLIC_JITSI_REQUIRE_JWT, + NEXT_PUBLIC_FIREBASE_API_KEY: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: + process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + NEXT_PUBLIC_FIREBASE_PROJECT_ID: + process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: + process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: + process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + NEXT_PUBLIC_FIREBASE_APP_ID: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: + process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, +}; + +const envSchema = z.object({ + NEXT_PUBLIC_API_URL: z.string().url().optional(), + NEXT_PUBLIC_AI_API_URL: z.string().url().optional(), + NEXT_PUBLIC_STELLAR_NETWORK: z.enum(["testnet", "mainnet"]).optional(), + NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: z.string().optional(), + NEXT_PUBLIC_JITSI_DOMAIN: z.string().optional(), + NEXT_PUBLIC_JITSI_REQUIRE_JWT: z.enum(["true", "false"]).optional(), + NEXT_PUBLIC_FIREBASE_API_KEY: z.string().optional(), + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: z.string().optional(), + NEXT_PUBLIC_FIREBASE_PROJECT_ID: z.string().optional(), + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: z.string().optional(), + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: z.string().optional(), + NEXT_PUBLIC_FIREBASE_APP_ID: z.string().optional(), + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: z.string().optional(), +}); + +const parsed = envSchema.safeParse(raw); + +if (!parsed.success) { + const issues = parsed.error.errors.map((e) => { + const path = e.path.join("."); + return ` - ${path}: ${e.message}`; + }); + throw new Error( + `\u274c Environment configuration is invalid:\n${issues.join("\n")}\n\nPlease check your .env.local file.` + ); +} + +const env = parsed.data; + +export const config = Object.freeze({ + get apiUrl() { + return ( + env.NEXT_PUBLIC_API_URL ?? + (warn("NEXT_PUBLIC_API_URL", "http://localhost:5000"), + "http://localhost:5000") + ); + }, + + get aiApiUrl() { + return ( + env.NEXT_PUBLIC_AI_API_URL ?? + (warn("NEXT_PUBLIC_AI_API_URL", "http://localhost:8000"), + "http://localhost:8000") + ); + }, + + get stellarNetwork() { + return env.NEXT_PUBLIC_STELLAR_NETWORK ?? "testnet"; + }, + + get cloudinaryCloudName() { + if (!env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME) { + warn("NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME", "(none)"); + } + return env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME; + }, + + get jitsiDomain() { + return env.NEXT_PUBLIC_JITSI_DOMAIN ?? "meet.jit.si"; + }, + + get jitsiRequireJwt() { + return env.NEXT_PUBLIC_JITSI_REQUIRE_JWT === "true"; + }, + + get firebase() { + return { + apiKey: + env.NEXT_PUBLIC_FIREBASE_API_KEY ?? + "AIzaSyC8LlmtlWXvbcbyVbdyv4r-tDsGhhukdag", + authDomain: + env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN ?? + "deen-bridge-22195.firebaseapp.com", + projectId: + env.NEXT_PUBLIC_FIREBASE_PROJECT_ID ?? "deen-bridge-22195", + storageBucket: + env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET ?? + "deen-bridge-22195.firebasestorage.app", + messagingSenderId: + env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID ?? "368531944242", + appId: + env.NEXT_PUBLIC_FIREBASE_APP_ID ?? + "1:368531944242:web:7994b11820741a69d35d2b", + measurementId: + env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID ?? "G-ZZ81THLVCC", + }; + }, +}); diff --git a/lib/config/firebase.config.js b/lib/config/firebase.config.js index bf91e266..9aef285b 100644 --- a/lib/config/firebase.config.js +++ b/lib/config/firebase.config.js @@ -1,15 +1,8 @@ import { initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; +import { config } from "@/lib/config/env"; -const firebaseConfig = { - apiKey: "AIzaSyC8LlmtlWXvbcbyVbdyv4r-tDsGhhukdag", - authDomain: "deen-bridge-22195.firebaseapp.com", - projectId: "deen-bridge-22195", - storageBucket: "deen-bridge-22195.firebasestorage.app", - messagingSenderId: "368531944242", - appId: "1:368531944242:web:7994b11820741a69d35d2b", - measurementId: "G-ZZ81THLVCC", -}; +const firebaseConfig = config.firebase; const app = initializeApp(firebaseConfig); const db = getFirestore(app); diff --git a/lib/utils/cloudinaryUpload.js b/lib/utils/cloudinaryUpload.js index 8c839f0e..69f46269 100644 --- a/lib/utils/cloudinaryUpload.js +++ b/lib/utils/cloudinaryUpload.js @@ -1,4 +1,5 @@ import axios from "axios"; +import { config } from "@/lib/config/env"; /** * Upload file directly to Cloudinary using axios @@ -8,7 +9,7 @@ import axios from "axios"; * @returns {Promise} - Upload result with secure_url */ export const uploadToCloudinary = async (file, uploadPreset, onProgress) => { - const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME; + const cloudName = config.cloudinaryCloudName; const url = `https://api.cloudinary.com/v1_1/${cloudName}/upload`; if (!cloudName) { @@ -55,7 +56,7 @@ export const uploadToCloudinary = async (file, uploadPreset, onProgress) => { * @returns {Promise} - Upload result */ export const uploadWithProgress = async (file, uploadPreset, onProgress) => { - const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME; + const cloudName = config.cloudinaryCloudName; const url = `https://api.cloudinary.com/v1_1/${cloudName}/upload`; if (!cloudName) {