Add continue with oidc button on login and register pages

This commit is contained in:
Zoe Roux 2024-03-03 16:10:46 +01:00
parent 5f8d0d1b99
commit 239ad9a4dc
9 changed files with 184 additions and 22 deletions

View File

@ -22,11 +22,21 @@ namespace Kyoo.Authentication.Models;
public class ServerInfo public class ServerInfo
{ {
/// <summary>
/// The list of oidc providers configured for this instance of kyoo.
/// </summary>
public Dictionary<string, OidcInfo> Oidc { get; set; } public Dictionary<string, OidcInfo> Oidc { get; set; }
} }
public class OidcInfo public class OidcInfo
{ {
/// <summary>
/// The name of this oidc service. Human readable.
/// </summary>
public string DisplayName { get; set; } public string DisplayName { get; set; }
/// <summary>
/// A url returing a square logo for this provider.
/// </summary>
public string? LogoUrl { get; set; } public string? LogoUrl { get; set; }
} }

View File

@ -32,3 +32,4 @@ export * from "./watch-info";
export * from "./watch-status"; export * from "./watch-status";
export * from "./watchlist"; export * from "./watchlist";
export * from "./user"; export * from "./user";
export * from "./server-info";

View File

@ -0,0 +1,54 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { z } from "zod";
import { imageFn } from "..";
export const OidcInfoP = z.object({
/*
* The name of this oidc service. Human readable.
*/
displayName: z.string(),
/*
* A url returing a square logo for this provider.
*/
logoUrl: z.string().nullable(),
});
export const ServerInfoP = z.object({
/*
* The list of oidc providers configured for this instance of kyoo.
*/
oidc: z
.record(z.string(), OidcInfoP)
.transform((x) =>
Object.fromEntries(
Object.entries(x).map(([provider, info]) => [
provider,
{ ...info, link: imageFn(`/auth/login/${provider}`) },
]),
),
),
});
/**
* A season of a Show.
*/
export type ServerInfo = z.infer<typeof ServerInfoP>;

View File

@ -23,14 +23,17 @@ import { Theme, useYoshiki } from "yoshiki/native";
import { PressableFeedback } from "./links"; import { PressableFeedback } from "./links";
import { P } from "./text"; import { P } from "./text";
import { ts } from "./utils"; import { ts } from "./utils";
import { View } from "react-native"; import { Falsy, View } from "react-native";
export const Button = forwardRef< export const Button = forwardRef<
View, View,
{ text: string; licon?: ReactElement; icon?: ReactElement } & ComponentProps< {
typeof PressableFeedback children?: ReactElement | Falsy;
> text?: string;
>(function Button({ text, icon, licon, ...props }, ref) { licon?: ReactElement | Falsy;
icon?: ReactElement | Falsy;
} & ComponentProps<typeof PressableFeedback>
>(function Button({ children, text, icon, licon, ...props }, ref) {
const { css } = useYoshiki("button"); const { css } = useYoshiki("button");
return ( return (
@ -55,17 +58,20 @@ export const Button = forwardRef<
props as any, props as any,
)} )}
> >
<View {(licon || text || icon) != null && (
{...css({ <View
paddingX: ts(3), {...css({
flexDirection: "row", paddingX: ts(3),
alignItems: "center", flexDirection: "row",
})} alignItems: "center",
> })}
{licon} >
<P {...css({ textAlign: "center" }, "text")}>{text}</P> {licon}
{icon} {text && <P {...css({ textAlign: "center" }, "text")}>{text}</P>}
</View> {icon}
</View>
)}
{children}
</PressableFeedback> </PressableFeedback>
); );
}); });

View File

@ -19,7 +19,7 @@
*/ */
import { HR as EHR } from "@expo/html-elements"; import { HR as EHR } from "@expo/html-elements";
import { px, Stylable, useYoshiki } from "yoshiki/native"; import { percent, px, Stylable, useYoshiki } from "yoshiki/native";
import { ts } from "./utils"; import { ts } from "./utils";
export const HR = ({ export const HR = ({
@ -39,13 +39,13 @@ export const HR = ({
}, },
orientation === "vertical" && { orientation === "vertical" && {
width: px(1), width: px(1),
height: "auto", height: percent(100),
marginY: ts(1), marginY: ts(1),
marginX: ts(2), marginX: ts(2),
}, },
orientation === "horizontal" && { orientation === "horizontal" && {
height: px(1), height: px(1),
width: "auto", width: percent(100),
marginX: ts(1), marginX: ts(1),
marginY: ts(2), marginY: ts(2),
}, },

View File

@ -29,7 +29,7 @@ import { percent, px, useYoshiki } from "yoshiki/native";
import { DefaultLayout } from "../layout"; 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 { OidcLogin } from "./oidc";
export const cleanApiUrl = (apiUrl: string) => { export const cleanApiUrl = (apiUrl: string) => {
if (Platform.OS === "web") return undefined; if (Platform.OS === "web") return undefined;
@ -45,7 +45,6 @@ export const LoginPage: QueryPage = () => {
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient();
const { t } = useTranslation(); const { t } = useTranslation();
const { css } = useYoshiki(); const { css } = useYoshiki();
@ -56,6 +55,7 @@ export const LoginPage: QueryPage = () => {
})} })}
> >
<H1>{t("login.login")}</H1> <H1>{t("login.login")}</H1>
<OidcLogin />
{Platform.OS !== "web" && ( {Platform.OS !== "web" && (
<> <>
<P {...css({ paddingLeft: ts(1) })}>{t("login.server")}</P> <P {...css({ paddingLeft: ts(1) })}>{t("login.server")}</P>
@ -102,4 +102,6 @@ export const LoginPage: QueryPage = () => {
); );
}; };
LoginPage.getFetchUrls = () => [OidcLogin.query()];
LoginPage.getLayout = DefaultLayout; LoginPage.getLayout = DefaultLayout;

View File

@ -0,0 +1,83 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryIdentifier, ServerInfo, ServerInfoP, useFetch } from "@kyoo/models";
import { Button, HR, P, Skeleton, tooltip, ts } from "@kyoo/primitives";
import { View, ImageBackground } from "react-native";
import { percent, rem, useYoshiki } from "yoshiki/native";
import { useTranslation } from "react-i18next";
import { ErrorView } from "../errors";
export const OidcLogin = () => {
const { css } = useYoshiki();
const { t } = useTranslation();
const { data, error } = useFetch(OidcLogin.query());
const btn = css({ width: { xs: percent(100), sm: percent(75) }, marginY: ts(1) });
return (
<View {...css({ alignItems: "center", marginY: ts(1) })}>
{error ? (
<ErrorView error={error} />
) : data ? (
Object.values(data.oidc).map((x) => (
<Button
href={x.link}
key={x.displayName}
licon={
x.logoUrl != null && (
<ImageBackground
source={{ uri: x.logoUrl }}
{...css({ width: ts(3), height: ts(3), marginRight: ts(2) })}
/>
)
}
text={t("login.via", { provider: x.displayName })}
{...tooltip(t("login.via", { provider: x.displayName }))}
{...btn}
/>
))
) : (
[...Array(3)].map((_, i) => (
<Button key={i} {...btn}>
<Skeleton {...css({ width: percent(66), marginY: rem(0.5) })} />
</Button>
))
)}
<View
{...css({
marginY: ts(1),
flexDirection: "row",
width: percent(100),
alignItems: "center",
})}
>
<HR {...css({ flexGrow: 1 })} />
<P>{t("misc.or")}</P>
<HR {...css({ flexGrow: 1 })} />
</View>
</View>
);
};
OidcLogin.query = (): QueryIdentifier<ServerInfo> => ({
path: ["info"],
parser: ServerInfoP,
});

View File

@ -30,6 +30,7 @@ import { DefaultLayout } from "../layout";
import { FormPage } from "./form"; import { FormPage } from "./form";
import { PasswordInput } from "./password-input"; import { PasswordInput } from "./password-input";
import { cleanApiUrl } from "./login"; import { cleanApiUrl } from "./login";
import { OidcLogin } from "./oidc";
export const RegisterPage: QueryPage = () => { export const RegisterPage: QueryPage = () => {
const [apiUrl, setApiUrl] = useState(""); const [apiUrl, setApiUrl] = useState("");
@ -46,6 +47,7 @@ export const RegisterPage: QueryPage = () => {
return ( return (
<FormPage> <FormPage>
<H1>{t("login.register")}</H1> <H1>{t("login.register")}</H1>
<OidcLogin />
{Platform.OS !== "web" && ( {Platform.OS !== "web" && (
<> <>
<P {...css({ paddingLeft: ts(1) })}>{t("login.server")}</P> <P {...css({ paddingLeft: ts(1) })}>{t("login.server")}</P>
@ -109,4 +111,6 @@ export const RegisterPage: QueryPage = () => {
); );
}; };
RegisterPage.getFetchUrls = () => [OidcLogin.query()];
RegisterPage.getLayout = DefaultLayout; RegisterPage.getLayout = DefaultLayout;

View File

@ -68,7 +68,8 @@
"more": "More", "more": "More",
"expand": "Expand", "expand": "Expand",
"collapse": "Collapse", "collapse": "Collapse",
"edit": "Edit" "edit": "Edit",
"or": "OR"
}, },
"navbar": { "navbar": {
"home": "Home", "home": "Home",
@ -165,6 +166,7 @@
"login": { "login": {
"login": "Login", "login": "Login",
"register": "Register", "register": "Register",
"via": "Continue via {{provider}}",
"add-account": "Add account", "add-account": "Add account",
"logout": "Logout", "logout": "Logout",
"server": "Server Address", "server": "Server Address",