diff --git a/auth/shell.nix b/auth/shell.nix index cde66962..0ee7c158 100644 --- a/auth/shell.nix +++ b/auth/shell.nix @@ -3,7 +3,9 @@ pkgs.mkShell { packages = with pkgs; [ go wgo - go-migrate + (go-migrate.overrideAttrs (_: { + tags = ["postgres" "file"]; + })) sqlc go-swag # for psql in cli (+ pgformatter for sql files) diff --git a/auth/sql/migrations/000004_oidc_flow.down.sql b/auth/sql/migrations/000004_oidc.down.sql similarity index 100% rename from auth/sql/migrations/000004_oidc_flow.down.sql rename to auth/sql/migrations/000004_oidc.down.sql diff --git a/auth/sql/migrations/000004_oidc_flow.up.sql b/auth/sql/migrations/000004_oidc.up.sql similarity index 100% rename from auth/sql/migrations/000004_oidc_flow.up.sql rename to auth/sql/migrations/000004_oidc.up.sql diff --git a/front/src/app/(public)/oidc-callback.tsx b/front/src/app/(public)/oidc-callback.tsx new file mode 100644 index 00000000..c9d64d01 --- /dev/null +++ b/front/src/app/(public)/oidc-callback.tsx @@ -0,0 +1,5 @@ +import { OidcCallbackPage } from "~/ui/login"; + +export { ErrorBoundary } from "~/ui/error-boundary"; + +export default OidcCallbackPage; diff --git a/front/src/primitives/button.tsx b/front/src/primitives/button.tsx index c90c6bf2..5d5b3251 100644 --- a/front/src/primitives/button.tsx +++ b/front/src/primitives/button.tsx @@ -1,4 +1,4 @@ -import type { ComponentProps, ComponentType, Ref } from "react"; +import type { ComponentProps, ComponentType, ReactElement, Ref } from "react"; import { type Falsy, type PressableProps, View } from "react-native"; import { cn } from "~/utils"; import { Icon } from "./icons"; @@ -9,6 +9,8 @@ export const Button = ({ text, icon, ricon, + left, + right, disabled, as, ref, @@ -18,7 +20,9 @@ export const Button = ({ disabled?: boolean | null; text?: string; icon?: ComponentProps["icon"] | Falsy; + left?: ReactElement | Falsy; ricon?: ComponentProps["icon"] | Falsy; + right?: ReactElement | Falsy; ref?: Ref; className?: string; as?: ComponentType; @@ -44,11 +48,13 @@ export const Button = ({ className="mx-2 group-focus-within:fill-slate-200 group-hover:fill-slate-200" /> )} + {left} {text && (

{text}

)} + {right} {ricon && ( @@ -28,7 +28,7 @@ export const Input = ({ ref={ref} textAlignVertical="center" className={cn( - "h-full flex-1 font-sans text-base text-slate-600 outline-0 dark:text-slate-400", + "h-6 flex-1 font-sans text-base text-slate-600 outline-0 dark:text-slate-400", className, )} {...props} diff --git a/front/src/ui/login/index.ts b/front/src/ui/login/index.ts index 27a86940..76c892f0 100644 --- a/front/src/ui/login/index.ts +++ b/front/src/ui/login/index.ts @@ -1,3 +1,4 @@ export { LoginPage } from "./login"; +export { OidcCallbackPage } from "./oidc-callback"; export { RegisterPage } from "./register"; export { ServerUrlPage } from "./server-url"; diff --git a/front/src/ui/login/logic.tsx b/front/src/ui/login/logic.tsx index d73f990f..b5bb8a39 100644 --- a/front/src/ui/login/logic.tsx +++ b/front/src/ui/login/logic.tsx @@ -52,35 +52,33 @@ export const login = async ( } }; -// export const oidcLogin = async ( -// provider: string, -// code: string, -// apiUrl?: string, -// ) => { -// if (!apiUrl || apiUrl.length === 0) apiUrl = getCurrentApiUrl()!; -// try { -// const token = await queryFn( -// { -// path: ["auth", "callback", provider, `?code=${code}`], -// method: "POST", -// authenticated: false, -// apiUrl, -// }, -// TokenP, -// ); -// const user = await queryFn( -// { path: ["auth", "me"], method: "GET", apiUrl }, -// UserP, -// `Bearer ${token.access_token}`, -// ); -// const account: Account = { ...user, apiUrl: apiUrl, token, selected: true }; -// addAccount(account); -// return { ok: true, value: account }; -// } catch (e) { -// console.error("oidcLogin", e); -// return { ok: false, error: (e as KyooErrors).errors[0] }; -// } -// }; +export const oidcLogin = async ( + provider: string, + code: string, + apiUrl?: string, +) => { + apiUrl ??= defaultApiUrl; + try { + const { token } = await queryFn({ + method: "POST", + 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, + }); + const account: Account = { ...user, apiUrl, token, selected: true }; + addAccount(account); + return { ok: true, value: account }; + } catch (e) { + console.error("oidcLogin", e); + return { ok: false, error: (e as KyooError).message }; + } +}; export const logout = async () => { const accounts = readAccounts(); diff --git a/front/src/ui/login/login.tsx b/front/src/ui/login/login.tsx index 5ba11cda..eb0ad04e 100644 --- a/front/src/ui/login/login.tsx +++ b/front/src/ui/login/login.tsx @@ -1,4 +1,4 @@ -import { useRouter } from "expo-router"; +import { useLocalSearchParams, useRouter } from "expo-router"; import { useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Platform } from "react-native"; @@ -7,6 +7,7 @@ import { defaultApiUrl } from "~/providers/account-provider"; import { useQueryState } from "~/utils"; import { FormPage } from "./form"; import { login } from "./logic"; +import { OidcLogin } from "./oidc"; import { PasswordInput } from "./password-input"; import { ServerUrlPage } from "./server-url"; @@ -14,7 +15,8 @@ export const LoginPage = () => { const [apiUrl] = useQueryState("apiUrl", defaultApiUrl); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - const [error, setError] = useState(); + const params = useLocalSearchParams(); + const [error, setError] = useState(params.error as string | null); const { t } = useTranslation(); const router = useRouter(); @@ -24,38 +26,40 @@ export const LoginPage = () => { return (

{t("login.login")}

-

{t("login.username")}

- setUsername(value)} - autoCapitalize="none" - /> -

{t("login.password")}

- setPassword(value)} - /> - {error &&

{error}

} - -// ))} -// -//
-//

{t("misc.or")}

-//
-//
-//
-// ); -// }; -// -// OidcLogin.query = (): QueryIdentifier => ({ -// path: ["info"], -// parser: ServerInfoP, -// }); -// -// export const OidcCallbackPage: QueryPage<{ -// apiUrl?: string; -// provider: string; -// code: string; -// error?: string; -// }> = ({ apiUrl, provider, code, error }) => { -// const hasRun = useRef(false); -// const router = useRouter(); -// -// useEffect(() => { -// if (hasRun.current) return; -// hasRun.current = true; -// -// function onError(error: string) { -// router.replace( -// { pathname: "/login", query: { error, apiUrl } }, -// undefined, -// { -// experimental: { -// nativeBehavior: "stack-replace", -// isNestedNavigator: false, -// }, -// }, -// ); -// } -// async function run() { -// const { error: loginError } = await oidcLogin(provider, code, apiUrl); -// if (loginError) onError(loginError); -// else { -// router.replace("/", undefined, { -// experimental: { -// nativeBehavior: "stack-replace", -// isNestedNavigator: false, -// }, -// }); -// } -// } -// -// if (error) onError(error); -// else run(); -// }, [provider, code, apiUrl, router, error]); -// return

{"Loading"}

; -// }; +import type { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { Image, Platform, View } from "react-native"; +import { z } from "zod/v4"; +import { Button, HR, Link, P, Skeleton } from "~/primitives"; +import { Fetch, type QueryIdentifier } from "~/query"; + +export const OidcLogin = ({ + apiUrl, + children, +}: { + apiUrl: string; + children: ReactNode; +}) => { + const { t } = useTranslation(); + + const or = ( + <> + +
+

{t("misc.or")}

+
+
+ {children} + + ); + + return ( + ( + <> + + {Object.entries(info.oidc).map(([id, provider]) => ( +