diff --git a/server/migrations/versions/0fea6d96b096_add_share_mode.py b/server/migrations/versions/0fea6d96b096_add_share_mode.py new file mode 100644 index 00000000..48746c3b --- /dev/null +++ b/server/migrations/versions/0fea6d96b096_add_share_mode.py @@ -0,0 +1,33 @@ +"""add share_mode + +Revision ID: 0fea6d96b096 +Revises: f819277e5169 +Create Date: 2023-11-07 11:12:21.614198 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "0fea6d96b096" +down_revision: Union[str, None] = "f819277e5169" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "transcript", + sa.Column("share_mode", sa.String(), server_default="private", nullable=False), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("transcript", "share_mode") + # ### end Alembic commands ### diff --git a/server/reflector/db/transcripts.py b/server/reflector/db/transcripts.py index 44a6d56b..0fba82ef 100644 --- a/server/reflector/db/transcripts.py +++ b/server/reflector/db/transcripts.py @@ -2,10 +2,11 @@ import json from contextlib import asynccontextmanager from datetime import datetime from pathlib import Path -from typing import Any +from typing import Any, Literal from uuid import uuid4 import sqlalchemy +from fastapi import HTTPException from pydantic import BaseModel, Field from reflector.db import database, metadata from reflector.processors.types import Word as ProcessorWord @@ -36,6 +37,12 @@ transcripts = sqlalchemy.Table( ), # with user attached, optional sqlalchemy.Column("user_id", sqlalchemy.String), + sqlalchemy.Column( + "share_mode", + sqlalchemy.String, + nullable=False, + server_default="private", + ), ) @@ -120,6 +127,7 @@ class Transcript(BaseModel): events: list[TranscriptEvent] = [] source_language: str = "en" target_language: str = "en" + share_mode: Literal["private", "semi-private", "public"] = "private" audio_location: str = "local" def add_event(self, event: str, data: BaseModel) -> TranscriptEvent: @@ -217,6 +225,7 @@ class TranscriptController: order_by: str | None = None, filter_empty: bool | None = False, filter_recording: bool | None = False, + return_query: bool = False, ) -> list[Transcript]: """ Get all transcripts @@ -243,6 +252,9 @@ class TranscriptController: if filter_recording: query = query.filter(transcripts.c.status != "recording") + if return_query: + return query + results = await database.fetch_all(query) return results @@ -258,6 +270,47 @@ class TranscriptController: return None return Transcript(**result) + async def get_by_id_for_http( + self, + transcript_id: str, + user_id: str | None, + ) -> Transcript: + """ + Get a transcript by ID for HTTP request. + + If not found, it will raise a 404 error. + If the user is not allowed to access the transcript, it will raise a 403 error. + + This method checks the share mode of the transcript and the user_id + to determine if the user can access the transcript. + """ + query = transcripts.select().where(transcripts.c.id == transcript_id) + result = await database.fetch_one(query) + if not result: + raise HTTPException(status_code=404, detail="Transcript not found") + + # if the transcript is anonymous, share mode is not checked + transcript = Transcript(**result) + if transcript.user_id is None: + return transcript + + if transcript.share_mode == "private": + # in private mode, only the owner can access the transcript + if transcript.user_id == user_id: + return transcript + + elif transcript.share_mode == "semi-private": + # in semi-private mode, only the owner and the users with the link + # can access the transcript + if user_id is not None: + return transcript + + elif transcript.share_mode == "public": + # in public mode, everyone can access the transcript + return transcript + + raise HTTPException(status_code=403, detail="Transcript access denied") + async def add( self, name: str, diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index 125aa311..44b55629 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import Annotated, Optional +from typing import Annotated, Literal, Optional import httpx import reflector.auth as auth @@ -13,7 +13,8 @@ from fastapi import ( WebSocketDisconnect, status, ) -from fastapi_pagination import Page, paginate +from fastapi_pagination import Page +from fastapi_pagination.ext.databases import paginate from jose import jwt from pydantic import BaseModel, Field from reflector.db.transcripts import ( @@ -49,6 +50,7 @@ def create_access_token(data: dict, expires_delta: timedelta): class GetTranscript(BaseModel): id: str + user_id: str | None name: str status: str locked: bool @@ -57,6 +59,7 @@ class GetTranscript(BaseModel): short_summary: str | None long_summary: str | None created_at: datetime + share_mode: str = Field("private") source_language: str | None target_language: str | None @@ -73,6 +76,7 @@ class UpdateTranscript(BaseModel): title: Optional[str] = Field(None) short_summary: Optional[str] = Field(None) long_summary: Optional[str] = Field(None) + share_mode: Optional[Literal["public", "semi-private", "private"]] = Field(None) class DeletionStatus(BaseModel): @@ -83,12 +87,19 @@ class DeletionStatus(BaseModel): async def transcripts_list( user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], ): + from reflector.db import database + if not user and not settings.PUBLIC_MODE: raise HTTPException(status_code=401, detail="Not authenticated") user_id = user["sub"] if user else None - return paginate( - await transcripts_controller.get_all(user_id=user_id, order_by="-created_at") + return await paginate( + database, + await transcripts_controller.get_all( + user_id=user_id, + order_by="-created_at", + return_query=True, + ), ) @@ -166,10 +177,9 @@ async def transcript_get( user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], ): user_id = user["sub"] if user else None - transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id) - if not transcript: - raise HTTPException(status_code=404, detail="Transcript not found") - return transcript + return await transcripts_controller.get_by_id_for_http( + transcript_id, user_id=user_id + ) @router.patch("/transcripts/{transcript_id}", response_model=GetTranscript) @@ -193,6 +203,8 @@ async def transcript_update( values["short_summary"] = info.short_summary if info.title is not None: values["title"] = info.title + if info.share_mode is not None: + values["share_mode"] = info.share_mode await transcripts_controller.update(transcript, values) return transcript @@ -231,9 +243,27 @@ async def transcript_get_audio_mp3( except jwt.JWTError: raise unauthorized_exception - transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id) - if not transcript: - raise HTTPException(status_code=404, detail="Transcript not found") + transcript = await transcripts_controller.get_by_id_for_http( + transcript_id, user_id=user_id + ) + + if transcript.audio_location == "storage": + # proxy S3 file, to prevent issue with CORS + url = await transcript.get_audio_url() + headers = {} + + copy_headers = ["range", "accept-encoding"] + for header in copy_headers: + if header in request.headers: + headers[header] = request.headers[header] + + async with httpx.AsyncClient() as client: + resp = await client.request(request.method, url, headers=headers) + return Response( + content=resp.content, + status_code=resp.status_code, + headers=resp.headers, + ) if transcript.audio_location == "storage": # proxy S3 file, to prevent issue with CORS @@ -254,7 +284,7 @@ async def transcript_get_audio_mp3( ) if not transcript.audio_mp3_filename.exists(): - raise HTTPException(status_code=404, detail="Audio not found") + raise HTTPException(status_code=500, detail="Audio not found") truncated_id = str(transcript.id).split("-")[0] filename = f"recording_{truncated_id}.mp3" @@ -273,9 +303,9 @@ async def transcript_get_audio_waveform( user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], ) -> AudioWaveform: user_id = user["sub"] if user else None - transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id) - if not transcript: - raise HTTPException(status_code=404, detail="Transcript not found") + transcript = await transcripts_controller.get_by_id_for_http( + transcript_id, user_id=user_id + ) if not transcript.audio_waveform_filename.exists(): raise HTTPException(status_code=404, detail="Audio not found") @@ -292,9 +322,9 @@ async def transcript_get_topics( user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], ): user_id = user["sub"] if user else None - transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id) - if not transcript: - raise HTTPException(status_code=404, detail="Transcript not found") + transcript = await transcripts_controller.get_by_id_for_http( + transcript_id, user_id=user_id + ) # convert to GetTranscriptTopic return [ @@ -363,9 +393,9 @@ async def transcript_record_webrtc( user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], ): user_id = user["sub"] if user else None - transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id) - if not transcript: - raise HTTPException(status_code=404, detail="Transcript not found") + transcript = await transcripts_controller.get_by_id_for_http( + transcript_id, user_id=user_id + ) if transcript.locked: raise HTTPException(status_code=400, detail="Transcript is locked") diff --git a/www/app/(auth)/fiefWrapper.tsx b/www/app/(auth)/fiefWrapper.tsx index 187fef7c..bb38f5ee 100644 --- a/www/app/(auth)/fiefWrapper.tsx +++ b/www/app/(auth)/fiefWrapper.tsx @@ -1,11 +1,18 @@ "use client"; import { FiefAuthProvider } from "@fief/fief/nextjs/react"; +import { createContext } from "react"; -export default function FiefWrapper({ children }) { +export const CookieContext = createContext<{ hasAuthCookie: boolean }>({ + hasAuthCookie: false, +}); + +export default function FiefWrapper({ children, hasAuthCookie }) { return ( - - {children} - + + + {children} + + ); } diff --git a/www/app/[domain]/layout.tsx b/www/app/[domain]/layout.tsx index dbe5ed11..73cc4841 100644 --- a/www/app/[domain]/layout.tsx +++ b/www/app/[domain]/layout.tsx @@ -11,6 +11,9 @@ import About from "../(aboutAndPrivacy)/about"; import Privacy from "../(aboutAndPrivacy)/privacy"; import { DomainContextProvider } from "./domainContext"; import { getConfig } from "../lib/edgeConfig"; +import { ErrorBoundary } from "@sentry/nextjs"; +import { cookies } from "next/dist/client/components/headers"; +import { SESSION_COOKIE_NAME } from "../lib/fief"; const poppins = Poppins({ subsets: ["latin"], weight: ["200", "400", "600"] }); @@ -70,86 +73,89 @@ type LayoutProps = { export default async function RootLayout({ children, params }: LayoutProps) { const config = await getConfig(params.domain); const { requireLogin, privacy, browse } = config.features; + const hasAuthCookie = !!cookies().get(SESSION_COOKIE_NAME); return ( - + - - -
-
- {/* Logo on the left */} - - Reflector -
-

- Reflector -

-

- Capture the signal, not the noise -

-
- -
- {/* Text link on the right */} + "something went really wrong"

}> + + +
+
+ {/* Logo on the left */} - Create + Reflector +
+

+ Reflector +

+

+ Capture the signal, not the noise +

+
- {browse ? ( - <> -  ·  - - Browse - - - ) : ( - <> - )} -  ·  - - {privacy ? ( - <> -  ·  - - - ) : ( - <> - )} - {requireLogin ? ( - <> -  ·  - - - ) : ( - <> - )} -
-
+
+ {/* Text link on the right */} + + Create + + {browse ? ( + <> +  ·  + + Browse + + + ) : ( + <> + )} +  ·  + + {privacy ? ( + <> +  ·  + + + ) : ( + <> + )} + {requireLogin ? ( + <> +  ·  + + + ) : ( + <> + )} +
+ - {children} -
-
+ {children} + + +
diff --git a/www/app/[domain]/transcripts/[transcriptId]/page.tsx b/www/app/[domain]/transcripts/[transcriptId]/page.tsx index b0986e94..54eca9f9 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/page.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/page.tsx @@ -14,6 +14,9 @@ import QRCode from "react-qr-code"; import TranscriptTitle from "../transcriptTitle"; import Player from "../player"; import WaveformLoading from "../waveformLoading"; +import { useRouter } from "next/navigation"; +import { faSpinner } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; type TranscriptDetails = { params: { @@ -21,26 +24,16 @@ type TranscriptDetails = { }; }; -const protectedPath = true; - export default function TranscriptDetails(details: TranscriptDetails) { const transcriptId = details.params.transcriptId; + const router = useRouter(); - const transcript = useTranscript(protectedPath, transcriptId); - const topics = useTopics(protectedPath, transcriptId); - const waveform = useWaveform(protectedPath, transcriptId); + const transcript = useTranscript(transcriptId); + const topics = useTopics(transcriptId); + const waveform = useWaveform(transcriptId); const useActiveTopic = useState(null); const mp3 = useMp3(transcriptId); - if (transcript?.error || topics?.error) { - return ( - - ); - } - useEffect(() => { const statusToRedirect = ["idle", "recording", "processing"]; if (statusToRedirect.includes(transcript.response?.status)) { @@ -48,8 +41,8 @@ export default function TranscriptDetails(details: TranscriptDetails) { // Shallow redirection does not work on NextJS 13 // https://github.com/vercel/next.js/discussions/48110 // https://github.com/vercel/next.js/discussions/49540 - // router.push(newUrl, undefined, { shallow: true }); - history.replaceState({}, "", newUrl); + router.push(newUrl, undefined); + // history.replaceState({}, "", newUrl); } }, [transcript.response?.status]); @@ -60,80 +53,89 @@ export default function TranscriptDetails(details: TranscriptDetails) { .replace(/ +/g, " ") .trim() || ""; + if (transcript.error || topics?.error) { + return ( + + ); + } + + if (transcript?.loading || topics?.loading) { + return ; + } + return ( <> - {transcript?.loading || topics?.loading ? ( - - ) : ( - <> -
- {transcript?.response?.title && ( - + {transcript?.response?.title && ( + + )} + {waveform.waveform && mp3.media ? ( + + ) : waveform.error ? ( +
"error loading this recording"
+ ) : ( + + )} +
+
+ + +
+
+ {transcript.response.longSummary ? ( + - )} - {waveform.waveform && mp3.media ? ( - - ) : waveform.error ? ( -
"error loading this recording"
) : ( - - )} -
-
- - -
-
- {transcript.response.longSummary ? ( - +
+ {transcript.response.status == "processing" ? ( +

Loading Transcript

) : ( -
- {transcript.response.status == "processing" ? ( -

Loading Transcript

- ) : ( -

- There was an error generating the final summary, please - come back later -

- )} -
+

+ There was an error generating the final summary, please come + back later +

)} -
+
+ )} + -
-
- -
-
- -
-
+
+
+
-
- - )} +
+ +
+ +
+ ); } diff --git a/www/app/[domain]/transcripts/[transcriptId]/record/page.tsx b/www/app/[domain]/transcripts/[transcriptId]/record/page.tsx index 2c5b73e0..8615a4b1 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/record/page.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/record/page.tsx @@ -15,7 +15,7 @@ import { faGear } from "@fortawesome/free-solid-svg-icons"; import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock"; import { useRouter } from "next/navigation"; import Player from "../../player"; -import useMp3, { Mp3Response } from "../../useMp3"; +import useMp3 from "../../useMp3"; import WaveformLoading from "../../waveformLoading"; type TranscriptDetails = { @@ -39,8 +39,8 @@ const TranscriptRecord = (details: TranscriptDetails) => { } }, []); - const transcript = useTranscript(true, details.params.transcriptId); - const webRTC = useWebRTC(stream, details.params.transcriptId, true); + const transcript = useTranscript(details.params.transcriptId); + const webRTC = useWebRTC(stream, details.params.transcriptId); const webSockets = useWebSockets(details.params.transcriptId); const { audioDevices, getAudioStream } = useAudioDevice(); diff --git a/www/app/[domain]/transcripts/createTranscript.ts b/www/app/[domain]/transcripts/createTranscript.ts index 0d96b8db..9ad1abe0 100644 --- a/www/app/[domain]/transcripts/createTranscript.ts +++ b/www/app/[domain]/transcripts/createTranscript.ts @@ -19,7 +19,7 @@ const useCreateTranscript = (): CreateTranscript => { const [loading, setLoading] = useState(false); const [error, setErrorState] = useState(null); const { setError } = useError(); - const api = getApi(true); + const api = getApi(); const create = (params: V1TranscriptsCreateRequest["createTranscript"]) => { if (loading || !api) return; diff --git a/www/app/[domain]/transcripts/finalSummary.tsx b/www/app/[domain]/transcripts/finalSummary.tsx index e0d0f1c9..47c757bf 100644 --- a/www/app/[domain]/transcripts/finalSummary.tsx +++ b/www/app/[domain]/transcripts/finalSummary.tsx @@ -5,7 +5,6 @@ import "../../styles/markdown.css"; import getApi from "../../lib/getApi"; type FinalSummaryProps = { - protectedPath: boolean; summary: string; fullTranscript: string; transcriptId: string; @@ -18,7 +17,7 @@ export default function FinalSummary(props: FinalSummaryProps) { const [isEditMode, setIsEditMode] = useState(false); const [preEditSummary, setPreEditSummary] = useState(props.summary); const [editedSummary, setEditedSummary] = useState(props.summary); - const api = getApi(props.protectedPath); + const api = getApi(); const updateSummary = async (newSummary: string, transcriptId: string) => { if (!api) return; diff --git a/www/app/[domain]/transcripts/shareLink.tsx b/www/app/[domain]/transcripts/shareLink.tsx index 49163a5b..dd66d6cb 100644 --- a/www/app/[domain]/transcripts/shareLink.tsx +++ b/www/app/[domain]/transcripts/shareLink.tsx @@ -1,15 +1,39 @@ import React, { useState, useRef, useEffect, use } from "react"; import { featureEnabled } from "../domainContext"; +import getApi from "../../lib/getApi"; +import { useFiefUserinfo } from "@fief/fief/nextjs/react"; +import SelectSearch from "react-select-search"; +import "react-select-search/style.css"; +import "../../styles/button.css"; +import "../../styles/form.scss"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faSpinner } from "@fortawesome/free-solid-svg-icons"; -const ShareLink = () => { +type ShareLinkProps = { + transcriptId: string; + userId: string | null; + shareMode: string; +}; + +const ShareLink = (props: ShareLinkProps) => { const [isCopied, setIsCopied] = useState(false); const inputRef = useRef(null); const [currentUrl, setCurrentUrl] = useState(""); + const requireLogin = featureEnabled("requireLogin"); + const [isOwner, setIsOwner] = useState(false); + const [shareMode, setShareMode] = useState(props.shareMode); + const [shareLoading, setShareLoading] = useState(false); + const userinfo = useFiefUserinfo(); + const api = getApi(); useEffect(() => { setCurrentUrl(window.location.href); }, []); + useEffect(() => { + setIsOwner(!!(requireLogin && userinfo?.sub === props.userId)); + }, [userinfo, props.userId]); + const handleCopyClick = () => { if (inputRef.current) { let text_to_copy = inputRef.current.value; @@ -23,6 +47,18 @@ const ShareLink = () => { } }; + const updateShareMode = async (selectedShareMode: string) => { + if (!api) return; + setShareLoading(true); + const updatedTranscript = await api.v1TranscriptUpdate({ + transcriptId: props.transcriptId, + updateTranscript: { + shareMode: selectedShareMode, + }, + }); + setShareMode(updatedTranscript.shareMode); + setShareLoading(false); + }; const privacyEnabled = featureEnabled("privacy"); return ( @@ -30,17 +66,60 @@ const ShareLink = () => { className="p-2 md:p-4 rounded" style={{ background: "rgba(96, 165, 250, 0.2)" }} > - {privacyEnabled ? ( -

- 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 - days. -

- ) : ( -

- You can share this link with others. Anyone with the link will have - access to the page, including the full audio recording. -

+ {requireLogin && ( +
+ {shareMode === "private" && ( +

This transcript is private and can only be accessed by you.

+ )} + {shareMode === "semi-private" && ( +

+ This transcript is secure. Only authenticated users can access it. +

+ )} + {shareMode === "public" && ( +

This transcript is public. Everyone can access it.

+ )} + + {isOwner && api && ( +
+ + {shareLoading && ( +
+ +
+ )} +
+ )} +
+ )} + {!requireLogin && ( + <> + {privacyEnabled ? ( +

+ Share this link to grant others access to this page. The link + includes the full audio recording and is valid for the next 7 + days. +

+ ) : ( +

+ Share this link to allow others to view this page and listen to + the full audio recording. +

+ )} + )}
{ const [displayedTitle, setDisplayedTitle] = useState(props.title); const [preEditTitle, setPreEditTitle] = useState(props.title); const [isEditing, setIsEditing] = useState(false); - const api = getApi(props.protectedPath); + const api = getApi(); const updateTitle = async (newTitle: string, transcriptId: string) => { if (!api) return; diff --git a/www/app/[domain]/transcripts/useMp3.ts b/www/app/[domain]/transcripts/useMp3.ts index 23249f94..363a4190 100644 --- a/www/app/[domain]/transcripts/useMp3.ts +++ b/www/app/[domain]/transcripts/useMp3.ts @@ -13,29 +13,33 @@ const useMp3 = (id: string, waiting?: boolean): Mp3Response => { const [media, setMedia] = useState(null); const [later, setLater] = useState(waiting); const [loading, setLoading] = useState(false); - const api = getApi(true); + const api = getApi(); const { api_url } = useContext(DomainContext); const accessTokenInfo = useFiefAccessTokenInfo(); - const [serviceWorkerReady, setServiceWorkerReady] = useState(false); + const [serviceWorker, setServiceWorker] = + useState(null); useEffect(() => { if ("serviceWorker" in navigator) { - navigator.serviceWorker.register("/service-worker.js").then(() => { - setServiceWorkerReady(true); + navigator.serviceWorker.register("/service-worker.js").then((worker) => { + setServiceWorker(worker); }); } + return () => { + serviceWorker?.unregister(); + }; }, []); useEffect(() => { if (!navigator.serviceWorker) return; if (!navigator.serviceWorker.controller) return; - if (!serviceWorkerReady) return; + if (!serviceWorker) return; // Send the token to the service worker navigator.serviceWorker.controller.postMessage({ type: "SET_AUTH_TOKEN", token: accessTokenInfo?.access_token, }); - }, [navigator.serviceWorker, serviceWorkerReady, accessTokenInfo]); + }, [navigator.serviceWorker, !serviceWorker, accessTokenInfo]); useEffect(() => { if (!id || !api || later) return; diff --git a/www/app/[domain]/transcripts/useTopics.ts b/www/app/[domain]/transcripts/useTopics.ts index 01053019..de4097b3 100644 --- a/www/app/[domain]/transcripts/useTopics.ts +++ b/www/app/[domain]/transcripts/useTopics.ts @@ -14,12 +14,12 @@ type TranscriptTopics = { error: Error | null; }; -const useTopics = (protectedPath, id: string): TranscriptTopics => { +const useTopics = (id: string): TranscriptTopics => { const [topics, setTopics] = useState(null); const [loading, setLoading] = useState(false); const [error, setErrorState] = useState(null); const { setError } = useError(); - const api = getApi(protectedPath); + const api = getApi(); useEffect(() => { if (!id || !api) return; diff --git a/www/app/[domain]/transcripts/useTranscript.ts b/www/app/[domain]/transcripts/useTranscript.ts index 987e57f3..91700d7a 100644 --- a/www/app/[domain]/transcripts/useTranscript.ts +++ b/www/app/[domain]/transcripts/useTranscript.ts @@ -24,14 +24,13 @@ type SuccessTranscript = { }; const useTranscript = ( - protectedPath: boolean, id: string | null, ): ErrorTranscript | LoadingTranscript | SuccessTranscript => { const [response, setResponse] = useState(null); const [loading, setLoading] = useState(true); const [error, setErrorState] = useState(null); const { setError } = useError(); - const api = getApi(protectedPath); + const api = getApi(); useEffect(() => { if (!id || !api) return; diff --git a/www/app/[domain]/transcripts/useTranscriptList.ts b/www/app/[domain]/transcripts/useTranscriptList.ts index cc8f4701..7b5abb37 100644 --- a/www/app/[domain]/transcripts/useTranscriptList.ts +++ b/www/app/[domain]/transcripts/useTranscriptList.ts @@ -15,7 +15,7 @@ const useTranscriptList = (page: number): TranscriptList => { const [loading, setLoading] = useState(true); const [error, setErrorState] = useState(null); const { setError } = useError(); - const api = getApi(true); + const api = getApi(); useEffect(() => { if (!api) return; diff --git a/www/app/[domain]/transcripts/useWaveform.ts b/www/app/[domain]/transcripts/useWaveform.ts index 4073b711..f80ad78c 100644 --- a/www/app/[domain]/transcripts/useWaveform.ts +++ b/www/app/[domain]/transcripts/useWaveform.ts @@ -1,8 +1,5 @@ import { useEffect, useState } from "react"; -import { - DefaultApi, - V1TranscriptGetAudioWaveformRequest, -} from "../../api/apis/DefaultApi"; +import { V1TranscriptGetAudioWaveformRequest } from "../../api/apis/DefaultApi"; import { AudioWaveform } from "../../api"; import { useError } from "../../(errors)/errorContext"; import getApi from "../../lib/getApi"; @@ -14,12 +11,12 @@ type AudioWaveFormResponse = { error: Error | null; }; -const useWaveform = (protectedPath, id: string): AudioWaveFormResponse => { +const useWaveform = (id: string): AudioWaveFormResponse => { const [waveform, setWaveform] = useState(null); const [loading, setLoading] = useState(true); const [error, setErrorState] = useState(null); const { setError } = useError(); - const api = getApi(protectedPath); + const api = getApi(); useEffect(() => { if (!id || !api) return; diff --git a/www/app/[domain]/transcripts/useWebRTC.ts b/www/app/[domain]/transcripts/useWebRTC.ts index f4421e4d..edd3bef0 100644 --- a/www/app/[domain]/transcripts/useWebRTC.ts +++ b/www/app/[domain]/transcripts/useWebRTC.ts @@ -10,11 +10,10 @@ import getApi from "../../lib/getApi"; const useWebRTC = ( stream: MediaStream | null, transcriptId: string | null, - protectedPath, ): Peer => { const [peer, setPeer] = useState(null); const { setError } = useError(); - const api = getApi(protectedPath); + const api = getApi(); useEffect(() => { if (!stream || !transcriptId) { diff --git a/www/app/[domain]/transcripts/useWebSockets.ts b/www/app/[domain]/transcripts/useWebSockets.ts index f289adbb..1e59781c 100644 --- a/www/app/[domain]/transcripts/useWebSockets.ts +++ b/www/app/[domain]/transcripts/useWebSockets.ts @@ -402,6 +402,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { console.debug("WebSocket connection closed"); switch (event.code) { case 1000: // Normal Closure: + case 1005: // Closure by client FF default: setError( new Error(`WebSocket closed unexpectedly with code: ${event.code}`), diff --git a/www/app/[domain]/transcripts/waveformLoading.tsx b/www/app/[domain]/transcripts/waveformLoading.tsx index 68e0c80f..56540927 100644 --- a/www/app/[domain]/transcripts/waveformLoading.tsx +++ b/www/app/[domain]/transcripts/waveformLoading.tsx @@ -5,7 +5,7 @@ export default () => (
); diff --git a/www/app/api/models/GetTranscript.ts b/www/app/api/models/GetTranscript.ts index 06e9c8ad..3c03c689 100644 --- a/www/app/api/models/GetTranscript.ts +++ b/www/app/api/models/GetTranscript.ts @@ -25,6 +25,12 @@ export interface GetTranscript { * @memberof GetTranscript */ id: any | null; + /** + * + * @type {any} + * @memberof GetTranscript + */ + userId: any | null; /** * * @type {any} @@ -73,6 +79,12 @@ export interface GetTranscript { * @memberof GetTranscript */ createdAt: any | null; + /** + * + * @type {any} + * @memberof GetTranscript + */ + shareMode?: any | null; /** * * @type {any} @@ -93,6 +105,7 @@ export interface GetTranscript { export function instanceOfGetTranscript(value: object): boolean { let isInstance = true; isInstance = isInstance && "id" in value; + isInstance = isInstance && "userId" in value; isInstance = isInstance && "name" in value; isInstance = isInstance && "status" in value; isInstance = isInstance && "locked" in value; @@ -120,6 +133,7 @@ export function GetTranscriptFromJSONTyped( } return { id: json["id"], + userId: json["user_id"], name: json["name"], status: json["status"], locked: json["locked"], @@ -128,6 +142,7 @@ export function GetTranscriptFromJSONTyped( shortSummary: json["short_summary"], longSummary: json["long_summary"], createdAt: json["created_at"], + shareMode: !exists(json, "share_mode") ? undefined : json["share_mode"], sourceLanguage: json["source_language"], targetLanguage: json["target_language"], }; @@ -142,6 +157,7 @@ export function GetTranscriptToJSON(value?: GetTranscript | null): any { } return { id: value.id, + user_id: value.userId, name: value.name, status: value.status, locked: value.locked, @@ -150,6 +166,7 @@ export function GetTranscriptToJSON(value?: GetTranscript | null): any { short_summary: value.shortSummary, long_summary: value.longSummary, created_at: value.createdAt, + share_mode: value.shareMode, source_language: value.sourceLanguage, target_language: value.targetLanguage, }; diff --git a/www/app/api/models/UpdateTranscript.ts b/www/app/api/models/UpdateTranscript.ts index d22df8b0..a710af69 100644 --- a/www/app/api/models/UpdateTranscript.ts +++ b/www/app/api/models/UpdateTranscript.ts @@ -49,6 +49,12 @@ export interface UpdateTranscript { * @memberof UpdateTranscript */ longSummary?: any | null; + /** + * + * @type {any} + * @memberof UpdateTranscript + */ + shareMode?: any | null; } /** @@ -81,6 +87,7 @@ export function UpdateTranscriptFromJSONTyped( longSummary: !exists(json, "long_summary") ? undefined : json["long_summary"], + shareMode: !exists(json, "share_mode") ? undefined : json["share_mode"], }; } @@ -97,5 +104,6 @@ export function UpdateTranscriptToJSON(value?: UpdateTranscript | null): any { title: value.title, short_summary: value.shortSummary, long_summary: value.longSummary, + share_mode: value.shareMode, }; } diff --git a/www/app/lib/edgeConfig.ts b/www/app/lib/edgeConfig.ts index 1140e555..5527121a 100644 --- a/www/app/lib/edgeConfig.ts +++ b/www/app/lib/edgeConfig.ts @@ -3,9 +3,9 @@ import { isDevelopment } from "./utils"; const localConfig = { features: { - requireLogin: false, + requireLogin: true, privacy: true, - browse: false, + browse: true, }, api_url: "http://127.0.0.1:1250", websocket_url: "ws://127.0.0.1:1250", diff --git a/www/app/lib/errorUtils.ts b/www/app/lib/errorUtils.ts index 81a39b5d..e9e5300d 100644 --- a/www/app/lib/errorUtils.ts +++ b/www/app/lib/errorUtils.ts @@ -1,5 +1,8 @@ function shouldShowError(error: Error | null | undefined) { - if (error?.name == "ResponseError" && error["response"].status == 404) + if ( + error?.name == "ResponseError" && + (error["response"].status == 404 || error["response"].status == 403) + ) return false; if (error?.name == "FetchError") return false; return true; diff --git a/www/app/lib/fief.ts b/www/app/lib/fief.ts index 02db67f5..3af5c30f 100644 --- a/www/app/lib/fief.ts +++ b/www/app/lib/fief.ts @@ -66,10 +66,6 @@ export const getFiefAuthMiddleware = async (url) => { matcher: "/transcripts", parameters: {}, }, - { - matcher: "/transcripts/((?!new).*)", - parameters: {}, - }, { matcher: "/browse", parameters: {}, diff --git a/www/app/lib/getApi.ts b/www/app/lib/getApi.ts index 7392cc90..e1ece2a9 100644 --- a/www/app/lib/getApi.ts +++ b/www/app/lib/getApi.ts @@ -4,17 +4,19 @@ import { DefaultApi } from "../api/apis/DefaultApi"; import { useFiefAccessTokenInfo } from "@fief/fief/nextjs/react"; import { useContext, useEffect, useState } from "react"; import { DomainContext, featureEnabled } from "../[domain]/domainContext"; +import { CookieContext } from "../(auth)/fiefWrapper"; -export default function getApi(protectedPath: boolean): DefaultApi | undefined { +export default function getApi(): DefaultApi | undefined { const accessTokenInfo = useFiefAccessTokenInfo(); const api_url = useContext(DomainContext).api_url; const requireLogin = featureEnabled("requireLogin"); const [api, setApi] = useState(); + const { hasAuthCookie } = useContext(CookieContext); if (!api_url) throw new Error("no API URL"); useEffect(() => { - if (protectedPath && requireLogin && !accessTokenInfo) { + if (hasAuthCookie && requireLogin && !accessTokenInfo) { return; } @@ -25,7 +27,7 @@ export default function getApi(protectedPath: boolean): DefaultApi | undefined { : undefined, }); setApi(new DefaultApi(apiConfiguration)); - }, [!accessTokenInfo, protectedPath]); + }, [!accessTokenInfo, hasAuthCookie]); return api; } diff --git a/www/app/styles/form.scss b/www/app/styles/form.scss index 90eb4a83..da81f1db 100644 --- a/www/app/styles/form.scss +++ b/www/app/styles/form.scss @@ -35,3 +35,8 @@ body.is-light-mode .input-container { max-width: 100%; width: auto; } + +body .select-search-container .select-search--top.select-search-select { + top: auto; + bottom: 46px; +}