diff --git a/front/apps/web/src/pages/_app.tsx b/front/apps/web/src/pages/_app.tsx index f5e4e033..a388c3d9 100755 --- a/front/apps/web/src/pages/_app.tsx +++ b/front/apps/web/src/pages/_app.tsx @@ -42,7 +42,7 @@ import { UserP, useUserTheme, } from "@kyoo/models"; -import { Component, ComponentType, useContext, useState } from "react"; +import { ComponentType, useContext, useState } from "react"; import NextApp, { AppContext, type AppProps } from "next/app"; import { Poppins } from "next/font/google"; import { useTheme, useMobileHover, useStyleRegistry, StyleRegistryProvider } from "yoshiki/web"; diff --git a/front/packages/models/src/accounts.tsx b/front/packages/models/src/accounts.tsx index e285f41e..42626d14 100644 --- a/front/packages/models/src/accounts.tsx +++ b/front/packages/models/src/accounts.tsx @@ -19,14 +19,15 @@ */ import { ReactNode, createContext, useContext, useEffect, useMemo, useRef } from "react"; -import { User, UserP } from "./resources"; +import { ServerInfoP, User, UserP } from "./resources"; import { z } from "zod"; import { zdate } from "./utils"; import { removeAccounts, setCookie, updateAccount } from "./account-internal"; import { useMMKVString } from "react-native-mmkv"; import { Platform } from "react-native"; -import { useFetch } from "./query"; import { useQueryClient } from "@tanstack/react-query"; +import { atom, getDefaultStore, useAtomValue, useSetAtom } from "jotai"; +import { useFetch, } from "./query"; import { KyooErrors } from "./kyoo-errors"; export const TokenP = z.object({ @@ -49,6 +50,16 @@ export const AccountP = UserP.and( ); export type Account = z.infer; +const defaultApiUrl = Platform.OS === "web" ? "/api" : null; +const currentApiUrl = atom(defaultApiUrl); +export const getCurrentApiUrl = () => { + const store = getDefaultStore(); + return store.get(currentApiUrl); +}; +export const useCurrentApiUrl = () => { + return useAtomValue(currentApiUrl); +}; + const AccountContext = createContext<(Account & { select: () => void; remove: () => void })[]>([]); export const ConnectionErrorContext = createContext<{ error: KyooErrors | null; @@ -66,10 +77,14 @@ export const AccountProvider = ({ ssrAccount?: Account; ssrError?: KyooErrors; }) => { + const setApiUrl = useSetAtom(currentApiUrl); if (Platform.OS === "web" && typeof window === "undefined") { const accs = ssrAccount ? [{ ...ssrAccount, selected: true, select: () => {}, remove: () => {} }] : []; + + setApiUrl(process.env.KYOO_URL ?? "http://localhost:5000"); + return ( accounts.find((x) => x.selected), [accounts]); + useEffect(() => { + setApiUrl(selected?.apiUrl ?? defaultApiUrl); + }, [selected, setApiUrl]); + const user = useFetch({ path: ["auth", "me"], parser: UserP, @@ -130,6 +149,7 @@ export const AccountProvider = ({ // update cookies for ssr (needs to contains token, theme, language...) if (Platform.OS === "web") { setCookie("account", selected); + // cookie used for images and videos since we can't add Authorization headers in img or video tags. setCookie("X-Bearer", selected?.token.access_token); } }, [selected, queryClient]); @@ -162,10 +182,14 @@ export const useAccounts = () => { export const useHasPermission = (perms?: string[]) => { const account = useAccount(); + const { data } = useFetch({ + path: ["info"], + parser: ServerInfoP, + }); if (!perms || !perms[0]) return true; - // TODO: Read permission of guest account here. - if (!account) return false; - return perms.every((perm) => account.permissions.includes(perm)); + const available = account?.permissions ?? data?.guestPermissions; + if (!available) return false; + return perms.every((perm) => available.includes(perm)); }; diff --git a/front/packages/models/src/login.ts b/front/packages/models/src/login.ts index 193fb85a..20e54d69 100644 --- a/front/packages/models/src/login.ts +++ b/front/packages/models/src/login.ts @@ -20,14 +20,9 @@ import { queryFn } from "./query"; import { KyooErrors } from "./kyoo-errors"; -import { Account, TokenP } from "./accounts"; +import { Account, TokenP, getCurrentApiUrl } from "./accounts"; import { UserP } from "./resources"; -import { - addAccount, - getCurrentAccount, - removeAccounts, - updateAccount, -} from "./account-internal"; +import { addAccount, getCurrentAccount, removeAccounts, updateAccount } from "./account-internal"; import { Platform } from "react-native"; type Result = @@ -38,6 +33,7 @@ export const login = async ( action: "register" | "login", { apiUrl, ...body }: { username: string; password: string; email?: string; apiUrl?: string }, ): Promise> => { + apiUrl ??= getCurrentApiUrl()!; try { const controller = new AbortController(); setTimeout(() => controller.abort(), 5_000); @@ -57,7 +53,7 @@ export const login = async ( UserP, `Bearer ${token.access_token}`, ); - const account: Account = { ...user, apiUrl: apiUrl ?? "/api", token, selected: true }; + const account: Account = { ...user, apiUrl: apiUrl, token, selected: true }; addAccount(account); return { ok: true, value: account }; } catch (e) { @@ -67,6 +63,7 @@ export const login = async ( }; export const oidcLogin = async (provider: string, code: string, apiUrl?: string) => { + apiUrl ??= getCurrentApiUrl()!; try { const token = await queryFn( { @@ -82,7 +79,7 @@ export const oidcLogin = async (provider: string, code: string, apiUrl?: string) UserP, `Bearer ${token.access_token}`, ); - const account: Account = { ...user, apiUrl: apiUrl ?? "/api", token, selected: true }; + const account: Account = { ...user, apiUrl: apiUrl, token, selected: true }; addAccount(account); return { ok: true, value: account }; } catch (e) { diff --git a/front/packages/models/src/query.tsx b/front/packages/models/src/query.tsx index 338172a3..1a9acb1e 100644 --- a/front/packages/models/src/query.tsx +++ b/front/packages/models/src/query.tsx @@ -29,14 +29,8 @@ import { import { z } from "zod"; import { KyooErrors } from "./kyoo-errors"; import { Page, Paged } from "./page"; -import { Platform } from "react-native"; import { getToken } from "./login"; -import { getCurrentAccount } from "./account-internal"; - -const kyooUrl = - typeof window === "undefined" ? process.env.KYOO_URL ?? "http://localhost:5000" : "/api"; -// The url of kyoo, set after each query (used by the image parser). -export let kyooApiUrl = kyooUrl; +import { getCurrentApiUrl } from "."; export const queryFn = async ( context: { @@ -55,9 +49,7 @@ export const queryFn = async ( type?: Parser, token?: string | null, ): Promise> => { - const url = context.apiUrl ?? (Platform.OS === "web" ? kyooUrl : getCurrentAccount()?.apiUrl); - kyooApiUrl = url; - + const url = context.apiUrl ?? getCurrentApiUrl(); if (token === undefined && context.authenticated !== false) token = await getToken(); const path = [url] .concat( diff --git a/front/packages/models/src/traits/images.ts b/front/packages/models/src/traits/images.ts index 47667ab6..51228982 100644 --- a/front/packages/models/src/traits/images.ts +++ b/front/packages/models/src/traits/images.ts @@ -20,11 +20,12 @@ import { Platform } from "react-native"; import { ZodObject, ZodRawShape, z } from "zod"; -import { kyooApiUrl } from ".."; +import { getCurrentApiUrl } from ".."; -export const imageFn = (url: string) => (Platform.OS === "web" ? `/api${url}` : kyooApiUrl + url); +export const imageFn = (url: string) => + Platform.OS === "web" ? `/api${url}` : `${getCurrentApiUrl()!}${url}`; -export const baseAppUrl = () => Platform.OS === "web" ? window.location.origin : "kyoo://"; +export const baseAppUrl = () => (Platform.OS === "web" ? window.location.origin : "kyoo://"); export const Img = z.object({ source: z.string(), diff --git a/front/packages/ui/src/login/login.tsx b/front/packages/ui/src/login/login.tsx index 2ea36b29..d26bc029 100644 --- a/front/packages/ui/src/login/login.tsx +++ b/front/packages/ui/src/login/login.tsx @@ -20,9 +20,8 @@ import { login, QueryPage } from "@kyoo/models"; import { Button, P, Input, ts, H1, A } from "@kyoo/primitives"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Platform } from "react-native"; import { Trans } from "react-i18next"; import { useRouter } from "solito/router"; import { percent, px, useYoshiki } from "yoshiki/native"; @@ -30,8 +29,12 @@ import { DefaultLayout } from "../layout"; import { FormPage } from "./form"; import { PasswordInput } from "./password-input"; import { OidcLogin } from "./oidc"; +import { Platform } from "react-native"; -export const LoginPage: QueryPage<{ error?: string }> = ({ error: initialError }) => { +export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({ + apiUrl, + error: initialError, +}) => { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(initialError); @@ -40,10 +43,17 @@ export const LoginPage: QueryPage<{ error?: string }> = ({ error: initialError } const { t } = useTranslation(); const { css } = useYoshiki(); + useEffect(() => { + if (!apiUrl && Platform.OS !== "web") + router.replace("/server-url", undefined, { + experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false }, + }); + }, [apiUrl, router]); + return (

{t("login.login")}

- +

{t("login.username")}

setUsername(value)} />

{t("login.password")}

@@ -59,7 +69,7 @@ export const LoginPage: QueryPage<{ error?: string }> = ({ error: initialError } const { error } = await login("login", { username, password, - apiUrl: cleanApiUrl(apiUrl), + apiUrl, }); setError(error); if (error) return; diff --git a/front/packages/ui/src/login/oidc.tsx b/front/packages/ui/src/login/oidc.tsx index ac297413..e9e08cae 100644 --- a/front/packages/ui/src/login/oidc.tsx +++ b/front/packages/ui/src/login/oidc.tsx @@ -34,10 +34,10 @@ import { useEffect, useRef } from "react"; import { useRouter } from "solito/router"; import { ErrorView } from "../errors"; -export const OidcLogin = () => { +export const OidcLogin = ({ apiUrl }: { apiUrl?: string }) => { const { css } = useYoshiki(); const { t } = useTranslation(); - const { data, error } = useFetch(OidcLogin.query()); + const { data, error } = useFetch({ options: { apiUrl }, ...OidcLogin.query() }); const btn = css({ width: { xs: percent(100), sm: percent(75) }, marginY: ts(1) }); @@ -48,7 +48,7 @@ export const OidcLogin = () => { ) : data ? ( Object.values(data.oidc).map((x) => (