mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Display permissions errors on the front
This commit is contained in:
parent
08c7ca99b6
commit
c6dd7203bb
@ -55,7 +55,7 @@ import arrayShuffle from "array-shuffle";
|
|||||||
import { Tooltip } from "react-tooltip";
|
import { Tooltip } from "react-tooltip";
|
||||||
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
|
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
|
||||||
import { PortalProvider } from "@gorhom/portal";
|
import { PortalProvider } from "@gorhom/portal";
|
||||||
import { ConnectionError } from "@kyoo/ui";
|
import { ConnectionError, ErrorContext } from "@kyoo/ui";
|
||||||
|
|
||||||
const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
|
const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
|
||||||
|
|
||||||
@ -136,7 +136,17 @@ const WithLayout = ({ Component, ...props }: { Component: ComponentType }) => {
|
|||||||
const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
||||||
const { Layout, props: layoutProps } =
|
const { Layout, props: layoutProps } =
|
||||||
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
|
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
|
||||||
return <Layout page={<Component {...props} />} randomItems={[]} {...layoutProps} />;
|
return (
|
||||||
|
<Layout
|
||||||
|
page={
|
||||||
|
<ErrorContext key={Component as any}>
|
||||||
|
<Component {...props} />
|
||||||
|
</ErrorContext>
|
||||||
|
}
|
||||||
|
randomItems={[]}
|
||||||
|
{...layoutProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
|
@ -33,8 +33,7 @@ export const withRoute = <Props,>(
|
|||||||
|
|
||||||
if (!hasPermissions)
|
if (!hasPermissions)
|
||||||
return <Unauthorized missing={(Component as QueryPage).requiredPermissions!} />;
|
return <Unauthorized missing={(Component as QueryPage).requiredPermissions!} />;
|
||||||
// @ts-ignore
|
return <Component {...defaultProps} {...router.query} {...(props as any)} />;
|
||||||
return <Component {...defaultProps} {...router.query} {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { ...all } = Component;
|
const { ...all } = Component;
|
||||||
|
@ -28,4 +28,6 @@ export interface KyooErrors {
|
|||||||
* @example `["InvalidFilter: no field 'startYear' on a collection"]`
|
* @example `["InvalidFilter: no field 'startYear' on a collection"]`
|
||||||
*/
|
*/
|
||||||
errors: string[];
|
errors: string[];
|
||||||
|
|
||||||
|
status: number | "aborted" | "parse" | "json"
|
||||||
}
|
}
|
||||||
|
@ -85,10 +85,10 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
if (typeof e === "object" && e && "name" in e && e.name === "AbortError")
|
if (typeof e === "object" && e && "name" in e && e.name === "AbortError")
|
||||||
throw { errors: ["Aborted"] } as KyooErrors;
|
throw { errors: ["Aborted"] } as KyooErrors;
|
||||||
console.log("Fetch error", e, path);
|
console.log("Fetch error", e, path);
|
||||||
throw { errors: ["Could not reach Kyoo's server."] } as KyooErrors;
|
throw { errors: ["Could not reach Kyoo's server."], status: "aborted" } as KyooErrors;
|
||||||
}
|
}
|
||||||
if (resp.status === 404) {
|
if (resp.status === 404) {
|
||||||
throw { errors: ["Resource not found."] } as KyooErrors;
|
throw { errors: ["Resource not found."], status: 404 } as KyooErrors;
|
||||||
}
|
}
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const error = await resp.text();
|
const error = await resp.text();
|
||||||
@ -98,6 +98,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
data = { errors: [error] } as KyooErrors;
|
data = { errors: [error] } as KyooErrors;
|
||||||
}
|
}
|
||||||
|
data.status = resp.status;
|
||||||
console.log(
|
console.log(
|
||||||
`Invalid response (${
|
`Invalid response (${
|
||||||
"method" in context && context.method ? context.method : "GET"
|
"method" in context && context.method ? context.method : "GET"
|
||||||
@ -117,7 +118,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
data = await resp.json();
|
data = await resp.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Invalid json from kyoo", e);
|
console.error("Invalid json from kyoo", e);
|
||||||
throw { errors: ["Invalid response from kyoo"] };
|
throw { errors: ["Invalid response from kyoo"], status: "json" };
|
||||||
}
|
}
|
||||||
if (!type) return data;
|
if (!type) return data;
|
||||||
const parsed = await type.safeParseAsync(data);
|
const parsed = await type.safeParseAsync(data);
|
||||||
@ -127,6 +128,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
errors: [
|
errors: [
|
||||||
"Invalid response from kyoo. Possible version mismatch between the server and the application.",
|
"Invalid response from kyoo. Possible version mismatch between the server and the application.",
|
||||||
],
|
],
|
||||||
|
status: "parse"
|
||||||
} as KyooErrors;
|
} as KyooErrors;
|
||||||
}
|
}
|
||||||
return parsed.data;
|
return parsed.data;
|
||||||
|
@ -78,6 +78,10 @@ export const UserP = ResourceP("user")
|
|||||||
)
|
)
|
||||||
.default({}),
|
.default({}),
|
||||||
})
|
})
|
||||||
.transform((x) => ({ ...x, logo: imageFn(`/user/${x.slug}/logo`) }));
|
.transform((x) => ({
|
||||||
|
...x,
|
||||||
|
logo: imageFn(`/user/${x.slug}/logo`),
|
||||||
|
isVerified: x.permissions.length > 0,
|
||||||
|
}));
|
||||||
|
|
||||||
export type User = z.infer<typeof UserP>;
|
export type User = z.infer<typeof UserP>;
|
||||||
|
@ -179,7 +179,7 @@ export const UserList = () => {
|
|||||||
username={user.username}
|
username={user.username}
|
||||||
avatar={user.logo}
|
avatar={user.logo}
|
||||||
isAdmin={user.permissions?.includes("admin.write")}
|
isAdmin={user.permissions?.includes("admin.write")}
|
||||||
isVerified={user.permissions && user.permissions.length > 0}
|
isVerified={user.isVerified}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</InfiniteFetch>
|
</InfiniteFetch>
|
||||||
|
@ -20,12 +20,25 @@
|
|||||||
|
|
||||||
import { KyooErrors } from "@kyoo/models";
|
import { KyooErrors } from "@kyoo/models";
|
||||||
import { P } from "@kyoo/primitives";
|
import { P } from "@kyoo/primitives";
|
||||||
|
import { ReactElement, createContext, useContext, useLayoutEffect, useState } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
|
import { PermissionError } from "./unauthorized";
|
||||||
|
|
||||||
export const ErrorView = ({ error }: { error: KyooErrors }) => {
|
export const ErrorView = ({
|
||||||
|
error,
|
||||||
|
noBubble = false,
|
||||||
|
}: {
|
||||||
|
error: KyooErrors;
|
||||||
|
noBubble?: boolean;
|
||||||
|
}) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
const setError = useErrorContext();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// if this is a permission error, make it go up the tree to have a whole page login screen.
|
||||||
|
if (!noBubble && (error.status === 401 || error.status == 403)) setError(error);
|
||||||
|
}, [error, noBubble, setError]);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -45,3 +58,15 @@ export const ErrorView = ({ error }: { error: KyooErrors }) => {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ErrorCtx = createContext<(val: KyooErrors | null) => void>(null!);
|
||||||
|
export const ErrorContext = ({ children }: { children: ReactElement }) => {
|
||||||
|
const [error, setError] = useState<KyooErrors | null>(null);
|
||||||
|
if (error && (error.status === 401 || error.status === 403))
|
||||||
|
return <PermissionError error={error} />;
|
||||||
|
if (error) return <ErrorView error={error} noBubble />;
|
||||||
|
return <ErrorCtx.Provider value={setError}>{children}</ErrorCtx.Provider>;
|
||||||
|
};
|
||||||
|
export const useErrorContext = () => {
|
||||||
|
return useContext(ErrorCtx);
|
||||||
|
};
|
||||||
|
@ -18,10 +18,13 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { P } from "@kyoo/primitives";
|
import { KyooErrors, useAccount } from "@kyoo/models";
|
||||||
|
import { Button, Icon, Link, P, ts } from "@kyoo/primitives";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { rem, useYoshiki } from "yoshiki/native";
|
||||||
|
import { ErrorView } from "./error";
|
||||||
|
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
|
||||||
|
|
||||||
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -40,3 +43,31 @@ export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PermissionError = ({ error }: { error: KyooErrors }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { css } = useYoshiki();
|
||||||
|
const account = useAccount();
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
|
||||||
|
>
|
||||||
|
<P>{t("errors.needAccount")}</P>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
href={"/register"}
|
||||||
|
text={t("login.register")}
|
||||||
|
licon={<Icon icon={Register} {...css({ marginRight: ts(2) })} />}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (account.isVerified) return <ErrorView error={error} noBubble />;
|
||||||
|
return (
|
||||||
|
<View {...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}>
|
||||||
|
<P>{t("errors.needVerification")}</P>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -207,7 +207,9 @@
|
|||||||
"try-again": "Try again",
|
"try-again": "Try again",
|
||||||
"re-login": "Re login",
|
"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."
|
"unauthorized": "You are missing the permissions {{permission}} to access this page.",
|
||||||
|
"needVerification": "Your account needs to be verified by your server administrator before you can use it.",
|
||||||
|
"needAccount": "This page can't be accessed in guest mode. You need to create an account or login."
|
||||||
},
|
},
|
||||||
"mediainfo": {
|
"mediainfo": {
|
||||||
"file": "File",
|
"file": "File",
|
||||||
|
@ -207,7 +207,9 @@
|
|||||||
"try-again": "Réessayer",
|
"try-again": "Réessayer",
|
||||||
"re-login": "Se reconnecter",
|
"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."
|
"unauthorized": "Il vous manque les autorisations {{permission}} pour accéder à cette page.",
|
||||||
|
"needVerification": "Votre compte doit être vérifié par l'administrateur de votre serveur avant de pouvoir l'utiliser.",
|
||||||
|
"needAccount": "Cette page n'est pas accessible en mode invité. Vous devez créer un compte ou vous connecter."
|
||||||
},
|
},
|
||||||
"mediainfo": {
|
"mediainfo": {
|
||||||
"file": "Fichier",
|
"file": "Fichier",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user