feat: replace nextjs-config with environment variables (#632)

* chore: remove nextjs-config

* build fix

* update readme

* explicit nextjs env vars + remove feature-unrelated things and obsolete vars from config

* full config removal

* remove force-dynamic from pages

* compile fix

* restore claude-deleted tests

* better .env.example

---------

Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
This commit is contained in:
Igor Monadical
2025-09-11 11:20:41 -04:00
committed by GitHub
parent fc363bd49b
commit 369ecdff13
25 changed files with 755 additions and 2159 deletions

View File

@@ -66,7 +66,6 @@ pnpm install
# Copy configuration templates # Copy configuration templates
cp .env_template .env cp .env_template .env
cp config-template.ts config.ts
``` ```
**Development:** **Development:**

View File

@@ -99,11 +99,10 @@ Start with `cd www`.
```bash ```bash
pnpm install pnpm install
cp .env_template .env cp .env.example .env
cp config-template.ts config.ts
``` ```
Then, fill in the environment variables in `.env` and the configuration in `config.ts` as needed. If you are unsure on how to proceed, ask in Zulip. Then, fill in the environment variables in `.env` as needed. If you are unsure on how to proceed, ask in Zulip.
**Run in development mode** **Run in development mode**
@@ -168,3 +167,34 @@ You can manually process an audio file by calling the process tool:
```bash ```bash
uv run python -m reflector.tools.process path/to/audio.wav uv run python -m reflector.tools.process path/to/audio.wav
``` ```
## Feature Flags
Reflector uses environment variable-based feature flags to control application functionality. These flags allow you to enable or disable features without code changes.
### Available Feature Flags
| Feature Flag | Environment Variable |
|-------------|---------------------|
| `requireLogin` | `NEXT_PUBLIC_FEATURE_REQUIRE_LOGIN` |
| `privacy` | `NEXT_PUBLIC_FEATURE_PRIVACY` |
| `browse` | `NEXT_PUBLIC_FEATURE_BROWSE` |
| `sendToZulip` | `NEXT_PUBLIC_FEATURE_SEND_TO_ZULIP` |
| `rooms` | `NEXT_PUBLIC_FEATURE_ROOMS` |
### Setting Feature Flags
Feature flags are controlled via environment variables using the pattern `NEXT_PUBLIC_FEATURE_{FEATURE_NAME}` where `{FEATURE_NAME}` is the SCREAMING_SNAKE_CASE version of the feature name.
**Examples:**
```bash
# Enable user authentication requirement
NEXT_PUBLIC_FEATURE_REQUIRE_LOGIN=true
# Disable browse functionality
NEXT_PUBLIC_FEATURE_BROWSE=false
# Enable Zulip integration
NEXT_PUBLIC_FEATURE_SEND_TO_ZULIP=true
```

34
www/.env.example Normal file
View File

@@ -0,0 +1,34 @@
# Environment
ENVIRONMENT=development
NEXT_PUBLIC_ENV=development
# Site Configuration
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Nextauth envs
# not used in app code but in lib code
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-nextauth-secret-here
# / Nextauth envs
# Authentication (Authentik OAuth/OIDC)
AUTHENTIK_ISSUER=https://authentik.example.com/application/o/reflector
AUTHENTIK_REFRESH_TOKEN_URL=https://authentik.example.com/application/o/token/
AUTHENTIK_CLIENT_ID=your-client-id-here
AUTHENTIK_CLIENT_SECRET=your-client-secret-here
# Feature Flags
# NEXT_PUBLIC_FEATURE_REQUIRE_LOGIN=true
# NEXT_PUBLIC_FEATURE_PRIVACY=false
# NEXT_PUBLIC_FEATURE_BROWSE=true
# NEXT_PUBLIC_FEATURE_SEND_TO_ZULIP=true
# NEXT_PUBLIC_FEATURE_ROOMS=true
# API URLs
NEXT_PUBLIC_API_URL=http://127.0.0.1:1250
NEXT_PUBLIC_WEBSOCKET_URL=ws://127.0.0.1:1250
NEXT_PUBLIC_AUTH_CALLBACK_URL=http://localhost:3000/auth-callback
# Sentry
# SENTRY_DSN=https://your-dsn@sentry.io/project-id
# SENTRY_IGNORE_API_RESOLUTION_ERROR=1

1
www/.gitignore vendored
View File

@@ -40,7 +40,6 @@ next-env.d.ts
# Sentry Auth Token # Sentry Auth Token
.sentryclirc .sentryclirc
config.ts
# openapi logs # openapi logs
openapi-ts-error-*.log openapi-ts-error-*.log

View File

@@ -1,5 +1,5 @@
import { Container, Flex, Link } from "@chakra-ui/react"; import { Container, Flex, Link } from "@chakra-ui/react";
import { getConfig } from "../lib/edgeConfig"; import { featureEnabled } from "../lib/features";
import NextLink from "next/link"; import NextLink from "next/link";
import Image from "next/image"; import Image from "next/image";
import UserInfo from "../(auth)/userInfo"; import UserInfo from "../(auth)/userInfo";
@@ -11,8 +11,6 @@ export default async function AppLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const config = await getConfig();
const { requireLogin, privacy, browse, rooms } = config.features;
return ( return (
<Container <Container
minW="100vw" minW="100vw"
@@ -58,7 +56,7 @@ export default async function AppLayout({
> >
Create Create
</Link> </Link>
{browse ? ( {featureEnabled("browse") ? (
<> <>
&nbsp;·&nbsp; &nbsp;·&nbsp;
<Link href="/browse" as={NextLink} className="font-light px-2"> <Link href="/browse" as={NextLink} className="font-light px-2">
@@ -68,7 +66,7 @@ export default async function AppLayout({
) : ( ) : (
<></> <></>
)} )}
{rooms ? ( {featureEnabled("rooms") ? (
<> <>
&nbsp;·&nbsp; &nbsp;·&nbsp;
<Link href="/rooms" as={NextLink} className="font-light px-2"> <Link href="/rooms" as={NextLink} className="font-light px-2">
@@ -78,7 +76,7 @@ export default async function AppLayout({
) : ( ) : (
<></> <></>
)} )}
{requireLogin ? ( {featureEnabled("requireLogin") ? (
<> <>
&nbsp;·&nbsp; &nbsp;·&nbsp;
<UserInfo /> <UserInfo />

View File

@@ -3,10 +3,11 @@ import ScrollToBottom from "../../scrollToBottom";
import { Topic } from "../../webSocketTypes"; import { Topic } from "../../webSocketTypes";
import useParticipants from "../../useParticipants"; import useParticipants from "../../useParticipants";
import { Box, Flex, Text, Accordion } from "@chakra-ui/react"; import { Box, Flex, Text, Accordion } from "@chakra-ui/react";
import { featureEnabled } from "../../../../domainContext";
import { TopicItem } from "./TopicItem"; import { TopicItem } from "./TopicItem";
import { TranscriptStatus } from "../../../../lib/transcript"; import { TranscriptStatus } from "../../../../lib/transcript";
import { featureEnabled } from "../../../../lib/features";
type TopicListProps = { type TopicListProps = {
topics: Topic[]; topics: Topic[];
useActiveTopic: [ useActiveTopic: [

View File

@@ -9,7 +9,6 @@ import { useRouter } from "next/navigation";
import useCreateTranscript from "../createTranscript"; import useCreateTranscript from "../createTranscript";
import SelectSearch from "react-select-search"; import SelectSearch from "react-select-search";
import { supportedLanguages } from "../../../supportedLanguages"; import { supportedLanguages } from "../../../supportedLanguages";
import { featureEnabled } from "../../../domainContext";
import { import {
Flex, Flex,
Box, Box,
@@ -21,10 +20,9 @@ import {
Spacer, Spacer,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useAuth } from "../../../lib/AuthProvider"; import { useAuth } from "../../../lib/AuthProvider";
import type { components } from "../../../reflector-api"; import { featureEnabled } from "../../../lib/features";
const TranscriptCreate = () => { const TranscriptCreate = () => {
const isClient = typeof window !== "undefined";
const router = useRouter(); const router = useRouter();
const auth = useAuth(); const auth = useAuth();
const isAuthenticated = auth.status === "authenticated"; const isAuthenticated = auth.status === "authenticated";
@@ -176,7 +174,7 @@ const TranscriptCreate = () => {
placeholder="Choose your language" placeholder="Choose your language"
/> />
</Box> </Box>
{isClient && !loading ? ( {!loading ? (
permissionOk ? ( permissionOk ? (
<Spacer /> <Spacer />
) : permissionDenied ? ( ) : permissionDenied ? (

View File

@@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { featureEnabled } from "../../domainContext";
import { ShareMode, toShareMode } from "../../lib/shareMode"; import { ShareMode, toShareMode } from "../../lib/shareMode";
import type { components } from "../../reflector-api"; import type { components } from "../../reflector-api";
@@ -24,6 +23,8 @@ import ShareCopy from "./shareCopy";
import ShareZulip from "./shareZulip"; import ShareZulip from "./shareZulip";
import { useAuth } from "../../lib/AuthProvider"; import { useAuth } from "../../lib/AuthProvider";
import { featureEnabled } from "../../lib/features";
type ShareAndPrivacyProps = { type ShareAndPrivacyProps = {
finalSummaryRef: any; finalSummaryRef: any;
transcriptResponse: GetTranscript; transcriptResponse: GetTranscript;

View File

@@ -1,8 +1,9 @@
import React, { useState, useRef, useEffect, use } from "react"; import React, { useState, useRef, useEffect, use } from "react";
import { featureEnabled } from "../../domainContext";
import { Button, Flex, Input, Text } from "@chakra-ui/react"; import { Button, Flex, Input, Text } from "@chakra-ui/react";
import QRCode from "react-qr-code"; import QRCode from "react-qr-code";
import { featureEnabled } from "../../lib/features";
type ShareLinkProps = { type ShareLinkProps = {
transcriptId: string; transcriptId: string;
}; };

View File

@@ -1,5 +1,4 @@
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { featureEnabled } from "../../domainContext";
import type { components } from "../../reflector-api"; import type { components } from "../../reflector-api";
type GetTranscript = components["schemas"]["GetTranscript"]; type GetTranscript = components["schemas"]["GetTranscript"];
@@ -25,6 +24,8 @@ import {
useTranscriptPostToZulip, useTranscriptPostToZulip,
} from "../../lib/apiHooks"; } from "../../lib/apiHooks";
import { featureEnabled } from "../../lib/features";
type ShareZulipProps = { type ShareZulipProps = {
transcriptResponse: GetTranscript; transcriptResponse: GetTranscript;
topicsResponse: GetTranscriptTopic[]; topicsResponse: GetTranscriptTopic[];

View File

@@ -1,7 +1,7 @@
import { useContext, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { DomainContext } from "../../domainContext";
import { useTranscriptGet } from "../../lib/apiHooks"; import { useTranscriptGet } from "../../lib/apiHooks";
import { useAuth } from "../../lib/AuthProvider"; import { useAuth } from "../../lib/AuthProvider";
import { API_URL } from "../../lib/apiClient";
export type Mp3Response = { export type Mp3Response = {
media: HTMLMediaElement | null; media: HTMLMediaElement | null;
@@ -19,7 +19,6 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
null, null,
); );
const [audioDeleted, setAudioDeleted] = useState<boolean | null>(null); const [audioDeleted, setAudioDeleted] = useState<boolean | null>(null);
const { api_url } = useContext(DomainContext);
const auth = useAuth(); const auth = useAuth();
const accessTokenInfo = const accessTokenInfo =
auth.status === "authenticated" ? auth.accessToken : null; auth.status === "authenticated" ? auth.accessToken : null;
@@ -78,7 +77,7 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
// Audio is not deleted, proceed to load it // Audio is not deleted, proceed to load it
audioElement = document.createElement("audio"); audioElement = document.createElement("audio");
audioElement.src = `${api_url}/v1/transcripts/${transcriptId}/audio/mp3`; audioElement.src = `${API_URL}/v1/transcripts/${transcriptId}/audio/mp3`;
audioElement.crossOrigin = "anonymous"; audioElement.crossOrigin = "anonymous";
audioElement.preload = "auto"; audioElement.preload = "auto";
@@ -110,7 +109,7 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
if (handleError) audioElement.removeEventListener("error", handleError); if (handleError) audioElement.removeEventListener("error", handleError);
} }
}; };
}, [transcriptId, transcript, later, api_url]); }, [transcriptId, transcript, later]);
const getNow = () => { const getNow = () => {
setLater(false); setLater(false);

View File

@@ -1,13 +1,12 @@
import { useContext, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Topic, FinalSummary, Status } from "./webSocketTypes"; import { Topic, FinalSummary, Status } from "./webSocketTypes";
import { useError } from "../../(errors)/errorContext"; import { useError } from "../../(errors)/errorContext";
import { DomainContext } from "../../domainContext";
import type { components } from "../../reflector-api"; import type { components } from "../../reflector-api";
type AudioWaveform = components["schemas"]["AudioWaveform"]; type AudioWaveform = components["schemas"]["AudioWaveform"];
type GetTranscriptSegmentTopic = type GetTranscriptSegmentTopic =
components["schemas"]["GetTranscriptSegmentTopic"]; components["schemas"]["GetTranscriptSegmentTopic"];
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { $api } from "../../lib/apiClient"; import { $api, WEBSOCKET_URL } from "../../lib/apiClient";
export type UseWebSockets = { export type UseWebSockets = {
transcriptTextLive: string; transcriptTextLive: string;
@@ -37,7 +36,6 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
const [status, setStatus] = useState<Status | null>(null); const [status, setStatus] = useState<Status | null>(null);
const { setError } = useError(); const { setError } = useError();
const { websocket_url: websocketUrl } = useContext(DomainContext);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [accumulatedText, setAccumulatedText] = useState<string>(""); const [accumulatedText, setAccumulatedText] = useState<string>("");
@@ -328,7 +326,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
if (!transcriptId) return; if (!transcriptId) return;
const url = `${websocketUrl}/v1/transcripts/${transcriptId}/events`; const url = `${WEBSOCKET_URL}/v1/transcripts/${transcriptId}/events`;
let ws = new WebSocket(url); let ws = new WebSocket(url);
ws.onopen = () => { ws.onopen = () => {
@@ -494,7 +492,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
return () => { return () => {
ws.close(); ws.close();
}; };
}, [transcriptId, websocketUrl]); }, [transcriptId]);
return { return {
transcriptTextLive, transcriptTextLive,

View File

@@ -1,49 +0,0 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { DomainConfig } from "./lib/edgeConfig";
type DomainContextType = Omit<DomainConfig, "auth_callback_url">;
export const DomainContext = createContext<DomainContextType>({
features: {
requireLogin: false,
privacy: true,
browse: false,
sendToZulip: false,
},
api_url: "",
websocket_url: "",
});
export const DomainContextProvider = ({
config,
children,
}: {
config: DomainConfig;
children: any;
}) => {
const [context, setContext] = useState<DomainContextType>();
useEffect(() => {
if (!config) return;
const { auth_callback_url, ...others } = config;
setContext(others);
}, [config]);
if (!context) return;
return (
<DomainContext.Provider value={context}>{children}</DomainContext.Provider>
);
};
// Get feature config client-side with
export const featureEnabled = (
featureName: "requireLogin" | "privacy" | "browse" | "sendToZulip",
) => {
const context = useContext(DomainContext);
return context.features[featureName] as boolean | undefined;
};
// Get config server-side (out of react) : see lib/edgeConfig.

View File

@@ -3,9 +3,7 @@ import { Metadata, Viewport } from "next";
import { Poppins } from "next/font/google"; import { Poppins } from "next/font/google";
import { ErrorProvider } from "./(errors)/errorContext"; import { ErrorProvider } from "./(errors)/errorContext";
import ErrorMessage from "./(errors)/errorMessage"; import ErrorMessage from "./(errors)/errorMessage";
import { DomainContextProvider } from "./domainContext";
import { RecordingConsentProvider } from "./recordingConsentContext"; import { RecordingConsentProvider } from "./recordingConsentContext";
import { getConfig } from "./lib/edgeConfig";
import { ErrorBoundary } from "@sentry/nextjs"; import { ErrorBoundary } from "@sentry/nextjs";
import { Providers } from "./providers"; import { Providers } from "./providers";
@@ -68,12 +66,9 @@ export default async function RootLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const config = await getConfig();
return ( return (
<html lang="en" className={poppins.className} suppressHydrationWarning> <html lang="en" className={poppins.className} suppressHydrationWarning>
<body className={"h-[100svh] w-[100svw] overflow-x-hidden relative"}> <body className={"h-[100svh] w-[100svw] overflow-x-hidden relative"}>
<DomainContextProvider config={config}>
<RecordingConsentProvider> <RecordingConsentProvider>
<ErrorBoundary fallback={<p>"something went really wrong"</p>}> <ErrorBoundary fallback={<p>"something went really wrong"</p>}>
<ErrorProvider> <ErrorProvider>
@@ -82,7 +77,6 @@ export default async function RootLayout({
</ErrorProvider> </ErrorProvider>
</ErrorBoundary> </ErrorBoundary>
</RecordingConsentProvider> </RecordingConsentProvider>
</DomainContextProvider>
</body> </body>
</html> </html>
); );

View File

@@ -6,10 +6,14 @@ import createFetchClient from "openapi-react-query";
import { assertExistsAndNonEmptyString } from "./utils"; import { assertExistsAndNonEmptyString } from "./utils";
import { isBuildPhase } from "./next"; import { isBuildPhase } from "./next";
const API_URL = !isBuildPhase export const API_URL = !isBuildPhase
? assertExistsAndNonEmptyString(process.env.NEXT_PUBLIC_API_URL) ? assertExistsAndNonEmptyString(process.env.NEXT_PUBLIC_API_URL)
: "http://localhost"; : "http://localhost";
// TODO decide strict validation or not
export const WEBSOCKET_URL =
process.env.NEXT_PUBLIC_WEBSOCKET_URL || "ws://127.0.0.1:1250";
export const client = createClient<paths>({ export const client = createClient<paths>({
baseUrl: API_URL, baseUrl: API_URL,
}); });

View File

@@ -1,3 +1,5 @@
import { assertExistsAndNonEmptyString } from "./utils";
export const REFRESH_ACCESS_TOKEN_ERROR = "RefreshAccessTokenError" as const; export const REFRESH_ACCESS_TOKEN_ERROR = "RefreshAccessTokenError" as const;
// 4 min is 1 min less than default authentic value. here we assume that authentic won't be set to access tokens < 4 min // 4 min is 1 min less than default authentic value. here we assume that authentic won't be set to access tokens < 4 min
export const REFRESH_ACCESS_TOKEN_BEFORE = 4 * 60 * 1000; export const REFRESH_ACCESS_TOKEN_BEFORE = 4 * 60 * 1000;

View File

@@ -1,54 +0,0 @@
import { get } from "@vercel/edge-config";
import { isBuildPhase } from "./next";
type EdgeConfig = {
[domainWithDash: string]: {
features: {
[featureName in
| "requireLogin"
| "privacy"
| "browse"
| "sendToZulip"]: boolean;
};
auth_callback_url: string;
websocket_url: string;
api_url: string;
};
};
export type DomainConfig = EdgeConfig["domainWithDash"];
// Edge config main keys can only be alphanumeric and _ or -
export function edgeKeyToDomain(key: string) {
return key.replaceAll("_", ".");
}
export function edgeDomainToKey(domain: string) {
return domain.replaceAll(".", "_");
}
// get edge config server-side (prefer DomainContext when available), domain is the hostname
export async function getConfig() {
if (process.env.NEXT_PUBLIC_ENV === "development") {
try {
return require("../../config").localConfig;
} catch (e) {
// next build() WILL try to execute the require above even if conditionally protected
// but thank god it at least runs catch{} block properly
if (!isBuildPhase) throw new Error(e);
return require("../../config-template").localConfig;
}
}
const domain = new URL(process.env.NEXT_PUBLIC_SITE_URL!).hostname;
let config = await get(edgeDomainToKey(domain));
if (typeof config !== "object") {
console.warn("No config for this domain, falling back to default");
config = await get(edgeDomainToKey("default"));
}
if (typeof config !== "object") throw Error("Error fetching config");
return config as DomainConfig;
}

55
www/app/lib/features.ts Normal file
View File

@@ -0,0 +1,55 @@
export const FEATURES = [
"requireLogin",
"privacy",
"browse",
"sendToZulip",
"rooms",
] as const;
export type FeatureName = (typeof FEATURES)[number];
export type Features = Readonly<Record<FeatureName, boolean>>;
export const DEFAULT_FEATURES: Features = {
requireLogin: false,
privacy: true,
browse: false,
sendToZulip: false,
rooms: false,
} as const;
function parseBooleanEnv(
value: string | undefined,
defaultValue: boolean = false,
): boolean {
if (!value) return defaultValue;
return value.toLowerCase() === "true";
}
// WARNING: keep process.env.* as-is, next.js won't see them if you generate dynamically
const features: Features = {
requireLogin: parseBooleanEnv(
process.env.NEXT_PUBLIC_FEATURE_REQUIRE_LOGIN,
DEFAULT_FEATURES.requireLogin,
),
privacy: parseBooleanEnv(
process.env.NEXT_PUBLIC_FEATURE_PRIVACY,
DEFAULT_FEATURES.privacy,
),
browse: parseBooleanEnv(
process.env.NEXT_PUBLIC_FEATURE_BROWSE,
DEFAULT_FEATURES.browse,
),
sendToZulip: parseBooleanEnv(
process.env.NEXT_PUBLIC_FEATURE_SEND_TO_ZULIP,
DEFAULT_FEATURES.sendToZulip,
),
rooms: parseBooleanEnv(
process.env.NEXT_PUBLIC_FEATURE_ROOMS,
DEFAULT_FEATURES.rooms,
),
};
export const featureEnabled = (featureName: FeatureName): boolean => {
return features[featureName];
};

View File

@@ -72,3 +72,7 @@ export const assertCustomSession = <S extends Session>(s: S): CustomSession => {
// no other checks for now // no other checks for now
return r as CustomSession; return r as CustomSession;
}; };
export type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

View File

@@ -171,5 +171,6 @@ export const assertNotExists = <T>(
export const assertExistsAndNonEmptyString = ( export const assertExistsAndNonEmptyString = (
value: string | null | undefined, value: string | null | undefined,
err?: string,
): NonEmptyString => ): NonEmptyString =>
parseNonEmptyString(assertExists(value, "Expected non-empty string")); parseNonEmptyString(assertExists(value, err || "Expected non-empty string"));

View File

@@ -2,8 +2,8 @@
import { ChakraProvider } from "@chakra-ui/react"; import { ChakraProvider } from "@chakra-ui/react";
import system from "./styles/theme"; import system from "./styles/theme";
import dynamic from "next/dynamic";
import { WherebyProvider } from "@whereby.com/browser-sdk/react";
import { Toaster } from "./components/ui/toaster"; import { Toaster } from "./components/ui/toaster";
import { NuqsAdapter } from "nuqs/adapters/next/app"; import { NuqsAdapter } from "nuqs/adapters/next/app";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
@@ -11,6 +11,14 @@ import { queryClient } from "./lib/queryClient";
import { AuthProvider } from "./lib/AuthProvider"; import { AuthProvider } from "./lib/AuthProvider";
import { SessionProvider as SessionProviderNextAuth } from "next-auth/react"; import { SessionProvider as SessionProviderNextAuth } from "next-auth/react";
const WherebyProvider = dynamic(
() =>
import("@whereby.com/browser-sdk/react").then((mod) => ({
default: mod.WherebyProvider,
})),
{ ssr: false },
);
export function Providers({ children }: { children: React.ReactNode }) { export function Providers({ children }: { children: React.ReactNode }) {
return ( return (
<NuqsAdapter> <NuqsAdapter>

View File

@@ -1,13 +0,0 @@
export const localConfig = {
features: {
requireLogin: true,
privacy: true,
browse: true,
sendToZulip: true,
rooms: true,
},
api_url: "http://127.0.0.1:1250",
websocket_url: "ws://127.0.0.1:1250",
auth_callback_url: "http://localhost:3000/auth-callback",
zulip_streams: "", // Find the value on zulip
};

View File

@@ -1,5 +1,5 @@
import { withAuth } from "next-auth/middleware"; import { withAuth } from "next-auth/middleware";
import { getConfig } from "./app/lib/edgeConfig"; import { featureEnabled } from "./app/lib/features";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { PROTECTED_PAGES } from "./app/lib/auth"; import { PROTECTED_PAGES } from "./app/lib/auth";
@@ -19,13 +19,12 @@ export const config = {
export default withAuth( export default withAuth(
async function middleware(request) { async function middleware(request) {
const config = await getConfig();
const pathname = request.nextUrl.pathname; const pathname = request.nextUrl.pathname;
// feature-flags protected paths // feature-flags protected paths
if ( if (
(!config.features.browse && pathname.startsWith("/browse")) || (!featureEnabled("browse") && pathname.startsWith("/browse")) ||
(!config.features.rooms && pathname.startsWith("/rooms")) (!featureEnabled("rooms") && pathname.startsWith("/rooms"))
) { ) {
return NextResponse.redirect(request.nextUrl.origin); return NextResponse.redirect(request.nextUrl.origin);
} }
@@ -33,10 +32,8 @@ export default withAuth(
{ {
callbacks: { callbacks: {
async authorized({ req, token }) { async authorized({ req, token }) {
const config = await getConfig();
if ( if (
config.features.requireLogin && featureEnabled("requireLogin") &&
PROTECTED_PAGES.test(req.nextUrl.pathname) PROTECTED_PAGES.test(req.nextUrl.pathname)
) { ) {
return !!token; return !!token;

View File

@@ -20,7 +20,6 @@
"@sentry/nextjs": "^7.77.0", "@sentry/nextjs": "^7.77.0",
"@tanstack/react-query": "^5.85.9", "@tanstack/react-query": "^5.85.9",
"@types/ioredis": "^5.0.0", "@types/ioredis": "^5.0.0",
"@vercel/edge-config": "^0.4.1",
"@whereby.com/browser-sdk": "^3.3.4", "@whereby.com/browser-sdk": "^3.3.4",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"axios": "^1.8.2", "axios": "^1.8.2",
@@ -63,8 +62,7 @@
"jest": "^30.1.3", "jest": "^30.1.3",
"openapi-typescript": "^7.9.1", "openapi-typescript": "^7.9.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"ts-jest": "^29.4.1", "ts-jest": "^29.4.1"
"vercel": "^37.3.0"
}, },
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748" "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
} }

2562
www/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff