mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-06-06 13:12:47 +00:00
Add link settings to add oidc to an account
This commit is contained in:
@@ -15,6 +15,7 @@ require (
|
||||
github.com/swaggo/echo-swagger v1.5.0
|
||||
github.com/swaggo/swag v1.16.6
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.17.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0
|
||||
go.opentelemetry.io/otel v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0
|
||||
@@ -31,6 +32,7 @@ require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
|
||||
+41
-4
@@ -217,6 +217,35 @@ func (h *Handler) TokenToJwt(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) OptionalAuthToJwt(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c *echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
auth := c.Request().Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Invalid bearer format")
|
||||
}
|
||||
token := auth[len("Bearer "):]
|
||||
|
||||
// this is only used to check if it is a session token or a jwt
|
||||
_, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
jwt, err := h.createJwt(ctx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Request().Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt))
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// @title Keibi - Kyoo's auth
|
||||
// @version 1.0
|
||||
// @description Auth system made for kyoo.
|
||||
@@ -348,15 +377,23 @@ func main() {
|
||||
g.POST("/users", h.Register)
|
||||
|
||||
g.POST("/sessions", h.Login)
|
||||
g.GET("/oidc/login/:provider", h.OidcLogin)
|
||||
g.GET("/oidc/logged/:provider", h.OidcLogged)
|
||||
g.GET("/oidc/callback/:provider", h.OidcCallback)
|
||||
r.GET("/sessions", h.ListMySessions)
|
||||
r.DELETE("/sessions", h.Logout)
|
||||
r.DELETE("/sessions/:id", h.Logout)
|
||||
r.DELETE("/oidc/login/:provider", h.OidcUnlink)
|
||||
r.GET("/users/:id/sessions", h.ListUserSessions)
|
||||
|
||||
g.GET("/oidc/login/:provider", h.OidcLogin)
|
||||
r.DELETE("/oidc/login/:provider", h.OidcUnlink)
|
||||
g.GET("/oidc/logged/:provider", h.OidcLogged)
|
||||
|
||||
or := e.Group("/auth")
|
||||
or.Use(h.OptionalAuthToJwt)
|
||||
or.Use(echojwt.WithConfig(echojwt.Config{
|
||||
SigningMethod: "RS256",
|
||||
SigningKey: h.config.JwtPublicKey,
|
||||
}))
|
||||
or.GET("/oidc/callback/:provider", h.OidcCallback)
|
||||
|
||||
r.GET("/keys", h.ListApiKey)
|
||||
r.POST("/keys", h.CreateApiKey)
|
||||
r.DELETE("/keys/:id", h.DeleteApiKey)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -241,15 +242,18 @@ func (h *Handler) exchangeOidcCode(c *echo.Context, provider OidcProviderConfig,
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
slog.Error("Error calling oidc token endpoint: %v", "err", err)
|
||||
return Token{}, echo.NewHTTPError(http.StatusBadGateway, "Could not reach OIDC token endpoint")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
slog.Error("Error on oidc token endpoint: %v", "err", err)
|
||||
return Token{}, echo.NewHTTPError(http.StatusBadGateway, "OIDC token exchange failed")
|
||||
}
|
||||
|
||||
var ret Token
|
||||
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
|
||||
slog.Error("Couldn't decode token: %v", "err", err)
|
||||
return Token{}, echo.NewHTTPError(http.StatusBadGateway, "Invalid OIDC token response")
|
||||
}
|
||||
return ret, nil
|
||||
@@ -286,15 +290,18 @@ func (h *Handler) fetchOidcProfile(c *echo.Context, provider OidcProviderConfig,
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
slog.Error("Error calling oidc profile endpoint: %v", "err", err)
|
||||
return Profile{}, echo.NewHTTPError(http.StatusInternalServerError, "Could not reach OIDC profile endpoint")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var profile RawProfile
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
slog.Error("Error on oidc profile endpoint: %v", "err", err)
|
||||
return Profile{}, echo.NewHTTPError(http.StatusInternalServerError, "Could not fetch OIDC profile")
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil {
|
||||
slog.Error("Error parsing oidc profile: %v", "err", err)
|
||||
return Profile{}, echo.NewHTTPError(http.StatusInternalServerError, "Invalid OIDC profile response")
|
||||
}
|
||||
sub := cmp.Or(profile.Sub, profile.Uid, profile.Id, profile.Guid)
|
||||
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
echootel "github.com/labstack/echo-opentelemetry"
|
||||
"github.com/labstack/echo/v5"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
|
||||
@@ -135,6 +137,7 @@ func setupOtel(ctx context.Context) (func(context.Context) error, error) {
|
||||
logotelglobal.SetLoggerProvider(lp)
|
||||
otel.SetMeterProvider(mp)
|
||||
otel.SetTracerProvider(tp)
|
||||
http.DefaultTransport = otelhttp.NewTransport(http.DefaultTransport)
|
||||
|
||||
// configure shutting down
|
||||
// noop providers do not have a Shudown method
|
||||
|
||||
+10
-12
@@ -2,8 +2,6 @@ import { z } from "zod/v4";
|
||||
|
||||
export const User = z
|
||||
.object({
|
||||
// // keep a default for older versions of the api
|
||||
// .default({}),
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
email: z.string(),
|
||||
@@ -33,17 +31,17 @@ export const User = z
|
||||
audioLanguage: "default",
|
||||
subtitleLanguage: null,
|
||||
}),
|
||||
// externalId: z
|
||||
// .record(
|
||||
// z.string(),
|
||||
// z.object({
|
||||
// id: z.string(),
|
||||
// username: z.string().nullable().default(""),
|
||||
// profileUrl: z.string().nullable(),
|
||||
// }),
|
||||
// )
|
||||
// .default({}),
|
||||
}),
|
||||
oidc: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
username: z.string().nullable().default(""),
|
||||
profileUrl: z.string().nullable(),
|
||||
}),
|
||||
)
|
||||
.default({}),
|
||||
})
|
||||
.transform((x) => ({
|
||||
...x,
|
||||
|
||||
@@ -12,7 +12,6 @@ export const Chip = ({
|
||||
label,
|
||||
href,
|
||||
replace,
|
||||
target,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
@@ -21,7 +20,6 @@ export const Chip = ({
|
||||
label: string;
|
||||
href: string | null;
|
||||
replace?: boolean;
|
||||
target?: string;
|
||||
onPress?: (e: GestureResponderEvent) => void;
|
||||
className?: string;
|
||||
}) => {
|
||||
@@ -29,7 +27,6 @@ export const Chip = ({
|
||||
<Link
|
||||
href={href}
|
||||
replace={replace}
|
||||
target={target}
|
||||
className={cn(
|
||||
"group justify-center overflow-hidden rounded-4xl border border-accent outline-0",
|
||||
size === "small" && "px-2.5 py-1",
|
||||
|
||||
@@ -51,7 +51,6 @@ export const A = ({
|
||||
...props
|
||||
}: TextProps & {
|
||||
href?: string | null;
|
||||
target?: string;
|
||||
replace?: boolean;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
@@ -102,7 +101,6 @@ export const Link = ({
|
||||
href?: string | null;
|
||||
replace?: boolean;
|
||||
download?: boolean;
|
||||
target?: string;
|
||||
} & PressableProps) => {
|
||||
const linkProps = useLinkTo({ href, replace });
|
||||
|
||||
|
||||
@@ -67,8 +67,6 @@ const SearchResultItem = ({
|
||||
icon={OpenInNew}
|
||||
as={Link}
|
||||
href={externalHref}
|
||||
target="_blank"
|
||||
className="absolute top-1 right-1 bg-gray-800/70 hover:bg-gray-800 focus:bg-gray-800"
|
||||
iconClassName="h-5 w-5 fill-slate-200 dark:fill-slate-200"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -121,7 +121,6 @@ const ButtonList = ({
|
||||
icon={Theaters}
|
||||
as={Link}
|
||||
href={trailerUrl}
|
||||
target="_blank"
|
||||
iconClassName={iconsClassName}
|
||||
{...tooltip(t("show.trailer"))}
|
||||
/>
|
||||
@@ -340,7 +339,6 @@ const ExternalIdChip = ({
|
||||
<Chip
|
||||
label={name}
|
||||
href={withLinks.length === 1 ? withLinks[0].link : null}
|
||||
target="_blank"
|
||||
size="small"
|
||||
outline
|
||||
className="m-1"
|
||||
@@ -357,7 +355,6 @@ const ExternalIdChip = ({
|
||||
<A
|
||||
key={x.dataId}
|
||||
href={x.link!}
|
||||
target="_blank"
|
||||
className="rounded p-4 hover:bg-popover"
|
||||
>
|
||||
{x.label ?? x.link}
|
||||
|
||||
@@ -55,22 +55,26 @@ export const login = async (
|
||||
export const oidcLogin = async (
|
||||
provider: string,
|
||||
code: string,
|
||||
linkToToken: string | null,
|
||||
apiUrl?: string,
|
||||
) => {
|
||||
apiUrl ??= defaultApiUrl;
|
||||
try {
|
||||
const { token } = await queryFn({
|
||||
const ret = await queryFn({
|
||||
method: "GET",
|
||||
url: `${apiUrl}/auth/oidc/callback/${provider}?token=${code}`,
|
||||
authToken: null,
|
||||
parser: z.object({ token: z.string() }),
|
||||
});
|
||||
const user = await queryFn({
|
||||
method: "GET",
|
||||
url: `${apiUrl}/auth/users/me`,
|
||||
authToken: token,
|
||||
parser: User,
|
||||
authToken: linkToToken,
|
||||
parser: linkToToken ? z.object({ token: z.string() }) : User,
|
||||
});
|
||||
const token = linkToToken ?? (ret as { token: string }).token;
|
||||
const user = linkToToken
|
||||
? (ret as User)
|
||||
: await queryFn({
|
||||
method: "GET",
|
||||
url: `${apiUrl}/auth/users/me`,
|
||||
authToken: token,
|
||||
parser: User,
|
||||
});
|
||||
const account: Account = { ...user, apiUrl, token, selected: true };
|
||||
addAccount(account);
|
||||
return { ok: true, value: account };
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
import { useRouter } from "expo-router";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { P } from "~/primitives";
|
||||
import { useToken } from "~/providers/account-context";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { oidcLogin } from "./logic";
|
||||
|
||||
export const OidcCallbackPage = () => {
|
||||
const { authToken } = useToken();
|
||||
const [apiUrl] = useQueryState("apiUrl", undefined!);
|
||||
const [provider] = useQueryState("provider", undefined!);
|
||||
const [code] = useQueryState("token", undefined!);
|
||||
const [error] = useQueryState("error", undefined!);
|
||||
const [link] = useQueryState("link", undefined!);
|
||||
|
||||
const hasRun = useRef(false);
|
||||
const router = useRouter();
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: useMountEffect
|
||||
useEffect(() => {
|
||||
if (hasRun.current) return;
|
||||
hasRun.current = true;
|
||||
|
||||
function onError(error: string) {
|
||||
router.replace({ pathname: "/login", params: { error, apiUrl } });
|
||||
}
|
||||
async function run() {
|
||||
const { error: loginError } = await oidcLogin(provider, code, apiUrl);
|
||||
const { error: loginError } = await oidcLogin(
|
||||
provider,
|
||||
code,
|
||||
link ? authToken : null,
|
||||
apiUrl,
|
||||
);
|
||||
if (loginError) onError(loginError);
|
||||
else router.replace("/");
|
||||
else router.replace(link ? "/settings" : "/");
|
||||
}
|
||||
|
||||
if (error) onError(error);
|
||||
else run();
|
||||
}, [provider, code, apiUrl, router, error]);
|
||||
}, []);
|
||||
return <P>{"Loading"}</P>;
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export const OidcLogin = ({
|
||||
<Button
|
||||
as={Link}
|
||||
key={id}
|
||||
href={provider.link}
|
||||
href={provider.connect}
|
||||
replace
|
||||
className="w-full sm:w-3/4"
|
||||
left={
|
||||
@@ -95,7 +95,7 @@ const AuthInfo = z
|
||||
),
|
||||
})
|
||||
.transform((x) => {
|
||||
const baseUrl = Platform.OS === "web" ? x.publicUrl : "kyoo://";
|
||||
const redirect = `${Platform.OS === "web" ? x.publicUrl : "kyoo://"}/oidc-callback?apiUrl=${x.publicUrl}`;
|
||||
return {
|
||||
...x,
|
||||
oidc: Object.fromEntries(
|
||||
@@ -103,7 +103,8 @@ const AuthInfo = z
|
||||
provider,
|
||||
{
|
||||
...info,
|
||||
link: `${x.publicUrl}/auth/oidc/login/${provider}?redirectUrl=${baseUrl}/oidc-callback?apiUrl=${x.publicUrl}`,
|
||||
connect: `${x.publicUrl}/auth/oidc/login/${provider}?redirectUrl=${encodeURIComponent(redirect)}`,
|
||||
link: `${x.publicUrl}/auth/oidc/login/${provider}?redirectUrl=${encodeURIComponent(`${redirect}&link=true`)}`,
|
||||
},
|
||||
]),
|
||||
),
|
||||
|
||||
@@ -55,17 +55,14 @@ export const About = () => {
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.about.label")}>
|
||||
<Link
|
||||
href="https://github.com/zoriya/kyoo/releases/latest/download/kyoo.apk"
|
||||
target="_blank"
|
||||
>
|
||||
<Link href="https://github.com/zoriya/kyoo/releases/latest/download/kyoo.apk">
|
||||
<Preference
|
||||
icon={Android}
|
||||
label={t("settings.about.android-app.label")}
|
||||
description={t("settings.about.android-app.description")}
|
||||
/>
|
||||
</Link>
|
||||
<Link href="https://github.com/zoriya/kyoo" target="_blank">
|
||||
<Link href="https://github.com/zoriya/kyoo">
|
||||
<Preference
|
||||
icon={Public}
|
||||
label={t("settings.about.git.label")}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ScrollView } from "react-native";
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
import { AccountSettings } from "./account";
|
||||
import { About, GeneralSettings } from "./general";
|
||||
// import { OidcSettings } from "./oidc";
|
||||
import { OidcSettings } from "./oidc";
|
||||
import { PlaybackSettings } from "./playback";
|
||||
|
||||
export const SettingsPage = () => {
|
||||
@@ -12,7 +12,7 @@ export const SettingsPage = () => {
|
||||
<GeneralSettings />
|
||||
{account && <PlaybackSettings />}
|
||||
{account && <AccountSettings />}
|
||||
{/* {account && <OidcSettings />} */}
|
||||
{account && <OidcSettings />}
|
||||
<About />
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
+96
-105
@@ -1,105 +1,96 @@
|
||||
// import {
|
||||
// type QueryIdentifier,
|
||||
// type ServerInfo,
|
||||
// ServerInfoP,
|
||||
// queryFn,
|
||||
// useAccount,
|
||||
// useFetch,
|
||||
// } from "@kyoo/models";
|
||||
// import { Button, IconButton, Link, Skeleton, tooltip, ts } from "@kyoo/primitives";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
// import { ImageBackground } from "react-native";
|
||||
// import { Preference, SettingsContainer } from "./base";
|
||||
//
|
||||
// import Badge from "@material-symbols/svg-400/outlined/badge.svg";
|
||||
// import Remove from "@material-symbols/svg-400/outlined/close.svg";
|
||||
// import OpenProfile from "@material-symbols/svg-400/outlined/open_in_new.svg";
|
||||
// import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
//
|
||||
// export const OidcSettings = () => {
|
||||
// const account = useAccount()!;
|
||||
// const { t } = useTranslation();
|
||||
// const { data, error } = useFetch(OidcSettings.query());
|
||||
// const queryClient = useQueryClient();
|
||||
// const { mutateAsync: unlinkAccount } = useMutation({
|
||||
// mutationFn: async (provider: string) =>
|
||||
// await queryFn({
|
||||
// path: ["auth", "login", provider],
|
||||
// method: "DELETE",
|
||||
// }),
|
||||
// onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["auth", "me"] }),
|
||||
// });
|
||||
//
|
||||
// return (
|
||||
// <SettingsContainer title={t("settings.oidc.label")}>
|
||||
// {error ? (
|
||||
// <ErrorView error={error} />
|
||||
// ) : data ? (
|
||||
// Object.entries(data.oidc).map(([id, x]) => {
|
||||
// const acc = account.externalId[id];
|
||||
// return (
|
||||
// <Preference
|
||||
// key={x.displayName}
|
||||
// icon={Badge}
|
||||
// label={x.displayName}
|
||||
// description={
|
||||
// acc
|
||||
// ? t("settings.oidc.connected", { username: acc.username })
|
||||
// : t("settings.oidc.not-connected")
|
||||
// }
|
||||
// customIcon={
|
||||
// x.logoUrl != null && (
|
||||
// <ImageBackground
|
||||
// source={{ uri: x.logoUrl }}
|
||||
// {...css({ width: ts(3), height: ts(3), marginRight: ts(2) })}
|
||||
// />
|
||||
// )
|
||||
// }
|
||||
// >
|
||||
// {acc ? (
|
||||
// <>
|
||||
// {acc.profileUrl && (
|
||||
// <IconButton
|
||||
// icon={OpenProfile}
|
||||
// as={Link}
|
||||
// href={acc.profileUrl}
|
||||
// target="_blank"
|
||||
// {...tooltip(t("settings.oidc.open-profile", { provider: x.displayName }))}
|
||||
// />
|
||||
// )}
|
||||
// <IconButton
|
||||
// icon={Remove}
|
||||
// onPress={() => unlinkAccount(id)}
|
||||
// {...tooltip(t("settings.oidc.delete", { provider: x.displayName }))}
|
||||
// />
|
||||
// </>
|
||||
// ) : (
|
||||
// <Button
|
||||
// text={t("settings.oidc.link")}
|
||||
// as={Link}
|
||||
// href={x.link}
|
||||
// {...css({ minWidth: rem(6) })}
|
||||
// />
|
||||
// )}
|
||||
// </Preference>
|
||||
// );
|
||||
// })
|
||||
// ) : (
|
||||
// [...Array(3)].map((_, i) => (
|
||||
// <Preference
|
||||
// key={i}
|
||||
// customIcon={<Skeleton {...css({ width: ts(3), height: ts(3) })} />}
|
||||
// icon={null!}
|
||||
// label={<Skeleton {...css({ width: rem(6) })} />}
|
||||
// description={<Skeleton {...css({ width: rem(7), height: rem(0.8) })} />}
|
||||
// />
|
||||
// ))
|
||||
// )}
|
||||
// </SettingsContainer>
|
||||
// );
|
||||
// };
|
||||
//
|
||||
// OidcSettings.query = (): QueryIdentifier<ServerInfo> => ({
|
||||
// path: ["info"],
|
||||
// parser: ServerInfoP,
|
||||
// });
|
||||
import Badge from "@material-symbols/svg-400/outlined/badge.svg";
|
||||
import Remove from "@material-symbols/svg-400/outlined/close.svg";
|
||||
import OpenProfile from "@material-symbols/svg-400/outlined/open_in_new.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Image } from "react-native";
|
||||
import { Button, IconButton, Link, P, Skeleton, tooltip } from "~/primitives";
|
||||
import { type QueryIdentifier, useFetch, useMutation } from "~/query";
|
||||
import { Preference, SettingsContainer } from "./base";
|
||||
import { OidcLogin } from "../login/oidc";
|
||||
import { User } from "~/models";
|
||||
|
||||
export const OidcSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useFetch(OidcLogin.query());
|
||||
const { data: user } = useFetch(OidcSettings.query());
|
||||
const { mutateAsync: unlinkAccount } = useMutation({
|
||||
method: "DELETE",
|
||||
compute: (provider: string) => ({
|
||||
path: ["auth", "oidc", "login", provider],
|
||||
}),
|
||||
invalidate: ["auth", "users", "me"],
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.oidc.label")}>
|
||||
{data && user
|
||||
? Object.entries(data.oidc).map(([id, x]) => {
|
||||
const acc = user.oidc[id];
|
||||
return (
|
||||
<Preference
|
||||
key={id}
|
||||
icon={Badge}
|
||||
label={x.name}
|
||||
description={
|
||||
acc
|
||||
? t("settings.oidc.connected", { username: acc.username })
|
||||
: t("settings.oidc.not-connected")
|
||||
}
|
||||
customIcon={
|
||||
x.logo != null && (
|
||||
<Image
|
||||
source={{ uri: x.logo }}
|
||||
className="mr-4 h-6 w-6"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{acc ? (
|
||||
<>
|
||||
{acc.profileUrl && (
|
||||
<IconButton
|
||||
icon={OpenProfile}
|
||||
as={Link}
|
||||
href={acc.profileUrl}
|
||||
{...tooltip(
|
||||
t("settings.oidc.open-profile", { provider: x.name }),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={Remove}
|
||||
onPress={() => unlinkAccount(id)}
|
||||
{...tooltip(
|
||||
t("settings.oidc.delete", { provider: x.name }),
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
text={t("settings.oidc.link")}
|
||||
as={Link}
|
||||
href={x.link}
|
||||
replace
|
||||
/>
|
||||
)}
|
||||
</Preference>
|
||||
);
|
||||
})
|
||||
: [...Array(3)].map((_, i) => (
|
||||
<Preference
|
||||
key={i}
|
||||
customIcon={<Skeleton className="h-6 w-6" />}
|
||||
icon={null!}
|
||||
label={<Skeleton className="w-24" />}
|
||||
description={<Skeleton className="h-4 w-28" />}
|
||||
/>
|
||||
))}
|
||||
</SettingsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
OidcSettings.query = (): QueryIdentifier<User> => ({
|
||||
path: ["auth", "users", "me"],
|
||||
parser: User,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user