Implement multi tenancy

This commit is contained in:
Sara
2023-10-27 11:26:00 +02:00
committed by Mathieu Virbel
parent 6bd5247bab
commit 4a69bffc9c
37 changed files with 409 additions and 236 deletions

View File

@@ -1,13 +1,9 @@
"use client"; "use client";
import { import { useFiefIsAuthenticated } from "@fief/fief/nextjs/react";
useFiefIsAuthenticated,
useFiefUserinfo,
} from "@fief/fief/nextjs/react";
import Link from "next/link"; import Link from "next/link";
export default function UserInfo() { export default function UserInfo() {
const isAuthenticated = useFiefIsAuthenticated(); const isAuthenticated = useFiefIsAuthenticated();
const userinfo = useFiefUserinfo();
return !isAuthenticated ? ( return !isAuthenticated ? (
<span className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"> <span className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2">

View File

@@ -0,0 +1,10 @@
import { NextApiRequest } from "next";
import { fief, getFiefAuth } from "../../lib/fief";
// type FiefNextApiHandler<T> = (req: NextApiRequest & AuthenticateRequestResult, res: NextApiResponse<T>) => unknown | Promise<unknown>;
export default (req: any, res) => {
const domain = req.url;
console.log("user", req.url, getFiefAuth("localhost").currentUser());
return getFiefAuth("localhost").currentUser()(req, res);
};
// export default fief.currentUser()

View File

@@ -1,17 +1,19 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import getApi from "../lib/getApi"; import getApi from "../../lib/getApi";
import { import {
PageGetTranscript, PageGetTranscript,
GetTranscript, GetTranscript,
GetTranscriptFromJSON, GetTranscriptFromJSON,
} from "../api"; } from "../../api";
import { Title } from "../lib/textComponents"; import { Title } from "../../lib/textComponents";
import Pagination from "./pagination"; import Pagination from "./pagination";
import Link from "next/link"; import Link from "next/link";
import { useFiefIsAuthenticated } from "@fief/fief/nextjs/react"; import { useFiefIsAuthenticated } from "@fief/fief/nextjs/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGear } from "@fortawesome/free-solid-svg-icons"; import { faGear } from "@fortawesome/free-solid-svg-icons";
import { featureEnabled } from "../domainContext";
import router from "next/router";
export default function TranscriptBrowser() { export default function TranscriptBrowser() {
const api = getApi(); const api = getApi();
@@ -19,6 +21,7 @@ export default function TranscriptBrowser() {
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const isAuthenticated = useFiefIsAuthenticated(); const isAuthenticated = useFiefIsAuthenticated();
const browseEnabled = featureEnabled("browse");
useEffect(() => { useEffect(() => {
if (!isAuthenticated) return; if (!isAuthenticated) return;

View File

@@ -0,0 +1,49 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
type DomainContextType = {
features: {
requireLogin: boolean;
privacy: boolean;
browse: boolean;
};
apiUrl: string | null;
};
export const DomainContext = createContext<DomainContextType>({
features: {
requireLogin: false,
privacy: true,
browse: false,
},
apiUrl: null,
});
export const DomainContextProvider = ({ config, children }) => {
const [context, setContext] = useState<DomainContextType>();
useEffect(() => {
if (!config) return;
setContext({
features: {
requireLogin: !!config["features"]["requireLogin"],
privacy: !!config["features"]["privacy"],
browse: !!config["features"]["browse"],
},
apiUrl: config["api_url"],
});
}, [config]);
if (!context) return;
return (
<DomainContext.Provider value={context}>{children}</DomainContext.Provider>
);
};
export const featureEnabled = (
featureName: "requireLogin" | "privacy" | "browse",
) => {
const context = useContext(DomainContext);
return context.features[featureName] as boolean | undefined;
};

162
www/app/[domain]/layout.tsx Normal file
View File

@@ -0,0 +1,162 @@
import "../styles/globals.scss";
import { Poppins } from "next/font/google";
import { Metadata } from "next";
import FiefWrapper from "../(auth)/fiefWrapper";
import UserInfo from "../(auth)/userInfo";
import { ErrorProvider } from "../(errors)/errorContext";
import ErrorMessage from "../(errors)/errorMessage";
import Image from "next/image";
import Link from "next/link";
import About from "../(aboutAndPrivacy)/about";
import Privacy from "../(aboutAndPrivacy)/privacy";
import { get } from "@vercel/edge-config";
import { DomainContextProvider } from "./domainContext";
const poppins = Poppins({ subsets: ["latin"], weight: ["200", "400", "600"] });
export const metadata: Metadata = {
title: {
template: "%s Reflector",
default: "Reflector - AI-Powered Meeting Transcriptions by Monadical",
},
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise",
applicationName: "Reflector",
referrer: "origin-when-cross-origin",
keywords: ["Reflector", "Monadical", "AI", "Meetings", "Transcription"],
authors: [{ name: "Monadical Team", url: "https://monadical.com/team.html" }],
formatDetection: {
email: false,
address: false,
telephone: false,
},
openGraph: {
title: "Reflector",
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise.",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Reflector",
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise.",
images: ["/r-icon.png"],
},
icons: {
icon: "/r-icon.png",
shortcut: "/r-icon.png",
apple: "/r-icon.png",
},
viewport: {
width: "device-width",
initialScale: 1,
maximumScale: 1,
},
robots: { index: false, follow: false, noarchive: true, noimageindex: true },
};
type LayoutProps = {
params: {
domain: string;
};
children: any;
};
export default async function RootLayout({ children, params }: LayoutProps) {
const config = await get(params.domain);
// console.log(config);
const requireLogin = config ? config["features"]["requireLogin"] : false;
// console.log(requireLogin);
const privacy = config ? config["features"]["privacy"] : true;
const browse = config ? config["features"]["browse"] : true;
return (
<html lang="en">
<body className={poppins.className + " h-screen relative"}>
<FiefWrapper>
<DomainContextProvider config={config}>
<ErrorProvider>
<ErrorMessage />
<div
id="container"
className="items-center h-[100svh] w-[100svw] p-2 md:p-4 grid grid-rows-layout gap-2 md:gap-4"
>
<header className="flex justify-between items-center w-full">
{/* Logo on the left */}
<Link
href="/"
className="flex outline-blue-300 md:outline-none focus-visible:underline underline-offset-2 decoration-[.5px] decoration-gray-500"
>
<Image
src="/reach.png"
width={16}
height={16}
className="h-10 w-auto"
alt="Reflector"
/>
<div className="hidden flex-col ml-2 md:block">
<h1 className="text-[38px] font-bold tracking-wide leading-tight">
Reflector
</h1>
<p className="text-gray-500 text-xs tracking-tighter">
Capture the signal, not the noise
</p>
</div>
</Link>
<div>
{/* Text link on the right */}
<Link
href="/transcripts/new"
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
>
Create
</Link>
{browse ? (
<>
&nbsp;·&nbsp;
<Link
href="/browse"
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
>
Browse
</Link>
</>
) : (
<></>
)}
&nbsp;·&nbsp;
<About buttonText="About" />
{privacy ? (
<>
&nbsp;·&nbsp;
<Privacy buttonText="Privacy" />
</>
) : (
<></>
)}
{requireLogin ? (
<>
&nbsp;·&nbsp;
<UserInfo />
</>
) : (
<></>
)}
</div>
</header>
{children}
</div>
</ErrorProvider>
</DomainContextProvider>
</FiefWrapper>
</body>
</html>
);
}

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import Modal from "../modal"; import Modal from "../modal";
import getApi from "../../lib/getApi"; import getApi from "../../../lib/getApi";
import useTranscript from "../useTranscript"; import useTranscript from "../useTranscript";
import useTopics from "../useTopics"; import useTopics from "../useTopics";
import useWaveform from "../useWaveform"; import useWaveform from "../useWaveform";
@@ -8,13 +8,13 @@ import { TopicList } from "../topicList";
import Recorder from "../recorder"; import Recorder from "../recorder";
import { Topic } from "../webSocketTypes"; import { Topic } from "../webSocketTypes";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import "../../styles/button.css"; import "../../../styles/button.css";
import FinalSummary from "../finalSummary"; import FinalSummary from "../finalSummary";
import ShareLink from "../shareLink"; import ShareLink from "../shareLink";
import QRCode from "react-qr-code"; import QRCode from "react-qr-code";
import TranscriptTitle from "../transcriptTitle"; import TranscriptTitle from "../transcriptTitle";
import { featRequireLogin } from "../../../app/lib/utils";
import { useFiefIsAuthenticated } from "@fief/fief/nextjs/react"; import { useFiefIsAuthenticated } from "@fief/fief/nextjs/react";
import { featureEnabled } from "../../domainContext";
type TranscriptDetails = { type TranscriptDetails = {
params: { params: {
@@ -32,7 +32,7 @@ export default function TranscriptDetails(details: TranscriptDetails) {
const useActiveTopic = useState<Topic | null>(null); const useActiveTopic = useState<Topic | null>(null);
useEffect(() => { useEffect(() => {
if (featRequireLogin() && !isAuthenticated) return; if (featureEnabled("requireLogin") && !isAuthenticated) return;
setTranscriptId(details.params.transcriptId); setTranscriptId(details.params.transcriptId);
}, [api]); }, [api]);

View File

@@ -6,14 +6,14 @@ import useWebRTC from "../../useWebRTC";
import useTranscript from "../../useTranscript"; import useTranscript from "../../useTranscript";
import { useWebSockets } from "../../useWebSockets"; import { useWebSockets } from "../../useWebSockets";
import useAudioDevice from "../../useAudioDevice"; import useAudioDevice from "../../useAudioDevice";
import "../../../styles/button.css"; import "../../../../styles/button.css";
import { Topic } from "../../webSocketTypes"; import { Topic } from "../../webSocketTypes";
import getApi from "../../../lib/getApi"; import getApi from "../../../../lib/getApi";
import LiveTrancription from "../../liveTranscription"; import LiveTrancription from "../../liveTranscription";
import DisconnectedIndicator from "../../disconnectedIndicator"; import DisconnectedIndicator from "../../disconnectedIndicator";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGear } from "@fortawesome/free-solid-svg-icons"; import { faGear } from "@fortawesome/free-solid-svg-icons";
import { lockWakeState, releaseWakeState } from "../../../lib/wakeLock"; import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
type TranscriptDetails = { type TranscriptDetails = {
params: { params: {

View File

@@ -1,8 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { DefaultApi, V1TranscriptsCreateRequest } from "../api/apis/DefaultApi"; import {
import { GetTranscript } from "../api"; DefaultApi,
import { useError } from "../(errors)/errorContext"; V1TranscriptsCreateRequest,
import getApi from "../lib/getApi"; } from "../../api/apis/DefaultApi";
import { GetTranscript } from "../../api";
import { useError } from "../../(errors)/errorContext";
import getApi from "../../lib/getApi";
type CreateTranscript = { type CreateTranscript = {
response: GetTranscript | null; response: GetTranscript | null;

View File

@@ -2,7 +2,7 @@ import { useRef, useState } from "react";
import React from "react"; import React from "react";
import ReactDom from "react-dom"; import ReactDom from "react-dom";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import "../styles/markdown.css"; import "../../styles/markdown.css";
type FinalSummaryProps = { type FinalSummaryProps = {
summary: string; summary: string;

View File

@@ -1,22 +1,22 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import useAudioDevice from "../useAudioDevice"; import useAudioDevice from "../useAudioDevice";
import "react-select-search/style.css"; import "react-select-search/style.css";
import "../../styles/button.css"; import "../../../styles/button.css";
import "../../styles/form.scss"; import "../../../styles/form.scss";
import About from "../../(aboutAndPrivacy)/about"; import About from "../../../(aboutAndPrivacy)/about";
import Privacy from "../../(aboutAndPrivacy)/privacy"; import Privacy from "../../../(aboutAndPrivacy)/privacy";
import { useRouter } from "next/navigation"; 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 { supportedLatinLanguages } from "../../supportedLanguages"; import { supportedLatinLanguages } from "../../../supportedLanguages";
import { featRequireLogin, featPrivacy } from "../../lib/utils";
import { useFiefIsAuthenticated } from "@fief/fief/nextjs/react"; import { useFiefIsAuthenticated } from "@fief/fief/nextjs/react";
import { featureEnabled } from "../../domainContext";
const TranscriptCreate = () => { const TranscriptCreate = () => {
const router = useRouter(); const router = useRouter();
const isAuthenticated = useFiefIsAuthenticated(); const isAuthenticated = useFiefIsAuthenticated();
const requireLogin = featRequireLogin(); const requireLogin = featureEnabled("requireLogin");
const [name, setName] = useState<string>(); const [name, setName] = useState<string>();
const nameChange = (event: React.ChangeEvent<HTMLInputElement>) => { const nameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -74,7 +74,9 @@ const TranscriptCreate = () => {
In order to use Reflector, we kindly request permission to access In order to use Reflector, we kindly request permission to access
your microphone during meetings and events. your microphone during meetings and events.
</p> </p>
{featPrivacy() && <Privacy buttonText="Privacy policy" />} {featureEnabled("privacy") && (
<Privacy buttonText="Privacy policy" />
)}
</div> </div>
</section> </section>
<section className="flex flex-col justify-center items-center w-full h-full"> <section className="flex flex-col justify-center items-center w-full h-full">

View File

@@ -1,20 +1,20 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from "react";
import WaveSurfer from "wavesurfer.js"; import WaveSurfer from "wavesurfer.js";
import RecordPlugin from "../lib/custom-plugins/record"; import RecordPlugin from "../../lib/custom-plugins/record";
import CustomRegionsPlugin from "../lib/custom-plugins/regions"; import CustomRegionsPlugin from "../../lib/custom-plugins/regions";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMicrophone } from "@fortawesome/free-solid-svg-icons"; import { faMicrophone } from "@fortawesome/free-solid-svg-icons";
import { faDownload } from "@fortawesome/free-solid-svg-icons"; import { faDownload } from "@fortawesome/free-solid-svg-icons";
import { formatTime } from "../lib/time"; import { formatTime } from "../../lib/time";
import { Topic } from "./webSocketTypes"; import { Topic } from "./webSocketTypes";
import { AudioWaveform } from "../api"; import { AudioWaveform } from "../../api";
import AudioInputsDropdown from "./audioInputsDropdown"; import AudioInputsDropdown from "./audioInputsDropdown";
import { Option } from "react-dropdown"; import { Option } from "react-dropdown";
import { useError } from "../(errors)/errorContext"; import { useError } from "../../(errors)/errorContext";
import { waveSurferStyles } from "../styles/recorder"; import { waveSurferStyles } from "../../styles/recorder";
type RecorderProps = { type RecorderProps = {
setStream?: React.Dispatch<React.SetStateAction<MediaStream | null>>; setStream?: React.Dispatch<React.SetStateAction<MediaStream | null>>;

View File

@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect, use } from "react"; import React, { useState, useRef, useEffect, use } from "react";
import { featPrivacy } from "../lib/utils"; import { featureEnabled } from "../domainContext";
const ShareLink = () => { const ShareLink = () => {
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
@@ -23,12 +23,14 @@ const ShareLink = () => {
} }
}; };
const privacyEnabled = featureEnabled("privacy");
return ( return (
<div <div
className="p-2 md:p-4 rounded" className="p-2 md:p-4 rounded"
style={{ background: "rgba(96, 165, 250, 0.2)" }} style={{ background: "rgba(96, 165, 250, 0.2)" }}
> >
{featPrivacy() ? ( {privacyEnabled ? (
<p className="text-sm mb-2"> <p className="text-sm mb-2">
You can share this link with others. Anyone with the link will have You can share this link with others. Anyone with the link will have
access to the page, including the full audio recording, for the next 7 access to the page, including the full audio recording, for the next 7

View File

@@ -4,7 +4,7 @@ import {
faChevronRight, faChevronRight,
faChevronDown, faChevronDown,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { formatTime } from "../lib/time"; import { formatTime } from "../../lib/time";
import ScrollToBottom from "./scrollToBottom"; import ScrollToBottom from "./scrollToBottom";
import { Topic } from "./webSocketTypes"; import { Topic } from "./webSocketTypes";

View File

@@ -2,9 +2,9 @@ import { useEffect, useState } from "react";
import { import {
DefaultApi, DefaultApi,
V1TranscriptGetAudioMp3Request, V1TranscriptGetAudioMp3Request,
} from "../api/apis/DefaultApi"; } from "../../api/apis/DefaultApi";
import {} from "../api"; import {} from "../../api";
import { useError } from "../(errors)/errorContext"; import { useError } from "../../(errors)/errorContext";
type Mp3Response = { type Mp3Response = {
url: string | null; url: string | null;

View File

@@ -2,9 +2,8 @@ import { useEffect, useState } from "react";
import { import {
DefaultApi, DefaultApi,
V1TranscriptGetTopicsRequest, V1TranscriptGetTopicsRequest,
} from "../api/apis/DefaultApi"; } from "../../api/apis/DefaultApi";
import { TranscriptTopic } from "../api"; import { useError } from "../../(errors)/errorContext";
import { useError } from "../(errors)/errorContext";
import { Topic } from "./webSocketTypes"; import { Topic } from "./webSocketTypes";
type TranscriptTopics = { type TranscriptTopics = {

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { DefaultApi, V1TranscriptGetRequest } from "../api/apis/DefaultApi"; import { DefaultApi, V1TranscriptGetRequest } from "../../api/apis/DefaultApi";
import { GetTranscript } from "../api"; import { GetTranscript } from "../../api";
import { useError } from "../(errors)/errorContext"; import { useError } from "../../(errors)/errorContext";
type Transcript = { type Transcript = {
response: GetTranscript | null; response: GetTranscript | null;

View File

@@ -2,9 +2,9 @@ import { useEffect, useState } from "react";
import { import {
DefaultApi, DefaultApi,
V1TranscriptGetAudioWaveformRequest, V1TranscriptGetAudioWaveformRequest,
} from "../api/apis/DefaultApi"; } from "../../api/apis/DefaultApi";
import { AudioWaveform } from "../api"; import { AudioWaveform } from "../../api";
import { useError } from "../(errors)/errorContext"; import { useError } from "../../(errors)/errorContext";
type AudioWaveFormResponse = { type AudioWaveFormResponse = {
waveform: AudioWaveform | null; waveform: AudioWaveform | null;

View File

@@ -3,8 +3,8 @@ import Peer from "simple-peer";
import { import {
DefaultApi, DefaultApi,
V1TranscriptRecordWebrtcRequest, V1TranscriptRecordWebrtcRequest,
} from "../api/apis/DefaultApi"; } from "../../api/apis/DefaultApi";
import { useError } from "../(errors)/errorContext"; import { useError } from "../../(errors)/errorContext";
const useWebRTC = ( const useWebRTC = (
stream: MediaStream | null, stream: MediaStream | null,

View File

@@ -1,6 +1,6 @@
import { 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 { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
type UseWebSockets = { type UseWebSockets = {

View File

@@ -1,139 +0,0 @@
import "./styles/globals.scss";
import { Poppins } from "next/font/google";
import { Metadata } from "next";
import FiefWrapper from "./(auth)/fiefWrapper";
import UserInfo from "./(auth)/userInfo";
import { ErrorProvider } from "./(errors)/errorContext";
import ErrorMessage from "./(errors)/errorMessage";
import Image from "next/image";
import Link from "next/link";
import About from "./(aboutAndPrivacy)/about";
import Privacy from "./(aboutAndPrivacy)/privacy";
import { featPrivacy, featRequireLogin } from "./lib/utils";
const poppins = Poppins({ subsets: ["latin"], weight: ["200", "400", "600"] });
export const metadata: Metadata = {
title: {
template: "%s Reflector",
default: "Reflector - AI-Powered Meeting Transcriptions by Monadical",
},
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise",
applicationName: "Reflector",
referrer: "origin-when-cross-origin",
keywords: ["Reflector", "Monadical", "AI", "Meetings", "Transcription"],
authors: [{ name: "Monadical Team", url: "https://monadical.com/team.html" }],
formatDetection: {
email: false,
address: false,
telephone: false,
},
openGraph: {
title: "Reflector",
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise.",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Reflector",
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise.",
images: ["/r-icon.png"],
},
icons: {
icon: "/r-icon.png",
shortcut: "/r-icon.png",
apple: "/r-icon.png",
},
viewport: {
width: "device-width",
initialScale: 1,
maximumScale: 1,
},
robots: { index: false, follow: false, noarchive: true, noimageindex: true },
};
export default function RootLayout({ children }) {
const requireLogin = featRequireLogin();
return (
<html lang="en">
<body className={poppins.className + " h-screen relative"}>
<FiefWrapper>
<ErrorProvider>
<ErrorMessage />
<div
id="container"
className="items-center h-[100svh] w-[100svw] p-2 md:p-4 grid grid-rows-layout gap-2 md:gap-4"
>
<header className="flex justify-between items-center w-full">
{/* Logo on the left */}
<Link
href="/"
className="flex outline-blue-300 md:outline-none focus-visible:underline underline-offset-2 decoration-[.5px] decoration-gray-500"
>
<Image
src="/reach.png"
width={16}
height={16}
className="h-10 w-auto"
alt="Reflector"
/>
<div className="hidden flex-col ml-2 md:block">
<h1 className="text-[38px] font-bold tracking-wide leading-tight">
Reflector
</h1>
<p className="text-gray-500 text-xs tracking-tighter">
Capture the signal, not the noise
</p>
</div>
</Link>
<div>
{/* Text link on the right */}
<Link
href="/transcripts/new"
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
>
Create
</Link>
&nbsp;·&nbsp;
<Link
href="/browse"
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
>
Browse
</Link>
&nbsp;·&nbsp;
<About buttonText="About" />
{featPrivacy() ? (
<>
&nbsp;·&nbsp;
<Privacy buttonText="Privacy" />
</>
) : (
<></>
)}
{requireLogin ? (
<>
&nbsp;·&nbsp;
<UserInfo />
</>
) : (
<></>
)}
</div>
</header>
{children}
</div>
</ErrorProvider>
</FiefWrapper>
</body>
</html>
);
}

View File

@@ -1,6 +1,8 @@
"use client";
import { Fief, FiefUserInfo } from "@fief/fief"; import { Fief, FiefUserInfo } from "@fief/fief";
import { FiefAuth, IUserInfoCache } from "@fief/fief/nextjs"; import { FiefAuth, IUserInfoCache } from "@fief/fief/nextjs";
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
import { useError } from "../(errors)/errorContext";
export const SESSION_COOKIE_NAME = "reflector-auth"; export const SESSION_COOKIE_NAME = "reflector-auth";
@@ -38,13 +40,54 @@ class MemoryUserInfoCache implements IUserInfoCache {
} }
} }
export const fiefAuth = new FiefAuth({ const FIEF_AUTHS = {} as { [domain: string]: FiefAuth };
client: fiefClient,
sessionCookieName: SESSION_COOKIE_NAME, export const getFiefAuth = async (url: URL) => {
redirectURI: if (FIEF_AUTHS[url.hostname]) {
process.env.NEXT_PUBLIC_AUTH_CALLBACK_URL || return FIEF_AUTHS[url.hostname];
"http://localhost:3000/auth-callback", } else {
logoutRedirectURI: const config = url && (await get(url.hostname));
process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000", if (config) {
userInfoCache: new MemoryUserInfoCache(), FIEF_AUTHS[url.hostname] = new FiefAuth({
}); client: fiefClient,
sessionCookieName: SESSION_COOKIE_NAME,
redirectURI: config["auth_callback_url"],
logoutRedirectURI: url.origin,
userInfoCache: new MemoryUserInfoCache(),
});
return FIEF_AUTHS[url.hostname];
} else {
throw new Error("Fief intanciation failed");
}
}
};
export const getFiefAuthMiddleware = async (url) => {
const protectedPaths = [
{
matcher: "/:domain/transcripts",
parameters: {},
},
{
matcher: "/:domain/transcripts/:path*",
parameters: {},
},
{
matcher: "/:domain/browse",
parameters: {},
},
{
matcher: "/transcripts",
parameters: {},
},
{
matcher: "/transcripts/:path*",
parameters: {},
},
{
matcher: "/browse",
parameters: {},
},
];
return (await getFiefAuth(url))?.middleware(protectedPaths);
};

View File

@@ -2,12 +2,16 @@ import { Configuration } from "../api/runtime";
import { DefaultApi } from "../api/apis/DefaultApi"; import { DefaultApi } from "../api/apis/DefaultApi";
import { useFiefAccessTokenInfo } from "@fief/fief/nextjs/react"; import { useFiefAccessTokenInfo } from "@fief/fief/nextjs/react";
import { useContext } from "react";
import { DomainContext } from "../[domain]/domainContext";
export default function getApi(): DefaultApi { export default function getApi(): DefaultApi {
const accessTokenInfo = useFiefAccessTokenInfo(); const accessTokenInfo = useFiefAccessTokenInfo();
const api_url = useContext(DomainContext).apiUrl;
if (!api_url) throw new Error("no API URL");
const apiConfiguration = new Configuration({ const apiConfiguration = new Configuration({
basePath: process.env.NEXT_PUBLIC_API_URL, basePath: api_url,
accessToken: accessTokenInfo accessToken: accessTokenInfo
? "Bearer " + accessTokenInfo.access_token ? "Bearer " + accessTokenInfo.access_token
: undefined, : undefined,

View File

@@ -1,15 +1,3 @@
export function isDevelopment() { export function isDevelopment() {
return process.env.NEXT_PUBLIC_ENV === "development"; return process.env.NEXT_PUBLIC_ENV === "development";
} }
export function featPrivacy() {
return process.env.NEXT_PUBLIC_FEAT_PRIVACY === "1";
}
export function featBrowse() {
return process.env.NEXT_PUBLIC_FEAT_BROWSE === "1";
}
export function featRequireLogin() {
return process.env.NEXT_PUBLIC_FEAT_LOGIN_REQUIRED === "1";
}

View File

@@ -1,22 +1,50 @@
import type { NextRequest } from "next/server"; import { NextResponse, NextRequest } from "next/server";
import { get } from "@vercel/edge-config";
import { fiefAuth } from "./app/lib/fief"; import { FiefAuth, IUserInfoCache } from "@fief/fief/nextjs";
import { getFiefAuth, getFiefAuthMiddleware } from "./app/lib/fief";
let protectedPath: any = [];
if (process.env.NEXT_PUBLIC_FEAT_LOGIN_REQUIRED === "1") {
protectedPath = [
{
matcher: "/transcripts/((?!new).*)",
parameters: {},
},
{
matcher: "/browse",
parameters: {},
},
];
}
const authMiddleware = fiefAuth.middleware(protectedPath);
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
return authMiddleware(request); const domain = request.nextUrl.hostname;
const config = await get(domain);
if (!config) return NextResponse.error();
// Feature-flag protedted paths
if (
!config["features"]["browse"] &&
request.nextUrl.pathname.startsWith("/browse")
) {
return NextResponse.redirect(request.nextUrl.origin);
}
if (config["features"]["requireLogin"]) {
const fiefMiddleware = await getFiefAuthMiddleware(request.nextUrl);
const fiefResponse = fiefMiddleware(request);
if (
request.nextUrl.pathname == "/" ||
request.nextUrl.pathname.startsWith("/transcripts") ||
request.nextUrl.pathname.startsWith("/browse")
) {
// return fiefAuthMiddleware(domain, config['auth_callback_url'])(request, {rewrite: request.nextUrl.origin + "/" + domain + request.nextUrl.pathname})
const response = NextResponse.rewrite(
request.nextUrl.origin + "/" + domain + request.nextUrl.pathname,
);
// response = (await fiefResponse).headers
return response;
}
return fiefResponse;
}
if (
request.nextUrl.pathname == "/" ||
request.nextUrl.pathname.startsWith("/transcripts") ||
request.nextUrl.pathname.startsWith("/browse")
) {
return NextResponse.rewrite(
request.nextUrl.origin + "/" + domain + request.nextUrl.pathname,
);
}
return NextResponse.next();
} }

View File

@@ -16,6 +16,7 @@
"@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@sentry/nextjs": "^7.64.0", "@sentry/nextjs": "^7.64.0",
"@vercel/edge-config": "^0.4.1",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"axios": "^1.4.0", "axios": "^1.4.0",
"fontawesome": "^5.6.3", "fontawesome": "^5.6.3",

View File

@@ -1,3 +1,13 @@
import { fiefAuth } from "../../app/lib/fief"; import { get } from "@vercel/edge-config";
import { getFiefAuth } from "../../app/lib/fief";
import { NextApiRequest, NextApiResponse } from "next";
export default fiefAuth.currentUser(); export default async (req: NextApiRequest, res: NextApiResponse<any>) => {
const fromUrl = req.headers["referer"] && new URL(req.headers["referer"]);
const fief = fromUrl && (await getFiefAuth(fromUrl));
if (fief) {
return fief.currentUser()(req as any, res as any);
} else {
return res.status(200).json({ userinfo: null, access_token_info: null });
}
};

View File

@@ -496,6 +496,18 @@
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
"@vercel/edge-config-fs@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@vercel/edge-config-fs/-/edge-config-fs-0.1.0.tgz#cda8327f611418c1d1cbb28c6bed02d782be928b"
integrity sha512-NRIBwfcS0bUoUbRWlNGetqjvLSwgYH/BqKqDN7vK1g32p7dN96k0712COgaz6VFizAm9b0g6IG6hR6+hc0KCPg==
"@vercel/edge-config@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@vercel/edge-config/-/edge-config-0.4.1.tgz#c247011dcd05ce6a6b4d17139215c8f684d83cb3"
integrity sha512-4Mc3H7lE+x4RrL17nY8CWeEorvJHbkNbQTy9p8H1tO7y11WeKj5xeZSr07wNgfWInKXDUwj5FZ3qd/jIzjPxug==
dependencies:
"@vercel/edge-config-fs" "0.1.0"
agent-base@6: agent-base@6:
version "6.0.2" version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"