diff --git a/front/apps/web/src/pages/_app.tsx b/front/apps/web/src/pages/_app.tsx index c91f1c33..c444af2d 100755 --- a/front/apps/web/src/pages/_app.tsx +++ b/front/apps/web/src/pages/_app.tsx @@ -55,7 +55,7 @@ import arrayShuffle from "array-shuffle"; import { Tooltip } from "react-tooltip"; import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal"; 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" }); @@ -136,7 +136,17 @@ const WithLayout = ({ Component, ...props }: { Component: ComponentType }) => { const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page); const { Layout, props: layoutProps } = typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo; - return } randomItems={[]} {...layoutProps} />; + return ( + + + + } + randomItems={[]} + {...layoutProps} + /> + ); }; const App = ({ Component, pageProps }: AppProps) => { diff --git a/front/apps/web/src/router.tsx b/front/apps/web/src/router.tsx index 5579fdfe..63642990 100644 --- a/front/apps/web/src/router.tsx +++ b/front/apps/web/src/router.tsx @@ -33,8 +33,7 @@ export const withRoute = ( if (!hasPermissions) return ; - // @ts-ignore - return ; + return ; }; const { ...all } = Component; diff --git a/front/packages/models/src/kyoo-errors.ts b/front/packages/models/src/kyoo-errors.ts index 329fc7bb..905b7485 100644 --- a/front/packages/models/src/kyoo-errors.ts +++ b/front/packages/models/src/kyoo-errors.ts @@ -28,4 +28,6 @@ export interface KyooErrors { * @example `["InvalidFilter: no field 'startYear' on a collection"]` */ errors: string[]; + + status: number | "aborted" | "parse" | "json" } diff --git a/front/packages/models/src/query.tsx b/front/packages/models/src/query.tsx index 69296f9f..2b7bc38c 100644 --- a/front/packages/models/src/query.tsx +++ b/front/packages/models/src/query.tsx @@ -85,10 +85,10 @@ export const queryFn = async ( if (typeof e === "object" && e && "name" in e && e.name === "AbortError") throw { errors: ["Aborted"] } as KyooErrors; 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) { - throw { errors: ["Resource not found."] } as KyooErrors; + throw { errors: ["Resource not found."], status: 404 } as KyooErrors; } if (!resp.ok) { const error = await resp.text(); @@ -98,6 +98,7 @@ export const queryFn = async ( } catch (e) { data = { errors: [error] } as KyooErrors; } + data.status = resp.status; console.log( `Invalid response (${ "method" in context && context.method ? context.method : "GET" @@ -117,7 +118,7 @@ export const queryFn = async ( data = await resp.json(); } catch (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; const parsed = await type.safeParseAsync(data); @@ -127,6 +128,7 @@ export const queryFn = async ( errors: [ "Invalid response from kyoo. Possible version mismatch between the server and the application.", ], + status: "parse" } as KyooErrors; } return parsed.data; diff --git a/front/packages/models/src/resources/user.ts b/front/packages/models/src/resources/user.ts index ca70aad5..f54442bf 100644 --- a/front/packages/models/src/resources/user.ts +++ b/front/packages/models/src/resources/user.ts @@ -78,6 +78,10 @@ export const UserP = ResourceP("user") ) .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; diff --git a/front/packages/ui/src/admin/users.tsx b/front/packages/ui/src/admin/users.tsx index e2739c92..43e3c7af 100644 --- a/front/packages/ui/src/admin/users.tsx +++ b/front/packages/ui/src/admin/users.tsx @@ -179,7 +179,7 @@ export const UserList = () => { username={user.username} avatar={user.logo} isAdmin={user.permissions?.includes("admin.write")} - isVerified={user.permissions && user.permissions.length > 0} + isVerified={user.isVerified} /> )} diff --git a/front/packages/ui/src/errors/error.tsx b/front/packages/ui/src/errors/error.tsx index 62279c96..2938a681 100644 --- a/front/packages/ui/src/errors/error.tsx +++ b/front/packages/ui/src/errors/error.tsx @@ -20,12 +20,25 @@ import { KyooErrors } from "@kyoo/models"; import { P } from "@kyoo/primitives"; +import { ReactElement, createContext, useContext, useLayoutEffect, useState } from "react"; import { View } from "react-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 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); return ( { ); }; + +const ErrorCtx = createContext<(val: KyooErrors | null) => void>(null!); +export const ErrorContext = ({ children }: { children: ReactElement }) => { + const [error, setError] = useState(null); + if (error && (error.status === 401 || error.status === 403)) + return ; + if (error) return ; + return {children}; +}; +export const useErrorContext = () => { + return useContext(ErrorCtx); +}; diff --git a/front/packages/ui/src/errors/unauthorized.tsx b/front/packages/ui/src/errors/unauthorized.tsx index d6969152..5ccc436c 100644 --- a/front/packages/ui/src/errors/unauthorized.tsx +++ b/front/packages/ui/src/errors/unauthorized.tsx @@ -18,10 +18,13 @@ * along with Kyoo. If not, see . */ -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 { 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[] }) => { const { t } = useTranslation(); @@ -40,3 +43,31 @@ export const Unauthorized = ({ missing }: { missing: string[] }) => { ); }; + +export const PermissionError = ({ error }: { error: KyooErrors }) => { + const { t } = useTranslation(); + const { css } = useYoshiki(); + const account = useAccount(); + + if (!account) { + return ( + +

{t("errors.needAccount")}

+