mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
chore: sentry and nextjs major bumps (#633)
* chore: remove nextjs-config * build fix * sentry update * nextjs update * feature flags doc * 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 * no sentry backward compat * better .env.example * AUTHENTIK_REFRESH_TOKEN_URL not so required * accommodate auth system to requiredLogin feature --------- Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState } from "react";
|
import { useState, use } from "react";
|
||||||
import TopicHeader from "./topicHeader";
|
import TopicHeader from "./topicHeader";
|
||||||
import TopicWords from "./topicWords";
|
import TopicWords from "./topicWords";
|
||||||
import TopicPlayer from "./topicPlayer";
|
import TopicPlayer from "./topicPlayer";
|
||||||
@@ -18,14 +18,16 @@ import { useRouter } from "next/navigation";
|
|||||||
import { Box, Grid } from "@chakra-ui/react";
|
import { Box, Grid } from "@chakra-ui/react";
|
||||||
|
|
||||||
export type TranscriptCorrect = {
|
export type TranscriptCorrect = {
|
||||||
params: {
|
params: Promise<{
|
||||||
transcriptId: string;
|
transcriptId: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TranscriptCorrect({
|
export default function TranscriptCorrect(props: TranscriptCorrect) {
|
||||||
params: { transcriptId },
|
const params = use(props.params);
|
||||||
}: TranscriptCorrect) {
|
|
||||||
|
const { transcriptId } = params;
|
||||||
|
|
||||||
const updateTranscriptMutation = useTranscriptUpdate();
|
const updateTranscriptMutation = useTranscriptUpdate();
|
||||||
const transcript = useTranscriptGet(transcriptId);
|
const transcript = useTranscriptGet(transcriptId);
|
||||||
const stateCurrentTopic = useState<GetTranscriptTopic>();
|
const stateCurrentTopic = useState<GetTranscriptTopic>();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import useWaveform from "../useWaveform";
|
|||||||
import useMp3 from "../useMp3";
|
import useMp3 from "../useMp3";
|
||||||
import { TopicList } from "./_components/TopicList";
|
import { TopicList } from "./_components/TopicList";
|
||||||
import { Topic } from "../webSocketTypes";
|
import { Topic } from "../webSocketTypes";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState, use } from "react";
|
||||||
import FinalSummary from "./finalSummary";
|
import FinalSummary from "./finalSummary";
|
||||||
import TranscriptTitle from "../transcriptTitle";
|
import TranscriptTitle from "../transcriptTitle";
|
||||||
import Player from "../player";
|
import Player from "../player";
|
||||||
@@ -15,13 +15,14 @@ import { useTranscriptGet } from "../../../lib/apiHooks";
|
|||||||
import { TranscriptStatus } from "../../../lib/transcript";
|
import { TranscriptStatus } from "../../../lib/transcript";
|
||||||
|
|
||||||
type TranscriptDetails = {
|
type TranscriptDetails = {
|
||||||
params: {
|
params: Promise<{
|
||||||
transcriptId: string;
|
transcriptId: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TranscriptDetails(details: TranscriptDetails) {
|
export default function TranscriptDetails(details: TranscriptDetails) {
|
||||||
const transcriptId = details.params.transcriptId;
|
const params = use(details.params);
|
||||||
|
const transcriptId = params.transcriptId;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const statusToRedirect = [
|
const statusToRedirect = [
|
||||||
"idle",
|
"idle",
|
||||||
@@ -43,7 +44,7 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (waiting) {
|
if (waiting) {
|
||||||
const newUrl = "/transcripts/" + details.params.transcriptId + "/record";
|
const newUrl = "/transcripts/" + params.transcriptId + "/record";
|
||||||
// Shallow redirection does not work on NextJS 13
|
// Shallow redirection does not work on NextJS 13
|
||||||
// https://github.com/vercel/next.js/discussions/48110
|
// https://github.com/vercel/next.js/discussions/48110
|
||||||
// https://github.com/vercel/next.js/discussions/49540
|
// https://github.com/vercel/next.js/discussions/49540
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, use } from "react";
|
||||||
import Recorder from "../../recorder";
|
import Recorder from "../../recorder";
|
||||||
import { TopicList } from "../_components/TopicList";
|
import { TopicList } from "../_components/TopicList";
|
||||||
import { useWebSockets } from "../../useWebSockets";
|
import { useWebSockets } from "../../useWebSockets";
|
||||||
@@ -14,19 +14,20 @@ import { useTranscriptGet } from "../../../../lib/apiHooks";
|
|||||||
import { TranscriptStatus } from "../../../../lib/transcript";
|
import { TranscriptStatus } from "../../../../lib/transcript";
|
||||||
|
|
||||||
type TranscriptDetails = {
|
type TranscriptDetails = {
|
||||||
params: {
|
params: Promise<{
|
||||||
transcriptId: string;
|
transcriptId: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TranscriptRecord = (details: TranscriptDetails) => {
|
const TranscriptRecord = (details: TranscriptDetails) => {
|
||||||
const transcript = useTranscriptGet(details.params.transcriptId);
|
const params = use(details.params);
|
||||||
|
const transcript = useTranscriptGet(params.transcriptId);
|
||||||
const [transcriptStarted, setTranscriptStarted] = useState(false);
|
const [transcriptStarted, setTranscriptStarted] = useState(false);
|
||||||
const useActiveTopic = useState<Topic | null>(null);
|
const useActiveTopic = useState<Topic | null>(null);
|
||||||
|
|
||||||
const webSockets = useWebSockets(details.params.transcriptId);
|
const webSockets = useWebSockets(params.transcriptId);
|
||||||
|
|
||||||
const mp3 = useMp3(details.params.transcriptId, true);
|
const mp3 = useMp3(params.transcriptId, true);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ const TranscriptRecord = (details: TranscriptDetails) => {
|
|||||||
if (newStatus && (newStatus == "ended" || newStatus == "error")) {
|
if (newStatus && (newStatus == "ended" || newStatus == "error")) {
|
||||||
console.log(newStatus, "redirecting");
|
console.log(newStatus, "redirecting");
|
||||||
|
|
||||||
const newUrl = "/transcripts/" + details.params.transcriptId;
|
const newUrl = "/transcripts/" + params.transcriptId;
|
||||||
router.replace(newUrl);
|
router.replace(newUrl);
|
||||||
}
|
}
|
||||||
}, [webSockets.status?.value, transcript.data?.status]);
|
}, [webSockets.status?.value, transcript.data?.status]);
|
||||||
@@ -75,7 +76,7 @@ const TranscriptRecord = (details: TranscriptDetails) => {
|
|||||||
<WaveformLoading />
|
<WaveformLoading />
|
||||||
) : (
|
) : (
|
||||||
// todo: only start recording animation when you get "recorded" status
|
// todo: only start recording animation when you get "recorded" status
|
||||||
<Recorder transcriptId={details.params.transcriptId} status={status} />
|
<Recorder transcriptId={params.transcriptId} status={status} />
|
||||||
)}
|
)}
|
||||||
<VStack
|
<VStack
|
||||||
align={"left"}
|
align={"left"}
|
||||||
@@ -98,7 +99,7 @@ const TranscriptRecord = (details: TranscriptDetails) => {
|
|||||||
topics={webSockets.topics}
|
topics={webSockets.topics}
|
||||||
useActiveTopic={useActiveTopic}
|
useActiveTopic={useActiveTopic}
|
||||||
autoscroll={true}
|
autoscroll={true}
|
||||||
transcriptId={details.params.transcriptId}
|
transcriptId={params.transcriptId}
|
||||||
status={status}
|
status={status}
|
||||||
currentTranscriptText={webSockets.accumulatedText}
|
currentTranscriptText={webSockets.accumulatedText}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, use } from "react";
|
||||||
import { useWebSockets } from "../../useWebSockets";
|
import { useWebSockets } from "../../useWebSockets";
|
||||||
import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
|
import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -9,18 +9,19 @@ import FileUploadButton from "../../fileUploadButton";
|
|||||||
import { useTranscriptGet } from "../../../../lib/apiHooks";
|
import { useTranscriptGet } from "../../../../lib/apiHooks";
|
||||||
|
|
||||||
type TranscriptUpload = {
|
type TranscriptUpload = {
|
||||||
params: {
|
params: Promise<{
|
||||||
transcriptId: string;
|
transcriptId: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TranscriptUpload = (details: TranscriptUpload) => {
|
const TranscriptUpload = (details: TranscriptUpload) => {
|
||||||
const transcript = useTranscriptGet(details.params.transcriptId);
|
const params = use(details.params);
|
||||||
|
const transcript = useTranscriptGet(params.transcriptId);
|
||||||
const [transcriptStarted, setTranscriptStarted] = useState(false);
|
const [transcriptStarted, setTranscriptStarted] = useState(false);
|
||||||
|
|
||||||
const webSockets = useWebSockets(details.params.transcriptId);
|
const webSockets = useWebSockets(params.transcriptId);
|
||||||
|
|
||||||
const mp3 = useMp3(details.params.transcriptId, true);
|
const mp3 = useMp3(params.transcriptId, true);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ const TranscriptUpload = (details: TranscriptUpload) => {
|
|||||||
if (newStatus && (newStatus == "ended" || newStatus == "error")) {
|
if (newStatus && (newStatus == "ended" || newStatus == "error")) {
|
||||||
console.log(newStatus, "redirecting");
|
console.log(newStatus, "redirecting");
|
||||||
|
|
||||||
const newUrl = "/transcripts/" + details.params.transcriptId;
|
const newUrl = "/transcripts/" + params.transcriptId;
|
||||||
router.replace(newUrl);
|
router.replace(newUrl);
|
||||||
}
|
}
|
||||||
}, [webSockets.status?.value, transcript.data?.status]);
|
}, [webSockets.status?.value, transcript.data?.status]);
|
||||||
@@ -84,7 +85,7 @@ const TranscriptUpload = (details: TranscriptUpload) => {
|
|||||||
Please select the file, supported formats: .mp3, m4a, .wav,
|
Please select the file, supported formats: .mp3, m4a, .wav,
|
||||||
.mp4, .mov or .webm
|
.mp4, .mov or .webm
|
||||||
</Text>
|
</Text>
|
||||||
<FileUploadButton transcriptId={details.params.transcriptId} />
|
<FileUploadButton transcriptId={params.transcriptId} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{status && status == "uploaded" && (
|
{status && status == "uploaded" && (
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
useContext,
|
useContext,
|
||||||
RefObject,
|
RefObject,
|
||||||
|
use,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -30,9 +31,9 @@ import { FaBars } from "react-icons/fa6";
|
|||||||
import { useAuth } from "../lib/AuthProvider";
|
import { useAuth } from "../lib/AuthProvider";
|
||||||
|
|
||||||
export type RoomDetails = {
|
export type RoomDetails = {
|
||||||
params: {
|
params: Promise<{
|
||||||
roomName: string;
|
roomName: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// stages: we focus on the consent, then whereby steals focus, then we focus on the consent again, then return focus to whoever stole it initially
|
// stages: we focus on the consent, then whereby steals focus, then we focus on the consent again, then return focus to whoever stole it initially
|
||||||
@@ -255,9 +256,10 @@ const useWhereby = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Room(details: RoomDetails) {
|
export default function Room(details: RoomDetails) {
|
||||||
|
const params = use(details.params);
|
||||||
const wherebyLoaded = useWhereby();
|
const wherebyLoaded = useWhereby();
|
||||||
const wherebyRef = useRef<HTMLElement>(null);
|
const wherebyRef = useRef<HTMLElement>(null);
|
||||||
const roomName = details.params.roomName;
|
const roomName = params.roomName;
|
||||||
const meeting = useRoomMeeting(roomName);
|
const meeting = useRoomMeeting(roomName);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const status = useAuth().status;
|
const status = useAuth().status;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import { authOptions } from "../../../lib/authBackend";
|
import { authOptions } from "../../../lib/authBackend";
|
||||||
|
|
||||||
const handler = NextAuth(authOptions);
|
const handler = NextAuth(authOptions());
|
||||||
|
|
||||||
export { handler as GET, handler as POST };
|
export { handler as GET, handler as POST };
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import ErrorMessage from "./(errors)/errorMessage";
|
|||||||
import { RecordingConsentProvider } from "./recordingConsentContext";
|
import { RecordingConsentProvider } from "./recordingConsentContext";
|
||||||
import { ErrorBoundary } from "@sentry/nextjs";
|
import { ErrorBoundary } from "@sentry/nextjs";
|
||||||
import { Providers } from "./providers";
|
import { Providers } from "./providers";
|
||||||
|
import { assertExistsAndNonEmptyString } from "./lib/utils";
|
||||||
|
|
||||||
const poppins = Poppins({
|
const poppins = Poppins({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -20,8 +21,13 @@ export const viewport: Viewport = {
|
|||||||
maximumScale: 1,
|
maximumScale: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NEXT_PUBLIC_SITE_URL = assertExistsAndNonEmptyString(
|
||||||
|
process.env.NEXT_PUBLIC_SITE_URL,
|
||||||
|
"NEXT_PUBLIC_SITE_URL required",
|
||||||
|
);
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL!),
|
metadataBase: new URL(NEXT_PUBLIC_SITE_URL),
|
||||||
title: {
|
title: {
|
||||||
template: "%s – Reflector",
|
template: "%s – Reflector",
|
||||||
default: "Reflector - AI-Powered Meeting Transcriptions by Monadical",
|
default: "Reflector - AI-Powered Meeting Transcriptions by Monadical",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Session } from "next-auth";
|
|||||||
import { SessionAutoRefresh } from "./SessionAutoRefresh";
|
import { SessionAutoRefresh } from "./SessionAutoRefresh";
|
||||||
import { REFRESH_ACCESS_TOKEN_ERROR } from "./auth";
|
import { REFRESH_ACCESS_TOKEN_ERROR } from "./auth";
|
||||||
import { assertExists } from "./utils";
|
import { assertExists } from "./utils";
|
||||||
|
import { featureEnabled } from "./features";
|
||||||
|
|
||||||
type AuthContextType = (
|
type AuthContextType = (
|
||||||
| { status: "loading" }
|
| { status: "loading" }
|
||||||
@@ -27,33 +28,50 @@ type AuthContextType = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
const isAuthEnabled = featureEnabled("requireLogin");
|
||||||
|
|
||||||
|
const noopAuthContext: AuthContextType = {
|
||||||
|
status: "unauthenticated",
|
||||||
|
update: async () => {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
signIn: async () => {
|
||||||
|
throw new Error("signIn not supposed to be called");
|
||||||
|
},
|
||||||
|
signOut: async () => {
|
||||||
|
throw new Error("signOut not supposed to be called");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { data: session, status, update } = useNextAuthSession();
|
const { data: session, status, update } = useNextAuthSession();
|
||||||
const customSession = session ? assertCustomSession(session) : null;
|
|
||||||
|
|
||||||
const contextValue: AuthContextType = {
|
const contextValue: AuthContextType = isAuthEnabled
|
||||||
|
? {
|
||||||
...(() => {
|
...(() => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "loading": {
|
case "loading": {
|
||||||
const sessionIsHere = !!customSession;
|
const sessionIsHere = !!session;
|
||||||
switch (sessionIsHere) {
|
// actually exists sometimes; nextAuth types are something else
|
||||||
|
switch (sessionIsHere as boolean) {
|
||||||
case false: {
|
case false: {
|
||||||
return { status };
|
return { status };
|
||||||
}
|
}
|
||||||
case true: {
|
case true: {
|
||||||
return {
|
return {
|
||||||
status: "refreshing" as const,
|
status: "refreshing" as const,
|
||||||
user: assertExists(customSession).user,
|
user: assertCustomSession(
|
||||||
|
assertExists(session as unknown as Session),
|
||||||
|
).user,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
const _: never = sessionIsHere;
|
|
||||||
throw new Error("unreachable");
|
throw new Error("unreachable");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "authenticated": {
|
case "authenticated": {
|
||||||
|
const customSession = assertCustomSession(session);
|
||||||
if (customSession?.error === REFRESH_ACCESS_TOKEN_ERROR) {
|
if (customSession?.error === REFRESH_ACCESS_TOKEN_ERROR) {
|
||||||
// token had expired but next auth still returns "authenticated" so show user unauthenticated state
|
// token had expired but next auth still returns "authenticated" so show user unauthenticated state
|
||||||
return {
|
return {
|
||||||
@@ -85,7 +103,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
update,
|
update,
|
||||||
signIn,
|
signIn,
|
||||||
signOut,
|
signOut,
|
||||||
};
|
}
|
||||||
|
: noopAuthContext;
|
||||||
|
|
||||||
// not useEffect, we need it ASAP
|
// not useEffect, we need it ASAP
|
||||||
// apparently, still no guarantee this code runs before mutations are fired
|
// apparently, still no guarantee this code runs before mutations are fired
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import { assertExistsAndNonEmptyString } from "./utils";
|
|||||||
import { isBuildPhase } from "./next";
|
import { isBuildPhase } from "./next";
|
||||||
|
|
||||||
export const API_URL = !isBuildPhase
|
export const API_URL = !isBuildPhase
|
||||||
? assertExistsAndNonEmptyString(process.env.NEXT_PUBLIC_API_URL)
|
? assertExistsAndNonEmptyString(
|
||||||
|
process.env.NEXT_PUBLIC_API_URL,
|
||||||
|
"NEXT_PUBLIC_API_URL required",
|
||||||
|
)
|
||||||
: "http://localhost";
|
: "http://localhost";
|
||||||
|
|
||||||
// TODO decide strict validation or not
|
// TODO decide strict validation or not
|
||||||
|
|||||||
12
www/app/lib/array.ts
Normal file
12
www/app/lib/array.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export type NonEmptyArray<T> = [T, ...T[]];
|
||||||
|
export const isNonEmptyArray = <T>(arr: T[]): arr is NonEmptyArray<T> =>
|
||||||
|
arr.length > 0;
|
||||||
|
export const assertNonEmptyArray = <T>(
|
||||||
|
arr: T[],
|
||||||
|
err?: string,
|
||||||
|
): NonEmptyArray<T> => {
|
||||||
|
if (isNonEmptyArray(arr)) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
throw new Error(err ?? "Expected non-empty array");
|
||||||
|
};
|
||||||
@@ -19,20 +19,41 @@ import {
|
|||||||
} from "./redisTokenCache";
|
} from "./redisTokenCache";
|
||||||
import { tokenCacheRedis, redlock } from "./redisClient";
|
import { tokenCacheRedis, redlock } from "./redisClient";
|
||||||
import { isBuildPhase } from "./next";
|
import { isBuildPhase } from "./next";
|
||||||
|
import { sequenceThrows } from "./errorUtils";
|
||||||
|
import { featureEnabled } from "./features";
|
||||||
|
|
||||||
const TOKEN_CACHE_TTL = REFRESH_ACCESS_TOKEN_BEFORE;
|
const TOKEN_CACHE_TTL = REFRESH_ACCESS_TOKEN_BEFORE;
|
||||||
const CLIENT_ID = !isBuildPhase
|
const getAuthentikClientId = () =>
|
||||||
? assertExistsAndNonEmptyString(process.env.AUTHENTIK_CLIENT_ID)
|
assertExistsAndNonEmptyString(
|
||||||
: "noop";
|
process.env.AUTHENTIK_CLIENT_ID,
|
||||||
const CLIENT_SECRET = !isBuildPhase
|
"AUTHENTIK_CLIENT_ID required",
|
||||||
? assertExistsAndNonEmptyString(process.env.AUTHENTIK_CLIENT_SECRET)
|
);
|
||||||
: "noop";
|
const getAuthentikClientSecret = () =>
|
||||||
|
assertExistsAndNonEmptyString(
|
||||||
|
process.env.AUTHENTIK_CLIENT_SECRET,
|
||||||
|
"AUTHENTIK_CLIENT_SECRET required",
|
||||||
|
);
|
||||||
|
const getAuthentikRefreshTokenUrl = () =>
|
||||||
|
assertExistsAndNonEmptyString(
|
||||||
|
process.env.AUTHENTIK_REFRESH_TOKEN_URL,
|
||||||
|
"AUTHENTIK_REFRESH_TOKEN_URL required",
|
||||||
|
);
|
||||||
|
|
||||||
export const authOptions: AuthOptions = {
|
export const authOptions = (): AuthOptions =>
|
||||||
|
featureEnabled("requireLogin")
|
||||||
|
? {
|
||||||
providers: [
|
providers: [
|
||||||
AuthentikProvider({
|
AuthentikProvider({
|
||||||
clientId: CLIENT_ID,
|
...(() => {
|
||||||
clientSecret: CLIENT_SECRET,
|
const [clientId, clientSecret] = sequenceThrows(
|
||||||
|
getAuthentikClientId,
|
||||||
|
getAuthentikClientSecret,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
};
|
||||||
|
})(),
|
||||||
issuer: process.env.AUTHENTIK_ISSUER,
|
issuer: process.env.AUTHENTIK_ISSUER,
|
||||||
authorization: {
|
authorization: {
|
||||||
params: {
|
params: {
|
||||||
@@ -114,7 +135,10 @@ export const authOptions: AuthOptions = {
|
|||||||
} satisfies CustomSession;
|
} satisfies CustomSession;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
: {
|
||||||
|
providers: [],
|
||||||
|
};
|
||||||
|
|
||||||
async function lockedRefreshAccessToken(
|
async function lockedRefreshAccessToken(
|
||||||
token: JWT,
|
token: JWT,
|
||||||
@@ -174,16 +198,19 @@ async function lockedRefreshAccessToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAccessToken(token: JWT): Promise<JWTWithAccessToken> {
|
async function refreshAccessToken(token: JWT): Promise<JWTWithAccessToken> {
|
||||||
|
const [url, clientId, clientSecret] = sequenceThrows(
|
||||||
|
getAuthentikRefreshTokenUrl,
|
||||||
|
getAuthentikClientId,
|
||||||
|
getAuthentikClientSecret,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const url = `${process.env.AUTHENTIK_REFRESH_TOKEN_URL}`;
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: process.env.AUTHENTIK_CLIENT_ID as string,
|
client_id: clientId,
|
||||||
client_secret: process.env.AUTHENTIK_CLIENT_SECRET as string,
|
client_secret: clientSecret,
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
refresh_token: token.refreshToken as string,
|
refresh_token: token.refreshToken as string,
|
||||||
}).toString(),
|
}).toString(),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
function shouldShowError(error: Error | null | undefined) {
|
import { isNonEmptyArray, NonEmptyArray } from "./array";
|
||||||
|
|
||||||
|
export function shouldShowError(error: Error | null | undefined) {
|
||||||
if (
|
if (
|
||||||
error?.name == "ResponseError" &&
|
error?.name == "ResponseError" &&
|
||||||
(error["response"].status == 404 || error["response"].status == 403)
|
(error["response"].status == 404 || error["response"].status == 403)
|
||||||
@@ -8,4 +10,40 @@ function shouldShowError(error: Error | null | undefined) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { shouldShowError };
|
const defaultMergeErrors = (ex: NonEmptyArray<unknown>): unknown => {
|
||||||
|
try {
|
||||||
|
return new Error(
|
||||||
|
ex
|
||||||
|
.map((e) =>
|
||||||
|
e ? (e.toString ? e.toString() : JSON.stringify(e)) : `${e}`,
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error merging errors:", e);
|
||||||
|
return ex[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReturnTypes<T extends readonly (() => any)[]> = {
|
||||||
|
[K in keyof T]: T[K] extends () => infer R ? R : never;
|
||||||
|
};
|
||||||
|
|
||||||
|
// sequence semantic for "throws"
|
||||||
|
// calls functions passed and collects its thrown values
|
||||||
|
export function sequenceThrows<Fns extends readonly (() => any)[]>(
|
||||||
|
...fs: Fns
|
||||||
|
): ReturnTypes<Fns> {
|
||||||
|
const results: unknown[] = [];
|
||||||
|
const errors: unknown[] = [];
|
||||||
|
|
||||||
|
for (const f of fs) {
|
||||||
|
try {
|
||||||
|
results.push(f());
|
||||||
|
} catch (e) {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errors.length) throw defaultMergeErrors(errors as NonEmptyArray<unknown>);
|
||||||
|
return results as ReturnTypes<Fns>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ export type FeatureName = (typeof FEATURES)[number];
|
|||||||
export type Features = Readonly<Record<FeatureName, boolean>>;
|
export type Features = Readonly<Record<FeatureName, boolean>>;
|
||||||
|
|
||||||
export const DEFAULT_FEATURES: Features = {
|
export const DEFAULT_FEATURES: Features = {
|
||||||
requireLogin: false,
|
requireLogin: true,
|
||||||
privacy: true,
|
privacy: true,
|
||||||
browse: false,
|
browse: true,
|
||||||
sendToZulip: false,
|
sendToZulip: true,
|
||||||
rooms: false,
|
rooms: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function parseBooleanEnv(
|
function parseBooleanEnv(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, use } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
@@ -30,9 +30,9 @@ const FORM_FIELDS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type WebinarDetails = {
|
export type WebinarDetails = {
|
||||||
params: {
|
params: Promise<{
|
||||||
title: string;
|
title: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Webinar = {
|
export type Webinar = {
|
||||||
@@ -63,7 +63,8 @@ const WEBINARS: Webinar[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function WebinarPage(details: WebinarDetails) {
|
export default function WebinarPage(details: WebinarDetails) {
|
||||||
const title = details.params.title;
|
const params = use(details.params);
|
||||||
|
const title = params.title;
|
||||||
const webinar = WEBINARS.find((webinar) => webinar.title === title);
|
const webinar = WEBINARS.find((webinar) => webinar.title === title);
|
||||||
if (!webinar) {
|
if (!webinar) {
|
||||||
return notFound();
|
return notFound();
|
||||||
|
|||||||
@@ -23,3 +23,5 @@ if (SENTRY_DSN) {
|
|||||||
replaysSessionSampleRate: 0.0,
|
replaysSessionSampleRate: 0.0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
||||||
9
www/instrumentation.ts
Normal file
9
www/instrumentation.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export async function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
await import("./sentry.server.config");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === "edge") {
|
||||||
|
await import("./sentry.edge.config");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
experimental: { esmExternals: "loose" },
|
|
||||||
env: {
|
env: {
|
||||||
IS_CI: process.env.IS_CI,
|
IS_CI: process.env.IS_CI,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,19 +17,19 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@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.77.0",
|
"@sentry/nextjs": "^10.11.0",
|
||||||
"@tanstack/react-query": "^5.85.9",
|
"@tanstack/react-query": "^5.85.9",
|
||||||
"@types/ioredis": "^5.0.0",
|
"@types/ioredis": "^5.0.0",
|
||||||
"@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",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.33.0",
|
||||||
"eslint-config-next": "^14.2.31",
|
"eslint-config-next": "^15.5.3",
|
||||||
"fontawesome": "^5.6.3",
|
"fontawesome": "^5.6.3",
|
||||||
"ioredis": "^5.7.0",
|
"ioredis": "^5.7.0",
|
||||||
"jest-worker": "^29.6.2",
|
"jest-worker": "^29.6.2",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "^14.2.30",
|
"next": "^15.5.3",
|
||||||
"next-auth": "^4.24.7",
|
"next-auth": "^4.24.7",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nuqs": "^2.4.3",
|
"nuqs": "^2.4.3",
|
||||||
|
|||||||
2379
www/pnpm-lock.yaml
generated
2379
www/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user