Skip to content

Commit

Permalink
Use series credentials to unlock events and improve creds type-safety
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
LukasKalbertodt committed Nov 19, 2024
1 parent e5d3a45 commit f2fdc08
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 17 deletions.
8 changes: 6 additions & 2 deletions frontend/src/routes/Embed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ export const EmbedVideoRoute = makeRoute({
}
`;

const creds = getCredentials("event", id);
const queryRef = loadQuery<EmbedQuery>(query, {
id,
...getCredentials("event", id),
eventUser: creds?.user,
eventPassword: creds?.password,
});


Expand Down Expand Up @@ -67,9 +69,11 @@ export const EmbedOpencastVideoRoute = makeRoute({
`;

const videoId = decodeURIComponent(matches[1]);
const creds = getCredentials("oc-event", videoId);
const queryRef = loadQuery<EmbedDirectOpencastQuery>(query, {
id: videoId,
...getCredentials("oc-event", videoId),
eventUser: creds?.user,
eventPassword: creds?.password,
});

return matchedEmbedRoute(query, queryRef);
Expand Down
69 changes: 55 additions & 14 deletions frontend/src/routes/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
playlistId,
getCredentials,
credentialsStorageKey,
Credentials,
} from "../util";
import { BREAKPOINT_SMALL, BREAKPOINT_MEDIUM } from "../GlobalStyle";
import { LinkButton } from "../ui/LinkButton";
Expand Down Expand Up @@ -135,11 +136,13 @@ export const VideoRoute = makeRoute({
}
`;

const creds = getCredentials("event", id);
const queryRef = loadQuery<VideoPageInRealmQuery>(query, {
id,
realmPath,
listId,
...getCredentials("event", id),
eventUser: creds?.user,
eventPassword: creds?.password,
});

return {
Expand Down Expand Up @@ -201,11 +204,13 @@ export const OpencastVideoRoute = makeRoute({
}
`;

const creds = getCredentials("oc-event", id);
const queryRef = loadQuery<VideoPageByOcIdInRealmQuery>(query, {
id,
realmPath,
listId,
...getCredentials("oc-event", id),
eventUser: creds?.user,
eventPassword: creds?.password,
});

return {
Expand Down Expand Up @@ -272,10 +277,12 @@ export const DirectVideoRoute = makeRoute({
}
`;
const id = eventId(decodeURIComponent(params[1]));
const creds = getCredentials("event", id);
const queryRef = loadQuery<VideoPageDirectLinkQuery>(query, {
id,
listId: makeListId(url.searchParams.get("list")),
...getCredentials("event", id),
eventUser: creds?.user,
eventPassword: creds?.password,
});

return matchedDirectRoute(query, queryRef);
Expand Down Expand Up @@ -312,10 +319,12 @@ export const DirectOpencastVideoRoute = makeRoute({
}
`;
const id = decodeURIComponent(matches[1]);
const creds = getCredentials("oc-event", id);
const queryRef = loadQuery<VideoPageDirectOpencastLinkQuery>(query, {
id,
listId: makeListId(url.searchParams.get("list")),
...getCredentials("oc-event", id),
eventUser: creds?.user,
eventPassword: creds?.password,
});

return matchedDirectRoute(query, queryRef);
Expand Down Expand Up @@ -633,33 +642,37 @@ const ProtectedPlayer: React.FC<ProtectedPlayerProps> = ({ event, embedded, refe
const user = useUser();
const [authState, setAuthState] = useState<AuthenticationFormState>("idle");
const [authError, setAuthError] = useState<string | null>(null);
const [triedSeries, setTriedSeries] = useState(false);

const embeddedStyles = {
height: "100%",
alignItems: "center",
justifyContent: "center",
};

const onSubmit = (data: FormData) => {
const tryCredentials = (creds: NonNullable<Credentials>, 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<VideoAuthorizedDataQuery>(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;
}

Expand All @@ -685,8 +698,10 @@ const ProtectedPlayer: React.FC<ProtectedPlayerProps> = ({ 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);
Expand All @@ -698,10 +713,36 @@ const ProtectedPlayer: React.FC<ProtectedPlayerProps> = ({ 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");
},
});
};

Expand Down
15 changes: 14 additions & 1 deletion frontend/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down

0 comments on commit f2fdc08

Please sign in to comment.