From 226b92c3474e05787643a77f2efde7abe132a346 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Tue, 7 Nov 2023 12:39:48 +0100 Subject: [PATCH 1/6] www/server: introduce share mode --- .../versions/0fea6d96b096_add_share_mode.py | 30 +++++++ server/reflector/db/transcripts.py | 55 +++++++++++- server/reflector/views/transcripts.py | 56 ++++++++----- .../transcripts/[transcriptId]/page.tsx | 7 +- www/app/[domain]/transcripts/shareLink.tsx | 84 ++++++++++++++++--- www/app/api/models/GetTranscript.ts | 17 ++++ www/app/api/models/UpdateTranscript.ts | 8 ++ www/app/styles/form.scss | 5 ++ 8 files changed, 228 insertions(+), 34 deletions(-) create mode 100644 server/migrations/versions/0fea6d96b096_add_share_mode.py 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..52a72d48 --- /dev/null +++ b/server/migrations/versions/0fea6d96b096_add_share_mode.py @@ -0,0 +1,30 @@ +"""add share_mode + +Revision ID: 0fea6d96b096 +Revises: 38a927dcb099 +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] = '38a927dcb099' +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 6ac2e32a..4b91423a 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 @@ -30,6 +31,12 @@ transcripts = sqlalchemy.Table( sqlalchemy.Column("target_language", sqlalchemy.String, nullable=True), # with user attached, optional sqlalchemy.Column("user_id", sqlalchemy.String), + sqlalchemy.Column( + "share_mode", + sqlalchemy.String, + nullable=False, + server_default="private", + ), ) @@ -99,6 +106,7 @@ class Transcript(BaseModel): events: list[TranscriptEvent] = [] source_language: str = "en" target_language: str = "en" + share_mode: Literal["private", "semi-private", "public"] = "private" def add_event(self, event: str, data: BaseModel) -> TranscriptEvent: ev = TranscriptEvent(event=event, data=data.model_dump()) @@ -169,6 +177,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 @@ -195,6 +204,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 @@ -210,6 +222,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 e3668ecb..5f1d7831 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 reflector.auth as auth from fastapi import ( @@ -11,7 +11,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 ( @@ -48,6 +49,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 @@ -56,6 +58,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 @@ -72,6 +75,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): @@ -82,12 +86,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, + ), ) @@ -165,10 +176,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) @@ -192,6 +202,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 @@ -229,12 +241,12 @@ 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 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" @@ -253,12 +265,12 @@ 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_mp3_filename.exists(): - raise HTTPException(status_code=404, detail="Audio not found") + raise HTTPException(status_code=500, detail="Audio not found") await run_in_threadpool(transcript.convert_audio_to_waveform) @@ -274,9 +286,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 [ @@ -345,9 +357,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/[domain]/transcripts/[transcriptId]/page.tsx b/www/app/[domain]/transcripts/[transcriptId]/page.tsx index 9f9348c8..7b57ff2a 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/page.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/page.tsx @@ -99,7 +99,12 @@ export default function TranscriptDetails(details: TranscriptDetails) { />
- +
diff --git a/www/app/[domain]/transcripts/shareLink.tsx b/www/app/[domain]/transcripts/shareLink.tsx index 49163a5b..44c57053 100644 --- a/www/app/[domain]/transcripts/shareLink.tsx +++ b/www/app/[domain]/transcripts/shareLink.tsx @@ -1,15 +1,37 @@ 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"; -const ShareLink = () => { +type ShareLinkProps = { + protectedPath: boolean; + 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 api = getApi(props.protectedPath); + const userinfo = useFiefUserinfo(); 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 +45,16 @@ const ShareLink = () => { } }; + const updateShareMode = async (selectedShareMode: string) => { + if (!api) return; + const updatedTranscript = await api.v1TranscriptUpdate({ + transcriptId: props.transcriptId, + updateTranscript: { + shareMode: selectedShareMode, + }, + }); + setShareMode(updatedTranscript.shareMode); + }; const privacyEnabled = featureEnabled("privacy"); return ( @@ -30,18 +62,50 @@ const ShareLink = () => { className="p-2 md:p-4 rounded" style={{ background: "rgba(96, 165, 250, 0.2)" }} > - {privacyEnabled ? ( + {requireLogin && (

- 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. + {shareMode === "private" && ( +

This transcript is only accessible by you.

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

This transcript is accessible by any authenticated users.

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

This transcript is accessible by anyone.

+ )} + + {isOwner && api && ( +

+ +

+ )}

)} + {!requireLogin && ( + <> + {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. +

+ )} + + )}
Date: Tue, 7 Nov 2023 18:41:51 +0100 Subject: [PATCH 2/6] www: edit from andreas feedback --- www/app/[domain]/transcripts/shareLink.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/www/app/[domain]/transcripts/shareLink.tsx b/www/app/[domain]/transcripts/shareLink.tsx index 44c57053..82ef52c9 100644 --- a/www/app/[domain]/transcripts/shareLink.tsx +++ b/www/app/[domain]/transcripts/shareLink.tsx @@ -65,13 +65,15 @@ const ShareLink = (props: ShareLinkProps) => { {requireLogin && (

{shareMode === "private" && ( -

This transcript is only accessible by you.

+

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

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

This transcript is accessible by any authenticated users.

+

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

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

This transcript is accessible by anyone.

+

This transcript is public. Everyone can access it.

)} {isOwner && api && ( @@ -80,7 +82,7 @@ const ShareLink = (props: ShareLinkProps) => { className="select-search--top select-search" options={[ { name: "Private", value: "private" }, - { name: "Semi-private", value: "semi-private" }, + { name: "Secure", value: "semi-private" }, { name: "Public", value: "public" }, ]} value={shareMode} @@ -94,14 +96,14 @@ const ShareLink = (props: ShareLinkProps) => { <> {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. + 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.

) : (

- You can share this link with others. Anyone with the link will - have access to the page, including the full audio recording. + Share this link to allow others to view this page and listen to + the full audio recording.

)} From f38dad3ad400830f5c35c78990ecce4142ad78ab Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 22 Nov 2023 12:25:21 +0100 Subject: [PATCH 3/6] small fixes and start auth fix --- www/app/[domain]/layout.tsx | 143 ++++++++-------- .../transcripts/[transcriptId]/page.tsx | 162 +++++++++--------- www/app/[domain]/transcripts/useMp3.ts | 14 +- www/app/[domain]/transcripts/useWaveform.ts | 5 +- www/app/[domain]/transcripts/useWebSockets.ts | 1 + www/app/lib/edgeConfig.ts | 4 +- www/app/lib/errorUtils.ts | 5 +- www/app/lib/fief.ts | 2 +- 8 files changed, 172 insertions(+), 164 deletions(-) diff --git a/www/app/[domain]/layout.tsx b/www/app/[domain]/layout.tsx index dbe5ed11..3e881ac3 100644 --- a/www/app/[domain]/layout.tsx +++ b/www/app/[domain]/layout.tsx @@ -11,6 +11,7 @@ import About from "../(aboutAndPrivacy)/about"; import Privacy from "../(aboutAndPrivacy)/privacy"; import { DomainContextProvider } from "./domainContext"; import { getConfig } from "../lib/edgeConfig"; +import { ErrorBoundary } from "@sentry/nextjs"; const poppins = Poppins({ subsets: ["latin"], weight: ["200", "400", "600"] }); @@ -76,80 +77,82 @@ export default async function RootLayout({ children, params }: LayoutProps) { - - -
-
- {/* 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 734f2609..23634b95 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/page.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/page.tsx @@ -14,6 +14,7 @@ import QRCode from "react-qr-code"; import TranscriptTitle from "../transcriptTitle"; import Player from "../player"; import WaveformLoading from "../waveformLoading"; +import { useRouter } from "next/navigation"; type TranscriptDetails = { params: { @@ -21,10 +22,11 @@ type TranscriptDetails = { }; }; -const protectedPath = true; +const protectedPath = false; export default function TranscriptDetails(details: TranscriptDetails) { const transcriptId = details.params.transcriptId; + const router = useRouter(); const transcript = useTranscript(protectedPath, transcriptId); const topics = useTopics(protectedPath, transcriptId); @@ -32,15 +34,6 @@ export default function TranscriptDetails(details: TranscriptDetails) { 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,85 +53,92 @@ 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/useMp3.ts b/www/app/[domain]/transcripts/useMp3.ts index 23249f94..58e0209d 100644 --- a/www/app/[domain]/transcripts/useMp3.ts +++ b/www/app/[domain]/transcripts/useMp3.ts @@ -16,26 +16,30 @@ const useMp3 = (id: string, waiting?: boolean): Mp3Response => { const api = getApi(true); 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/useWaveform.ts b/www/app/[domain]/transcripts/useWaveform.ts index 4073b711..d2bd0fd6 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"; 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/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..176aa847 100644 --- a/www/app/lib/fief.ts +++ b/www/app/lib/fief.ts @@ -67,7 +67,7 @@ export const getFiefAuthMiddleware = async (url) => { parameters: {}, }, { - matcher: "/transcripts/((?!new).*)", + matcher: "/transcripts/((?!new))", parameters: {}, }, { From f14e6f5a7f9e137ff9d43cf87e25c3ecd32688d4 Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 22 Nov 2023 13:20:11 +0100 Subject: [PATCH 4/6] fix auth --- www/app/(auth)/fiefWrapper.tsx | 15 +++++++++++---- www/app/[domain]/layout.tsx | 5 ++++- .../[domain]/transcripts/[transcriptId]/page.tsx | 11 +++-------- .../transcripts/[transcriptId]/record/page.tsx | 6 +++--- www/app/[domain]/transcripts/createTranscript.ts | 2 +- www/app/[domain]/transcripts/finalSummary.tsx | 3 +-- www/app/[domain]/transcripts/shareLink.tsx | 3 +-- www/app/[domain]/transcripts/transcriptTitle.tsx | 3 +-- www/app/[domain]/transcripts/useMp3.ts | 2 +- www/app/[domain]/transcripts/useTopics.ts | 4 ++-- www/app/[domain]/transcripts/useTranscript.ts | 3 +-- www/app/[domain]/transcripts/useTranscriptList.ts | 2 +- www/app/[domain]/transcripts/useWaveform.ts | 4 ++-- www/app/[domain]/transcripts/useWebRTC.ts | 3 +-- www/app/lib/fief.ts | 4 ---- www/app/lib/getApi.ts | 8 +++++--- 16 files changed, 38 insertions(+), 40 deletions(-) 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 3e881ac3..73cc4841 100644 --- a/www/app/[domain]/layout.tsx +++ b/www/app/[domain]/layout.tsx @@ -12,6 +12,8 @@ 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"] }); @@ -71,11 +73,12 @@ 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 ( - + "something went really wrong"

}> diff --git a/www/app/[domain]/transcripts/[transcriptId]/page.tsx b/www/app/[domain]/transcripts/[transcriptId]/page.tsx index 23634b95..472c573b 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/page.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/page.tsx @@ -22,15 +22,13 @@ type TranscriptDetails = { }; }; -const protectedPath = false; - 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); @@ -71,7 +69,6 @@ export default function TranscriptDetails(details: TranscriptDetails) {
{transcript?.response?.title && ( @@ -101,7 +98,6 @@ export default function TranscriptDetails(details: TranscriptDetails) {
{transcript.response.longSummary ? (
{ } }, []); - 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 82ef52c9..e6449bd3 100644 --- a/www/app/[domain]/transcripts/shareLink.tsx +++ b/www/app/[domain]/transcripts/shareLink.tsx @@ -8,7 +8,6 @@ import "../../styles/button.css"; import "../../styles/form.scss"; type ShareLinkProps = { - protectedPath: boolean; transcriptId: string; userId: string | null; shareMode: string; @@ -21,8 +20,8 @@ const ShareLink = (props: ShareLinkProps) => { const requireLogin = featureEnabled("requireLogin"); const [isOwner, setIsOwner] = useState(false); const [shareMode, setShareMode] = useState(props.shareMode); - const api = getApi(props.protectedPath); const userinfo = useFiefUserinfo(); + const api = getApi(); useEffect(() => { setCurrentUrl(window.location.href); diff --git a/www/app/[domain]/transcripts/transcriptTitle.tsx b/www/app/[domain]/transcripts/transcriptTitle.tsx index d2f901fa..afc29e51 100644 --- a/www/app/[domain]/transcripts/transcriptTitle.tsx +++ b/www/app/[domain]/transcripts/transcriptTitle.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import getApi from "../../lib/getApi"; type TranscriptTitle = { - protectedPath: boolean; title: string; transcriptId: string; }; @@ -11,7 +10,7 @@ const TranscriptTitle = (props: TranscriptTitle) => { 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 58e0209d..363a4190 100644 --- a/www/app/[domain]/transcripts/useMp3.ts +++ b/www/app/[domain]/transcripts/useMp3.ts @@ -13,7 +13,7 @@ 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 [serviceWorker, setServiceWorker] = 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 d2bd0fd6..f80ad78c 100644 --- a/www/app/[domain]/transcripts/useWaveform.ts +++ b/www/app/[domain]/transcripts/useWaveform.ts @@ -11,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/lib/fief.ts b/www/app/lib/fief.ts index 176aa847..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; } From 4226428f582821383e7fb1af06cfbadb919c1f7e Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 22 Nov 2023 13:53:12 +0100 Subject: [PATCH 5/6] minor styling changes --- .../transcripts/[transcriptId]/page.tsx | 2 ++ www/app/[domain]/transcripts/shareLink.tsx | 22 +++++++++++++++---- .../[domain]/transcripts/waveformLoading.tsx | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/www/app/[domain]/transcripts/[transcriptId]/page.tsx b/www/app/[domain]/transcripts/[transcriptId]/page.tsx index 472c573b..54eca9f9 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/page.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/page.tsx @@ -15,6 +15,8 @@ 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: { diff --git a/www/app/[domain]/transcripts/shareLink.tsx b/www/app/[domain]/transcripts/shareLink.tsx index e6449bd3..dd66d6cb 100644 --- a/www/app/[domain]/transcripts/shareLink.tsx +++ b/www/app/[domain]/transcripts/shareLink.tsx @@ -6,6 +6,8 @@ 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"; type ShareLinkProps = { transcriptId: string; @@ -20,6 +22,7 @@ const ShareLink = (props: ShareLinkProps) => { 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(); @@ -46,6 +49,7 @@ const ShareLink = (props: ShareLinkProps) => { const updateShareMode = async (selectedShareMode: string) => { if (!api) return; + setShareLoading(true); const updatedTranscript = await api.v1TranscriptUpdate({ transcriptId: props.transcriptId, updateTranscript: { @@ -53,6 +57,7 @@ const ShareLink = (props: ShareLinkProps) => { }, }); setShareMode(updatedTranscript.shareMode); + setShareLoading(false); }; const privacyEnabled = featureEnabled("privacy"); @@ -62,7 +67,7 @@ const ShareLink = (props: ShareLinkProps) => { style={{ background: "rgba(96, 165, 250, 0.2)" }} > {requireLogin && ( -

+

{shareMode === "private" && (

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

)} @@ -76,7 +81,7 @@ const ShareLink = (props: ShareLinkProps) => { )} {isOwner && api && ( -

+

{ ]} value={shareMode} onChange={updateShareMode} + closeOnSelect={true} /> -

+ {shareLoading && ( +
+ +
+ )} +
)} -

+
)} {!requireLogin && ( <> 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 () => (
); From f8407874f77172c3aafa379f5089e80b00533064 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Thu, 23 Nov 2023 12:41:39 +0100 Subject: [PATCH 6/6] server: fixes share_mode script --- .../versions/0fea6d96b096_add_share_mode.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/server/migrations/versions/0fea6d96b096_add_share_mode.py b/server/migrations/versions/0fea6d96b096_add_share_mode.py index 52a72d48..48746c3b 100644 --- a/server/migrations/versions/0fea6d96b096_add_share_mode.py +++ b/server/migrations/versions/0fea6d96b096_add_share_mode.py @@ -1,7 +1,7 @@ """add share_mode Revision ID: 0fea6d96b096 -Revises: 38a927dcb099 +Revises: f819277e5169 Create Date: 2023-11-07 11:12:21.614198 """ @@ -12,19 +12,22 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '0fea6d96b096' -down_revision: Union[str, None] = '38a927dcb099' +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)) + 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') + op.drop_column("transcript", "share_mode") # ### end Alembic commands ###