From f2fdc08b10f36e9a4a3d943344be9f0c256e9196 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Tue, 19 Nov 2024 13:54:14 +0100 Subject: [PATCH] Use series credentials to unlock events and improve creds type-safety My previous changes accidentally removed the feature that events are also unlocked by the credentials associated with their series. For that, a second request is necessary, unfortunately. I also reworked how credentials are loaded from localStorage as that was fishy before: `getCredentials` states it returns `Credentials` which has `user` and `password` fields. But in practice, the object stored in local storage always contained fields `eventUser` and `eventPassword`. And that was actually fine at runtime with all places that read that data. However, it's obviously not ideal to have types that don't match the runtime data, so I fixed that. The fields in local storage are now `user` and `password`. --- frontend/src/routes/Embed.tsx | 8 +++- frontend/src/routes/Video.tsx | 69 ++++++++++++++++++++++++++++------- frontend/src/util/index.ts | 15 +++++++- 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/frontend/src/routes/Embed.tsx b/frontend/src/routes/Embed.tsx index 93f232d2e..730057b37 100644 --- a/frontend/src/routes/Embed.tsx +++ b/frontend/src/routes/Embed.tsx @@ -37,9 +37,11 @@ export const EmbedVideoRoute = makeRoute({ } `; + const creds = getCredentials("event", id); const queryRef = loadQuery(query, { id, - ...getCredentials("event", id), + eventUser: creds?.user, + eventPassword: creds?.password, }); @@ -67,9 +69,11 @@ export const EmbedOpencastVideoRoute = makeRoute({ `; const videoId = decodeURIComponent(matches[1]); + const creds = getCredentials("oc-event", videoId); const queryRef = loadQuery(query, { id: videoId, - ...getCredentials("oc-event", videoId), + eventUser: creds?.user, + eventPassword: creds?.password, }); return matchedEmbedRoute(query, queryRef); diff --git a/frontend/src/routes/Video.tsx b/frontend/src/routes/Video.tsx index cb99303db..bc711256b 100644 --- a/frontend/src/routes/Video.tsx +++ b/frontend/src/routes/Video.tsx @@ -46,6 +46,7 @@ import { playlistId, getCredentials, credentialsStorageKey, + Credentials, } from "../util"; import { BREAKPOINT_SMALL, BREAKPOINT_MEDIUM } from "../GlobalStyle"; import { LinkButton } from "../ui/LinkButton"; @@ -135,11 +136,13 @@ export const VideoRoute = makeRoute({ } `; + const creds = getCredentials("event", id); const queryRef = loadQuery(query, { id, realmPath, listId, - ...getCredentials("event", id), + eventUser: creds?.user, + eventPassword: creds?.password, }); return { @@ -201,11 +204,13 @@ export const OpencastVideoRoute = makeRoute({ } `; + const creds = getCredentials("oc-event", id); const queryRef = loadQuery(query, { id, realmPath, listId, - ...getCredentials("oc-event", id), + eventUser: creds?.user, + eventPassword: creds?.password, }); return { @@ -272,10 +277,12 @@ export const DirectVideoRoute = makeRoute({ } `; const id = eventId(decodeURIComponent(params[1])); + const creds = getCredentials("event", id); const queryRef = loadQuery(query, { id, listId: makeListId(url.searchParams.get("list")), - ...getCredentials("event", id), + eventUser: creds?.user, + eventPassword: creds?.password, }); return matchedDirectRoute(query, queryRef); @@ -312,10 +319,12 @@ export const DirectOpencastVideoRoute = makeRoute({ } `; const id = decodeURIComponent(matches[1]); + const creds = getCredentials("oc-event", id); const queryRef = loadQuery(query, { id, listId: makeListId(url.searchParams.get("list")), - ...getCredentials("oc-event", id), + eventUser: creds?.user, + eventPassword: creds?.password, }); return matchedDirectRoute(query, queryRef); @@ -633,6 +642,7 @@ const ProtectedPlayer: React.FC = ({ event, embedded, refe const user = useUser(); const [authState, setAuthState] = useState("idle"); const [authError, setAuthError] = useState(null); + const [triedSeries, setTriedSeries] = useState(false); const embeddedStyles = { height: "100%", @@ -640,26 +650,29 @@ const ProtectedPlayer: React.FC = ({ event, embedded, refe justifyContent: "center", }; - const onSubmit = (data: FormData) => { + const tryCredentials = (creds: NonNullable, callbacks: { + start?: () => void; + error?: (e: Error) => void; + eventAuthError?: () => void; + incorrectCredentials?: () => void; + }) => { const credentialVars = { - eventUser: data.userid, - eventPassword: data.password, + eventUser: creds.user, + eventPassword: creds.password, }; fetchQuery(environment, authorizedDataQuery, { id: event.id, ...credentialVars, }).subscribe({ - start: () => setAuthState("pending"), + start: callbacks.start, next: ({ node }) => { if (node?.__typename !== "AuthorizedEvent") { - setAuthError(t("no-preview-permission")); - setAuthState("idle"); + callbacks.eventAuthError?.(); return; } if (!node.authorizedData) { - setAuthError(t("invalid-credentials")); - setAuthState("idle"); + callbacks.incorrectCredentials?.(); return; } @@ -685,8 +698,10 @@ const ProtectedPlayer: React.FC = ({ event, embedded, refe // The check will return a result for either ID regardless of its kind, as long as // one of them is stored. const credentials = JSON.stringify({ - eventUser: data.userid, - eventPassword: data.password, + // Explicitly listing fields here to keep storage format + // explicit and avoid accidentally changing it. + user: creds.user, + password: creds.password, }); const storage = isRealUser(user) ? window.localStorage : window.sessionStorage; storage.setItem(credentialsStorageKey("event", event.id), credentials); @@ -698,10 +713,36 @@ const ProtectedPlayer: React.FC = ({ event, embedded, refe storage.setItem(credentialsStorageKey("series", event.series.id), credentials); } }, + error: callbacks.error, + }); + }; + + // We also try the credentials we have associated with the series. + // Unfortunately, we can only do that now and not in the beginning because + // we don't know the series ID from the start. + useEffect(() => { + const seriesCredentials = event.series && getCredentials("series", event.series.id); + if (!triedSeries && seriesCredentials) { + setTriedSeries(true); + tryCredentials(seriesCredentials, {}); + } + }); + + const onSubmit = (data: FormData) => { + tryCredentials({ user: data.userid, password: data.password }, { + start: () => setAuthState("pending"), error: (error: Error) => { setAuthError(error.message); setAuthState("idle"); }, + eventAuthError: () => { + setAuthError(t("no-preview-permission")); + setAuthState("idle"); + }, + incorrectCredentials: () => { + setAuthError(t("invalid-credentials")); + setAuthState("idle"); + }, }); }; diff --git a/frontend/src/util/index.ts b/frontend/src/util/index.ts index 546f03212..060454fb4 100644 --- a/frontend/src/util/index.ts +++ b/frontend/src/util/index.ts @@ -242,7 +242,20 @@ export const getCredentials = (kind: IdKind, id: string): Credentials => { const credentials = window.localStorage.getItem(credentialsStorageKey(kind, id)) ?? window.sessionStorage.getItem(credentialsStorageKey(kind, id)); - return credentials && JSON.parse(credentials); + if (!credentials) { + return null; + } + + const parsed = JSON.parse(credentials); + if ("user" in parsed && typeof parsed.user === "string" + && "password" in parsed && typeof parsed.password === "string") { + return { + user: parsed.user, + password: parsed.password, + }; + } else { + return null; + } }; export const credentialsStorageKey = (kind: IdKind, id: string) =>