mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add error consumer for unauthorized
This commit is contained in:
parent
b4144e61d3
commit
6cfeba6c62
10
front/app/(app)/_layout.tsx
Normal file
10
front/app/(app)/_layout.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Slot } from "one";
|
||||||
|
import { ErrorConsumer } from "~/providers/error-provider";
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
return (
|
||||||
|
<ErrorConsumer scope="app">
|
||||||
|
<Slot />
|
||||||
|
</ErrorConsumer>
|
||||||
|
);
|
||||||
|
}
|
2
front/routes.d.ts
vendored
2
front/routes.d.ts
vendored
@ -6,7 +6,7 @@ import type { OneRouter } from 'one'
|
|||||||
declare module 'one' {
|
declare module 'one' {
|
||||||
export namespace OneRouter {
|
export namespace OneRouter {
|
||||||
export interface __routes<T extends string = string> extends Record<string, unknown> {
|
export interface __routes<T extends string = string> extends Record<string, unknown> {
|
||||||
StaticRoutes: `/` | `/_sitemap`
|
StaticRoutes: `/` | `/(app)` | `/_sitemap`
|
||||||
DynamicRoutes: never
|
DynamicRoutes: never
|
||||||
DynamicRouteTemplate: never
|
DynamicRouteTemplate: never
|
||||||
IsTyped: true
|
IsTyped: true
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { type ReactNode, createContext, useEffect, useMemo, useRef } from "react";
|
import { type ReactNode, createContext, useContext, useEffect, useMemo, useRef } from "react";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { type Account, AccountP, type Token, UserP } from "~/models";
|
import { type Account, AccountP, type Token, UserP } from "~/models";
|
||||||
@ -8,14 +8,12 @@ import { removeAccounts, updateAccount } from "./account-store";
|
|||||||
import { useSetError } from "./error-provider";
|
import { useSetError } from "./error-provider";
|
||||||
import { useStoreValue } from "./settings";
|
import { useStoreValue } from "./settings";
|
||||||
|
|
||||||
export const ssrApiUrl = process.env.KYOO_URL ?? "http://back/api";
|
|
||||||
|
|
||||||
export const AccountContext = createContext<{
|
export const AccountContext = createContext<{
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
authToken: Token | null;
|
authToken: Token | null;
|
||||||
selectedAccount: Account | null;
|
selectedAccount: Account | null;
|
||||||
accounts: (Account & { select: () => void; remove: () => void })[];
|
accounts: (Account & { select: () => void; remove: () => void })[];
|
||||||
}>({ apiUrl: ssrApiUrl, authToken: null, selectedAccount: null, accounts: [] });
|
}>({ apiUrl: "/api", authToken: null, selectedAccount: null, accounts: [] });
|
||||||
|
|
||||||
export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [setError, clearError] = useSetError("account");
|
const [setError, clearError] = useSetError("account");
|
||||||
@ -26,7 +24,7 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
return {
|
return {
|
||||||
apiUrl: Platform.OS === "web" ? "/api" : acc?.apiUrl!,
|
apiUrl: Platform.OS === "web" ? "/api" : acc?.apiUrl!,
|
||||||
authToken: acc?.token ?? null,
|
authToken: acc?.token ?? null,
|
||||||
selectedAccount: acc,
|
selectedAccount: acc ?? null,
|
||||||
accounts: accounts.map((account) => ({
|
accounts: accounts.map((account) => ({
|
||||||
...account,
|
...account,
|
||||||
select: () => updateAccount(account.id, { ...account, selected: true }),
|
select: () => updateAccount(account.id, { ...account, selected: true }),
|
||||||
@ -51,6 +49,7 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
authToken: ret.authToken?.access_token,
|
authToken: ret.authToken?.access_token,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log(user);
|
||||||
// Use a ref here because we don't want the effect to trigger when the selected
|
// Use a ref here because we don't want the effect to trigger when the selected
|
||||||
// value has changed, only when the fetch result changed
|
// value has changed, only when the fetch result changed
|
||||||
// If we trigger the effect when the selected value change, we enter an infinite render loop
|
// If we trigger the effect when the selected value change, we enter an infinite render loop
|
||||||
@ -59,7 +58,7 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedRef.current || !userIsSuccess || userIsPlaceholder) return;
|
if (!selectedRef.current || !userIsSuccess || userIsPlaceholder) return;
|
||||||
// The id is different when user is stale data, we need to wait for the use effect to invalidate the query.
|
// The id is different when user is stale data, we need to wait for the use effect to invalidate the query.
|
||||||
if (user.id !== selectedRef.current.id) return;
|
if (user?.id !== selectedRef.current.id) return;
|
||||||
const nUser = { ...selectedRef.current, ...user };
|
const nUser = { ...selectedRef.current, ...user };
|
||||||
updateAccount(nUser.id, nUser);
|
updateAccount(nUser.id, nUser);
|
||||||
}, [user, userIsSuccess, userIsPlaceholder]);
|
}, [user, userIsSuccess, userIsPlaceholder]);
|
||||||
@ -83,3 +82,8 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
|
|
||||||
return <AccountContext.Provider value={ret}>{children}</AccountContext.Provider>;
|
return <AccountContext.Provider value={ret}>{children}</AccountContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAccount = () => {
|
||||||
|
const { selectedAccount } = useContext(AccountContext);
|
||||||
|
return selectedAccount;
|
||||||
|
};
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { type ReactNode, createContext, useContext, useState } from "react";
|
import {
|
||||||
|
type ReactNode,
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import type { KyooError } from "~/models";
|
import type { KyooError } from "~/models";
|
||||||
import { ErrorView, errorHandlers } from "~/ui/errors";
|
import { ErrorView, errorHandlers } from "~/ui/errors";
|
||||||
|
|
||||||
@ -8,41 +16,49 @@ type Error = {
|
|||||||
retry?: () => void;
|
retry?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ErrorContext = createContext<{
|
const ErrorContext = createContext<Error | null>(null);
|
||||||
error: Error | null;
|
const ErrorSetterContext = createContext<{
|
||||||
setError: (error: Error | null) => void;
|
setError: (error: Error | null) => void;
|
||||||
}>({ error: null, setError: () => {} });
|
clearError: (key: string) => void;
|
||||||
|
}>(null!);
|
||||||
|
|
||||||
export const ErrorProvider = ({ children }: { children: ReactNode }) => {
|
export const ErrorProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
return (
|
const currentKey = useRef(error?.key);
|
||||||
<ErrorContext.Provider
|
currentKey.current = error?.key;
|
||||||
value={{
|
const clearError = useCallback((key: string) => {
|
||||||
error,
|
if (key === currentKey.current) setError(null);
|
||||||
|
}, []);
|
||||||
|
const provider = useMemo(
|
||||||
|
() => ({
|
||||||
setError,
|
setError,
|
||||||
}}
|
clearError,
|
||||||
>
|
}),
|
||||||
{children}
|
[clearError],
|
||||||
</ErrorContext.Provider>
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorSetterContext.Provider value={provider}>
|
||||||
|
<ErrorContext.Provider value={error}>{children}</ErrorContext.Provider>
|
||||||
|
</ErrorSetterContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ErrorConsumer = ({ children, scope }: { children: ReactNode; scope: string }) => {
|
export const ErrorConsumer = ({ children, scope }: { children: ReactNode; scope: string }) => {
|
||||||
const { error } = useContext(ErrorContext);
|
const error = useContext(ErrorContext);
|
||||||
if (!error) return children;
|
if (!error) return children;
|
||||||
|
|
||||||
const handler = errorHandlers[error.key] ?? { view: ErrorView };
|
const handler = errorHandlers[error.key] ?? { view: ErrorView };
|
||||||
if (handler.forbid && handler.forbid !== scope) return children;
|
if (handler.forbid && handler.forbid !== scope) return children;
|
||||||
const Handler = handler.view;
|
const Handler = handler.view;
|
||||||
return <Handler {...(error as any)} />;
|
const { key, ...val } = error;
|
||||||
|
return <Handler {...(val as any)} />;
|
||||||
};
|
};
|
||||||
export const useSetError = (key: string) => {
|
export const useSetError = (key: string) => {
|
||||||
const { error, setError } = useContext(ErrorContext);
|
const { setError, clearError } = useContext(ErrorSetterContext);
|
||||||
const set = ({ key: nKey, ...obj }: Error & { key?: Error["key"] }) =>
|
const set = ({ key: nKey, ...obj }: Omit<Error, "key"> & { key?: Error["key"] }) =>
|
||||||
setError({ key: nKey ?? key, ...obj });
|
setError({ key: nKey ?? key, ...obj });
|
||||||
const clearError = () => {
|
const clear = () => clearError(key);
|
||||||
if (error?.key === key) setError(null);
|
return [set, clear] as const;
|
||||||
};
|
|
||||||
return [set, clearError] as const;
|
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ReactElement } from "react";
|
import { useLayoutEffect, type ReactElement } from "react";
|
||||||
import { useSetError } from "~/providers/error-provider";
|
import { useSetError } from "~/providers/error-provider";
|
||||||
import { ErrorView } from "~/ui/errors";
|
import { ErrorView } from "~/ui/errors";
|
||||||
import { type QueryIdentifier, useFetch } from "./query";
|
import { type QueryIdentifier, useFetch } from "./query";
|
||||||
@ -15,15 +15,18 @@ export const Fetch = <Data,>({
|
|||||||
const { data, isPaused, error } = useFetch(query);
|
const { data, isPaused, error } = useFetch(query);
|
||||||
const [setError] = useSetError("fetch");
|
const [setError] = useSetError("fetch");
|
||||||
|
|
||||||
if (error) {
|
useLayoutEffect(() => {
|
||||||
if (error.status === 401 || error.status === 403) {
|
|
||||||
setError({ key: "unauthorized", error });
|
|
||||||
}
|
|
||||||
return <ErrorView error={error} />;
|
|
||||||
}
|
|
||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
setError({ key: "offline" });
|
setError({ key: "offline" });
|
||||||
}
|
}
|
||||||
|
if (error && (error.status === 401 || error.status === 403)) {
|
||||||
|
setError({ key: "unauthorized", error });
|
||||||
|
}
|
||||||
|
}, [error, isPaused]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorView error={error} />;
|
||||||
|
}
|
||||||
if (!data) return <Loader />;
|
if (!data) return <Loader />;
|
||||||
return <Render {...data} />;
|
return <Render {...data} />;
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { QueryClient, dehydrate, useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
import { QueryClient, dehydrate, useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||||
import { setServerData } from "one";
|
import { setServerData } from "one";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
import { Platform } from "react-native";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { type KyooError, type Page, Paged } from "~/models";
|
import { type KyooError, type Page, Paged } from "~/models";
|
||||||
import { AccountContext, ssrApiUrl } from "~/providers/account-provider";
|
import { AccountContext } from "~/providers/account-provider";
|
||||||
|
|
||||||
|
const ssrApiUrl = process.env.KYOO_URL ?? "http://back/api";
|
||||||
|
|
||||||
const cleanSlash = (str: string | null, keepFirst = false) => {
|
const cleanSlash = (str: string | null, keepFirst = false) => {
|
||||||
if (!str) return null;
|
if (!str) return null;
|
||||||
@ -21,6 +24,8 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(context: {
|
|||||||
parser?: Parser;
|
parser?: Parser;
|
||||||
signal: AbortSignal;
|
signal: AbortSignal;
|
||||||
}): Promise<z.infer<Parser>> => {
|
}): Promise<z.infer<Parser>> => {
|
||||||
|
if (Platform.OS === "web" && typeof window === "undefined" && context.url.startsWith("/api"))
|
||||||
|
context.url = `${ssrApiUrl}/${context.url.substring(4)}`;
|
||||||
let resp: Response;
|
let resp: Response;
|
||||||
try {
|
try {
|
||||||
resp = await fetch(context.url, {
|
resp = await fetch(context.url, {
|
||||||
@ -55,11 +60,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(context: {
|
|||||||
data = { message: error } as KyooError;
|
data = { message: error } as KyooError;
|
||||||
}
|
}
|
||||||
data.status = resp.status;
|
data.status = resp.status;
|
||||||
console.trace(
|
console.log(`Invalid response (${context.method ?? "GET"} ${context.url}):`, data, resp.status);
|
||||||
`Invalid response (${context.method ?? "GET"} ${context.url}):`,
|
|
||||||
data,
|
|
||||||
resp.status,
|
|
||||||
);
|
|
||||||
throw data as KyooError;
|
throw data as KyooError;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +200,9 @@ export const prefetch = async (...queries: QueryIdentifier[]) => {
|
|||||||
const authToken = undefined;
|
const authToken = undefined;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
queries.map((query) => {
|
queries
|
||||||
|
.filter((x) => x.enabled !== false)
|
||||||
|
.map((query) => {
|
||||||
const key = toQueryKey({
|
const key = toQueryKey({
|
||||||
apiUrl: ssrApiUrl,
|
apiUrl: ssrApiUrl,
|
||||||
path: query.path,
|
path: query.path,
|
||||||
|
@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { Button, Icon, Link, P, ts } from "~/primitives";
|
import { Button, Icon, Link, P, ts } from "~/primitives";
|
||||||
|
import { useAccount } from "~/providers/account-provider";
|
||||||
|
|
||||||
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user