Add unauthorized guard for the admin panel

This commit is contained in:
Zoe Roux 2024-02-14 01:01:24 +01:00
parent 81131edf2d
commit 1fb3088f0e
20 changed files with 226 additions and 36 deletions

View File

@ -0,0 +1,23 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { AdminPage } from "@kyoo/ui";
export default AdminPage;

View File

@ -24,7 +24,8 @@ import { StatusBar, StatusBarProps } from "react-native";
import * as ScreenOrientation from "expo-screen-orientation";
import * as NavigationBar from "expo-navigation-bar";
import arrayShuffle from "array-shuffle";
import { QueryPage } from "@kyoo/models";
import { QueryPage, useHasPermission } from "@kyoo/models";
import { Unauthorized } from "@kyoo/ui";
const FullscreenProvider = () => {
useEffect(() => {
@ -49,6 +50,11 @@ export const withRoute = <Props,>(
const { statusBar, fullscreen, ...routeOptions } = options ?? {};
const WithUseRoute = (props: any) => {
const routeParams = useLocalSearchParams();
const hasPermissions = useHasPermission((Component as QueryPage).requiredPermissions ?? []);
if (!hasPermissions)
return <Unauthorized missing={(Component as QueryPage).requiredPermissions!} />;
return (
<>
{routeOptions && <Stack.Screen {...routeOptions} />}

View File

@ -0,0 +1,24 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { AdminPage } from "@kyoo/ui";
import { withRoute } from "~/router";
export default withRoute(AdminPage);

View File

@ -18,6 +18,8 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryPage, useHasPermission } from "@kyoo/models";
import { Unauthorized } from "@kyoo/ui";
import { useRouter } from "next/router";
import { ComponentType } from "react";
@ -27,7 +29,10 @@ export const withRoute = <Props,>(
) => {
const WithUseRoute = (props: Props) => {
const router = useRouter();
const hasPermissions = useHasPermission((Component as QueryPage).requiredPermissions ?? []);
if (!hasPermissions)
return <Unauthorized missing={(Component as QueryPage).requiredPermissions!} />;
// @ts-ignore
return <Component {...defaultProps} {...router.query} {...props} />;
};

View File

@ -158,9 +158,11 @@ export const useAccounts = () => {
return useContext(AccountContext);
};
export const useHasPermission = (...perms: string[]) => {
export const useHasPermission = (perms?: string[]) => {
const account = useAccount();
if (!perms) return true;
// TODO: Read permission of guest account here.
if (!account) return false;
return perms.every((perm) => account.permissions.includes(perm));

View File

@ -183,6 +183,7 @@ export type QueryPage<Props = {}, Items = unknown> = ComponentType<
getLayout?:
| QueryPage<{ page: ReactElement }>
| { Layout: QueryPage<{ page: ReactElement }>; props: object };
requiredPermissions?: string[]
randomItems?: Items[];
};

View File

@ -0,0 +1,35 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryPage } from "@kyoo/models";
import { P, ts } from "@kyoo/primitives";
import { ScrollView } from "react-native";
import { DefaultLayout } from "../layout";
export const AdminPage: QueryPage = () => {
return (
<ScrollView contentContainerStyle={{ gap: ts(4), paddingBottom: ts(4) }}>
<P>hbgen</P>
</ScrollView>
);
};
AdminPage.getLayout = DefaultLayout;
AdminPage.requiredPermissions = ["admin.read"];

View File

@ -28,7 +28,7 @@ import {
import { Container, H2, ImageBackground, Link, P, focusReset, ts } from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
import { Theme, useYoshiki } from "yoshiki/native";
import { ErrorView } from "../fetch";
import { ErrorView } from "../errors";
export const PartOf = ({
name,

View File

@ -0,0 +1,46 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { KyooErrors } from "@kyoo/models";
import { P } from "@kyoo/primitives";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
export const ErrorView = ({ error }: { error: KyooErrors }) => {
const { css } = useYoshiki();
return (
<View
{...css({
backgroundColor: (theme) => theme.colors.red,
flexGrow: 1,
flexShrink: 1,
justifyContent: "center",
alignItems: "center",
})}
>
{error.errors.map((x, i) => (
<P key={i} {...css({ color: (theme) => theme.colors.white })}>
{x}
</P>
))}
</View>
);
};

View File

@ -0,0 +1,22 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./error";
export * from "./unauthorized";

View File

@ -0,0 +1,42 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { P } from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
export const Unauthorized = ({ missing }: { missing: string[] }) => {
const { t } = useTranslation();
const { css } = useYoshiki();
return (
<View
{...css({
flexGrow: 1,
flexShrink: 1,
justifyContent: "center",
alignItems: "center",
})}
>
<P>{t("errors.unauthorized", { permission: missing.join(", ") })}</P>
</View>
);
};

View File

@ -22,7 +22,8 @@ import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
import { useBreakpointMap, HR } from "@kyoo/primitives";
import { ContentStyle, FlashList } from "@shopify/flash-list";
import { ComponentProps, ComponentType, isValidElement, ReactElement, useRef } from "react";
import { EmptyView, ErrorView, Layout, OfflineView, WithLoading, addHeader } from "./fetch";
import { EmptyView, Layout, OfflineView, WithLoading, addHeader } from "./fetch";
import { ErrorView } from "./errors";
import { FlatList, View, ViewStyle } from "react-native";
const emulateGap = (

View File

@ -31,7 +31,8 @@ import {
useRef,
} from "react";
import { Stylable, nativeStyleToCss, useYoshiki, ysMap } from "yoshiki";
import { EmptyView, ErrorView, Layout, WithLoading, addHeader } from "./fetch";
import { EmptyView, Layout, WithLoading, addHeader } from "./fetch";
import { ErrorView } from "./errors";
import type { ContentStyle } from "@shopify/flash-list";
const InfiniteScroll = <Props,>({

View File

@ -18,12 +18,13 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Page, QueryIdentifier, useFetch, KyooErrors } from "@kyoo/models";
import { Page, QueryIdentifier, useFetch } from "@kyoo/models";
import { Breakpoint, P } from "@kyoo/primitives";
import { ComponentType, ReactElement, isValidElement } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { ErrorView } from "./errors";
export type Layout = {
numColumns: Breakpoint<number>;
@ -84,28 +85,6 @@ export const OfflineView = () => {
);
};
export const ErrorView = ({ error }: { error: KyooErrors }) => {
const { css } = useYoshiki();
return (
<View
{...css({
backgroundColor: (theme) => theme.colors.red,
flexGrow: 1,
flexShrink: 1,
justifyContent: "center",
alignItems: "center",
})}
>
{error.errors.map((x, i) => (
<P key={i} {...css({ color: (theme) => theme.colors.white })}>
{x}
</P>
))}
</View>
);
};
export const EmptyView = ({ message }: { message: string }) => {
const { css } = useYoshiki();

View File

@ -28,3 +28,5 @@ export { SearchPage } from "./search";
export { LoginPage, RegisterPage } from "./login";
export { DownloadPage, DownloadProvider } from "./downloads";
export { SettingsPage } from "./settings";
export { AdminPage } from "./admin";
export * from "./errors";

View File

@ -44,6 +44,7 @@ import Admin from "@material-symbols/svg-400/rounded/admin_panel_settings.svg";
import Settings from "@material-symbols/svg-400/rounded/settings.svg";
import { KyooLongLogo } from "./icon";
import { forwardRef, useEffect, useRef, useState } from "react";
import { AdminPage } from "../admin";
export const NavbarTitle = (props: Stylable & { onLayout?: ViewProps["onLayout"] }) => {
const { t } = useTranslation();
@ -139,7 +140,7 @@ export const NavbarRight = () => {
const { css, theme } = useYoshiki();
const { t } = useTranslation();
const { push } = useRouter();
const isAdmin = useHasPermission("admin.read");
const isAdmin = useHasPermission(AdminPage.requiredPermissions);
return (
<View {...css({ flexDirection: "row", alignItems: "center", flexShrink: 1 })}>

View File

@ -22,12 +22,12 @@ import { useFetch, QueryIdentifier, imageFn, Chapter } from "@kyoo/models";
import { Sprite, P, imageBorderRadius, ts } from "@kyoo/primitives";
import { View, Platform } from "react-native";
import { percent, useYoshiki, px, Theme, useForceRerender } from "yoshiki/native";
import { ErrorView } from "../../fetch";
import { useMemo } from "react";
import { useAtomValue } from "jotai";
import { durationAtom } from "../state";
import { toTimerString } from "./left-buttons";
import { seekProgressAtom } from "./hover";
import { ErrorView } from "../../errors";
type Thumb = {
from: number;

View File

@ -40,8 +40,8 @@ import { fullscreenAtom, Video } from "./state";
import { episodeDisplayNumber } from "../details/episode";
import { useVideoKeyboard } from "./keyboard";
import { MediaSessionManager } from "./media-session";
import { ErrorView } from "../fetch";
import { WatchStatusObserver } from "./watch-status-observer";
import { ErrorView } from "../errors";
type Item = (Movie & { type: "movie" }) | (Episode & { type: "episode" });
@ -66,8 +66,6 @@ const mapData = (
};
};
let firstSlug: string | null = null;
export const Player = ({
slug,
type,

View File

@ -189,11 +189,12 @@
},
"errors": {
"connection": "Could not connect to the kyoo's server",
"connection-tips": "Troublshotting tips:\n - Are you connected to internet?\n - Is your kyoo's server online?\n - Have your account been banned?",
"connection-tips": "Troubleshooting tips:\n - Are you connected to internet?\n - Is your kyoo's server online?\n - Have your account been banned?",
"unknown": "Unknown error",
"try-again": "Try again",
"re-login": "Re login",
"offline": "You are not connected to internet. Try again later."
"offline": "You are not connected to internet. Try again later.",
"unauthorized": "You are missing the permissions {{permission}} to access this page."
},
"mediainfo": {
"file": "File",

View File

@ -193,7 +193,8 @@
"unknown": "Erreur inconnue",
"try-again": "Réessayer",
"re-login": "Se reconnecter",
"offline": "Vous n'êtes pas connecté à Internet. Réessayez plus tard."
"offline": "Vous n'êtes pas connecté à Internet. Réessayez plus tard.",
"unauthorized": "Il vous manque les autorisations {{permission}} pour accéder à cette page."
},
"mediainfo": {
"file": "Fichier",