mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Rework apiurl handling to allow guests login on android
This commit is contained in:
parent
158058b720
commit
d52cc045e0
@ -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";
|
||||
|
@ -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<typeof AccountP>;
|
||||
|
||||
const defaultApiUrl = Platform.OS === "web" ? "/api" : null;
|
||||
const currentApiUrl = atom<string | null>(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 (
|
||||
<AccountContext.Provider value={accs}>
|
||||
<ConnectionErrorContext.Provider
|
||||
@ -103,6 +118,10 @@ export const AccountProvider = ({
|
||||
|
||||
// update user's data from kyoo un startup, it could have changed.
|
||||
const selected = useMemo(() => 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));
|
||||
};
|
||||
|
@ -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<A, B> =
|
||||
@ -38,6 +33,7 @@ export const login = async (
|
||||
action: "register" | "login",
|
||||
{ apiUrl, ...body }: { username: string; password: string; email?: string; apiUrl?: string },
|
||||
): Promise<Result<Account, string>> => {
|
||||
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) {
|
||||
|
@ -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 <Parser extends z.ZodTypeAny>(
|
||||
context: {
|
||||
@ -55,9 +49,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
||||
type?: Parser,
|
||||
token?: string | null,
|
||||
): Promise<z.infer<Parser>> => {
|
||||
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(
|
||||
|
@ -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(),
|
||||
|
@ -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<string | undefined>(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 (
|
||||
<FormPage>
|
||||
<H1>{t("login.login")}</H1>
|
||||
<OidcLogin />
|
||||
<OidcLogin apiUrl={apiUrl} />
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
|
||||
<Input autoComplete="username" variant="big" onChangeText={(value) => setUsername(value)} />
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
|
||||
@ -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;
|
||||
|
@ -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) => (
|
||||
<Button
|
||||
href={x.link}
|
||||
href={apiUrl ? `${x.link}&apiUrl=${apiUrl}` : x.link}
|
||||
key={x.displayName}
|
||||
licon={
|
||||
x.logoUrl != null && (
|
||||
@ -90,11 +90,12 @@ OidcLogin.query = (): QueryIdentifier<ServerInfo> => ({
|
||||
parser: ServerInfoP,
|
||||
});
|
||||
|
||||
export const OidcCallbackPage: QueryPage<{ provider: string; code: string; error?: string }> = ({
|
||||
provider,
|
||||
code,
|
||||
error,
|
||||
}) => {
|
||||
export const OidcCallbackPage: QueryPage<{
|
||||
apiUrl?: string;
|
||||
provider: string;
|
||||
code: string;
|
||||
error?: string;
|
||||
}> = ({ apiUrl, provider, code, error }) => {
|
||||
const hasRun = useRef(false);
|
||||
const router = useRouter();
|
||||
|
||||
@ -103,12 +104,12 @@ export const OidcCallbackPage: QueryPage<{ provider: string; code: string; error
|
||||
hasRun.current = true;
|
||||
|
||||
function onError(error: string) {
|
||||
router.replace(`/login?error=${error}`, undefined, {
|
||||
router.replace(`/login?error=${error}${apiUrl ? `&apiUrl=${apiUrl}` : ""}`, undefined, {
|
||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
||||
});
|
||||
}
|
||||
async function run() {
|
||||
const { error: loginError } = await oidcLogin(provider, code);
|
||||
const { error: loginError } = await oidcLogin(provider, code, apiUrl);
|
||||
if (loginError) onError(loginError);
|
||||
else {
|
||||
router.replace("/", undefined, {
|
||||
@ -119,6 +120,6 @@ export const OidcCallbackPage: QueryPage<{ provider: string; code: string; error
|
||||
|
||||
if (error) onError(error);
|
||||
else run();
|
||||
}, [provider, code, router, error]);
|
||||
}, [provider, code, apiUrl, router, error]);
|
||||
return <P>{"Loading"}</P>;
|
||||
};
|
||||
|
@ -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,9 @@ import { DefaultLayout } from "../layout";
|
||||
import { FormPage } from "./form";
|
||||
import { PasswordInput } from "./password-input";
|
||||
import { OidcLogin } from "./oidc";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
export const RegisterPage: QueryPage = () => {
|
||||
export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
@ -42,17 +42,17 @@ export const RegisterPage: QueryPage = () => {
|
||||
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 (
|
||||
<FormPage>
|
||||
<H1>{t("login.register")}</H1>
|
||||
<OidcLogin />
|
||||
{Platform.OS !== "web" && (
|
||||
<>
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.server")}</P>
|
||||
<Input variant="big" onChangeText={setApiUrl} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<OidcLogin apiUrl={apiUrl} />
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
|
||||
<Input autoComplete="username" variant="big" onChangeText={(value) => setUsername(value)} />
|
||||
|
||||
@ -81,10 +81,7 @@ export const RegisterPage: QueryPage = () => {
|
||||
text={t("login.register")}
|
||||
disabled={password !== confirm}
|
||||
onPress={async () => {
|
||||
const { error } = await login(
|
||||
"register",
|
||||
{ email, username, password, apiUrl: cleanApiUrl(apiUrl) },
|
||||
);
|
||||
const { error } = await login("register", { email, username, password, apiUrl });
|
||||
setError(error);
|
||||
if (error) return;
|
||||
router.replace("/", undefined, {
|
||||
|
Loading…
x
Reference in New Issue
Block a user