mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 12:14:46 -04:00
Rework login and navbar account display
This commit is contained in:
parent
fbf43fb10f
commit
0ac388f3eb
@ -18,102 +18,74 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from "zod";
|
|
||||||
import { deleteSecureItem, getSecureItem, setSecureItem } from "./secure-store";
|
|
||||||
import { zdate } from "./utils";
|
|
||||||
import { queryFn } from "./query";
|
import { queryFn } from "./query";
|
||||||
import { KyooErrors } from "./kyoo-errors";
|
import { KyooErrors } from "./kyoo-errors";
|
||||||
import { Platform } from "react-native";
|
import { Account, Token, TokenP } from "./accounts";
|
||||||
|
import { UserP } from "./resources";
|
||||||
const TokenP = z.object({
|
import { addAccount, getCurrentAccount, removeAccounts, updateAccount } from "./account-internal";
|
||||||
token_type: z.literal("Bearer"),
|
|
||||||
access_token: z.string(),
|
|
||||||
refresh_token: z.string(),
|
|
||||||
expire_in: z.string(),
|
|
||||||
expire_at: zdate(),
|
|
||||||
});
|
|
||||||
type Token = z.infer<typeof TokenP>;
|
|
||||||
|
|
||||||
type Result<A, B> =
|
type Result<A, B> =
|
||||||
| { ok: true; value: A; error?: undefined }
|
| { ok: true; value: A; error?: undefined }
|
||||||
| { ok: false; value?: undefined; error: B };
|
| { ok: false; value?: undefined; error: B };
|
||||||
|
|
||||||
export type Account = Token & { apiUrl: string; username: string };
|
export const login = async (
|
||||||
|
action: "register" | "login",
|
||||||
const addAccount = (token: Token, apiUrl: string, username: string | null) => {
|
{ apiUrl, ...body }: { username: string; password: string; email?: string; apiUrl?: string },
|
||||||
const accounts: Account[] = JSON.parse(getSecureItem("accounts") ?? "[]");
|
|
||||||
if (accounts.find((x) => x.username === username && x.apiUrl === apiUrl)) return;
|
|
||||||
accounts.push({ ...token, username: username!, apiUrl });
|
|
||||||
setSecureItem("accounts", JSON.stringify(accounts));
|
|
||||||
setSecureItem("selected", (accounts.length - 1).toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCurrentAccountToken = (token: Token) => {
|
|
||||||
const accounts: Account[] = JSON.parse(getSecureItem("accounts") ?? "[]");
|
|
||||||
const selected = parseInt(getSecureItem("selected") ?? "0");
|
|
||||||
if (selected >= accounts.length) return;
|
|
||||||
|
|
||||||
accounts[selected] = { ...accounts[selected], ...token };
|
|
||||||
setSecureItem("accounts", JSON.stringify(accounts));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loginFunc = async (
|
|
||||||
action: "register" | "login" | "refresh",
|
|
||||||
body: { username: string; password: string; email?: string } | string,
|
|
||||||
apiUrl?: string,
|
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
): Promise<Result<Token, string>> => {
|
): Promise<Result<Account, string>> => {
|
||||||
try {
|
try {
|
||||||
const controller = timeout !== undefined ? new AbortController() : undefined;
|
|
||||||
if (controller) setTimeout(() => controller.abort(), timeout);
|
|
||||||
const token = await queryFn(
|
const token = await queryFn(
|
||||||
{
|
{
|
||||||
path: ["auth", action, typeof body === "string" && `?token=${body}`],
|
path: ["auth", action],
|
||||||
method: typeof body === "string" ? "GET" : "POST",
|
method: "POST",
|
||||||
body: typeof body === "object" ? body : undefined,
|
body,
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
apiUrl,
|
apiUrl,
|
||||||
abortSignal: controller?.signal,
|
timeout,
|
||||||
},
|
},
|
||||||
TokenP,
|
TokenP,
|
||||||
);
|
);
|
||||||
|
const user = await queryFn(
|
||||||
if (typeof window !== "undefined") setSecureItem("auth", JSON.stringify(token));
|
{ path: ["auth", "me"], method: "GET", apiUrl },
|
||||||
if (Platform.OS !== "web" && apiUrl && typeof body !== "string")
|
UserP,
|
||||||
addAccount(token, apiUrl, body.username);
|
token.access_token,
|
||||||
else if (Platform.OS !== "web" && action === "refresh") setCurrentAccountToken(token);
|
);
|
||||||
return { ok: true, value: token };
|
const account: Account = { ...user, apiUrl: apiUrl ?? "/api", token, selected: true };
|
||||||
|
addAccount(account);
|
||||||
|
return { ok: true, value: account };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(action, e);
|
console.error(action, e);
|
||||||
return { ok: false, error: (e as KyooErrors).errors[0] };
|
return { ok: false, error: (e as KyooErrors).errors[0] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTokenWJ = async (cookies?: string): Promise<[string, Token] | [null, null]> => {
|
export const getTokenWJ = async (account?: Account | null): Promise<[string, Token] | [null, null]> => {
|
||||||
const tokenStr = getSecureItem("auth", cookies);
|
if (account === null)
|
||||||
if (!tokenStr) return [null, null];
|
account = getCurrentAccount();
|
||||||
let token = TokenP.parse(JSON.parse(tokenStr));
|
if (!account) return [null, null];
|
||||||
|
|
||||||
if (token.expire_at <= new Date(new Date().getTime() + 10 * 1000)) {
|
if (account.token.expire_at <= new Date(new Date().getTime() + 10 * 1000)) {
|
||||||
const { ok, value: nToken, error } = await loginFunc("refresh", token.refresh_token);
|
try {
|
||||||
if (!ok) console.error("Error refreshing token durring ssr:", error);
|
const token = await queryFn(
|
||||||
else token = nToken;
|
{
|
||||||
|
path: ["auth", "refresh", `?token=${account.token.refresh_token}`],
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
TokenP,
|
||||||
|
);
|
||||||
|
updateAccount(account.id, { ...account, token });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error refreshing token durring ssr:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [`${token.token_type} ${token.access_token}`, token];
|
return [`${account.token.token_type} ${account.token.access_token}`, account.token];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getToken = async (cookies?: string): Promise<string | null> =>
|
export const getToken = async (): Promise<string | null> =>
|
||||||
(await getTokenWJ(cookies))[0];
|
(await getTokenWJ())[0];
|
||||||
|
|
||||||
export const logout = () => {
|
export const logout = () => {
|
||||||
if (Platform.OS !== "web") {
|
removeAccounts((x) => x.selected);
|
||||||
let accounts: Account[] = JSON.parse(getSecureItem("accounts") ?? "[]");
|
|
||||||
const selected = parseInt(getSecureItem("selected") ?? "0");
|
|
||||||
accounts.splice(selected, 1);
|
|
||||||
setSecureItem("accounts", JSON.stringify(accounts));
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSecureItem("auth");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteAccount = async () => {
|
export const deleteAccount = async () => {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loginFunc, QueryPage } from "@kyoo/models";
|
import { login, QueryPage } from "@kyoo/models";
|
||||||
import { Button, P, Input, ts, H1, A } from "@kyoo/primitives";
|
import { Button, P, Input, ts, H1, A } from "@kyoo/primitives";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -74,10 +74,9 @@ export const LoginPage: QueryPage = () => {
|
|||||||
<Button
|
<Button
|
||||||
text={t("login.login")}
|
text={t("login.login")}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
const { error } = await loginFunc("login", { username, password }, cleanApiUrl(apiUrl));
|
const { error } = await login("login", { username, password, apiUrl: cleanApiUrl(apiUrl) });
|
||||||
setError(error);
|
setError(error);
|
||||||
if (error) return;
|
if (error) return;
|
||||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
|
||||||
router.replace("/", undefined, {
|
router.replace("/", undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
||||||
});
|
});
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loginFunc, QueryPage } from "@kyoo/models";
|
import { login, QueryPage } from "@kyoo/models";
|
||||||
import { Button, P, Input, ts, H1, A } from "@kyoo/primitives";
|
import { Button, P, Input, ts, H1, A } from "@kyoo/primitives";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -30,7 +30,6 @@ import { DefaultLayout } from "../layout";
|
|||||||
import { FormPage } from "./form";
|
import { FormPage } from "./form";
|
||||||
import { PasswordInput } from "./password-input";
|
import { PasswordInput } from "./password-input";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { setSecureItem } from "@kyoo/models/src/secure-store";
|
|
||||||
import { cleanApiUrl } from "./login";
|
import { cleanApiUrl } from "./login";
|
||||||
|
|
||||||
export const RegisterPage: QueryPage = () => {
|
export const RegisterPage: QueryPage = () => {
|
||||||
@ -84,15 +83,13 @@ export const RegisterPage: QueryPage = () => {
|
|||||||
text={t("login.register")}
|
text={t("login.register")}
|
||||||
disabled={password !== confirm}
|
disabled={password !== confirm}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
const { error } = await loginFunc(
|
const { error } = await login(
|
||||||
"register",
|
"register",
|
||||||
{ email, username, password },
|
{ email, username, password, apiUrl: cleanApiUrl(apiUrl) },
|
||||||
cleanApiUrl(apiUrl),
|
|
||||||
5_000,
|
5_000,
|
||||||
);
|
);
|
||||||
setError(error);
|
setError(error);
|
||||||
if (error) return;
|
if (error) return;
|
||||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
|
||||||
router.replace("/", undefined, {
|
router.replace("/", undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
||||||
});
|
});
|
||||||
|
@ -18,7 +18,15 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AccountContext, deleteAccount, logout, QueryIdentifier, User, UserP } from "@kyoo/models";
|
import {
|
||||||
|
deleteAccount,
|
||||||
|
logout,
|
||||||
|
QueryIdentifier,
|
||||||
|
useAccount,
|
||||||
|
useAccounts,
|
||||||
|
User,
|
||||||
|
UserP,
|
||||||
|
} from "@kyoo/models";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Input,
|
Input,
|
||||||
@ -99,79 +107,65 @@ const getDisplayUrl = (url: string) => {
|
|||||||
export const NavbarProfile = () => {
|
export const NavbarProfile = () => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const account = useAccount();
|
||||||
const { accounts, selected, setSelected } = useContext(AccountContext);
|
const accounts = useAccounts();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FetchNE query={MeQuery}>
|
<Menu
|
||||||
{({ isError: isGuest, username }) => (
|
Trigger={Avatar}
|
||||||
<Menu
|
as={PressableFeedback}
|
||||||
Trigger={Avatar}
|
placeholder={account?.username}
|
||||||
as={PressableFeedback}
|
alt={t("navbar.login")}
|
||||||
placeholder={username}
|
size={24}
|
||||||
alt={t("navbar.login")}
|
color={theme.colors.white}
|
||||||
size={24}
|
{...css({ marginLeft: ts(1), justifyContent: "center" })}
|
||||||
color={theme.colors.white}
|
{...tooltip(account?.username ?? t("navbar.login"))}
|
||||||
{...css({ marginLeft: ts(1), justifyContent: "center" })}
|
>
|
||||||
{...tooltip(username ?? t("navbar.login"))}
|
{accounts?.map((x, i) => (
|
||||||
>
|
<Menu.Item
|
||||||
{accounts?.map((x, i) => (
|
key={x.id}
|
||||||
<Menu.Item
|
label={`${x.username} - ${getDisplayUrl(x.apiUrl)}`}
|
||||||
key={x.refresh_token}
|
left={<Avatar placeholder={x.username} />}
|
||||||
label={`${x.username} - ${getDisplayUrl(x.apiUrl)}`}
|
selected={x.selected}
|
||||||
left={<Avatar placeholder={x.username} />}
|
onSelect={() => x.select()}
|
||||||
selected={selected === i}
|
/>
|
||||||
onSelect={() => setSelected!(i)}
|
))}
|
||||||
/>
|
{accounts.length > 0 && <HR />}
|
||||||
))}
|
{!accounts ? (
|
||||||
{accounts && accounts.length > 0 && <HR />}
|
<>
|
||||||
{isGuest ? (
|
<Menu.Item label={t("login.login")} icon={Login} href="/login" />
|
||||||
<>
|
<Menu.Item label={t("login.register")} icon={Register} href="/register" />
|
||||||
<Menu.Item label={t("login.login")} icon={Login} href="/login" />
|
</>
|
||||||
<Menu.Item label={t("login.register")} icon={Register} href="/register" />
|
) : (
|
||||||
</>
|
<>
|
||||||
) : (
|
<Menu.Item label={t("login.add-account")} icon={Login} href="/login" />
|
||||||
<>
|
<Menu.Item label={t("login.logout")} icon={Logout} onSelect={logout} />
|
||||||
<Menu.Item label={t("login.add-account")} icon={Login} href="/login" />
|
<Menu.Item
|
||||||
<Menu.Item
|
label={t("login.delete")}
|
||||||
label={t("login.logout")}
|
icon={Delete}
|
||||||
icon={Logout}
|
onSelect={async () => {
|
||||||
onSelect={() => {
|
Alert.alert(
|
||||||
logout();
|
t("login.delete"),
|
||||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
t("login.delete-confirmation"),
|
||||||
}}
|
[
|
||||||
/>
|
{
|
||||||
<Menu.Item
|
text: t("misc.delete"),
|
||||||
label={t("login.delete")}
|
onPress: deleteAccount,
|
||||||
icon={Delete}
|
style: "destructive",
|
||||||
onSelect={async () => {
|
},
|
||||||
Alert.alert(
|
{ text: t("misc.cancel"), style: "cancel" },
|
||||||
t("login.delete"),
|
],
|
||||||
t("login.delete-confirmation"),
|
{
|
||||||
[
|
cancelable: true,
|
||||||
{
|
userInterfaceStyle: theme.mode === "auto" ? "light" : theme.mode,
|
||||||
text: t("misc.delete"),
|
icon: "warning",
|
||||||
onPress: async () => {
|
},
|
||||||
await deleteAccount();
|
);
|
||||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
}}
|
||||||
},
|
/>
|
||||||
style: "destructive",
|
</>
|
||||||
},
|
|
||||||
{ text: t("misc.cancel"), style: "cancel" },
|
|
||||||
],
|
|
||||||
{
|
|
||||||
cancelable: true,
|
|
||||||
userInterfaceStyle: theme.mode === "auto" ? "light" : theme.mode,
|
|
||||||
icon: "warning",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
)}
|
)}
|
||||||
</FetchNE>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const NavbarRight = () => {
|
export const NavbarRight = () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user