Skip to content

Commit

Permalink
chore: improve idp integration using server action
Browse files Browse the repository at this point in the history
  • Loading branch information
yordis committed Dec 27, 2024
1 parent 5f19892 commit c09e947
Show file tree
Hide file tree
Showing 6 changed files with 363 additions and 314 deletions.
9 changes: 5 additions & 4 deletions apps/login/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@
"clsx": "1.2.1",
"copy-to-clipboard": "^3.3.3",
"deepmerge": "^4.3.1",
"lucide-react": "0.469.0",
"moment": "^2.29.4",
"next": "15.0.4-canary.23",
"next-intl": "^3.25.1",
"next-themes": "^0.2.1",
"nice-grpc": "2.0.1",
"qrcode.react": "^3.1.0",
"react": "19.0.0-rc-66855b96-20241106",
"react-dom": "19.0.0-rc-66855b96-20241106",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-hook-form": "7.39.5",
"swr": "^2.2.0",
"tinycolor2": "1.4.2"
Expand All @@ -62,8 +63,8 @@
"@testing-library/react": "^16.0.1",
"@types/ms": "0.7.34",
"@types/node": "22.9.0",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@types/react": "19.0.2",
"@types/react-dom": "19.0.2",
"@types/tinycolor2": "1.4.3",
"@types/uuid": "^10.0.0",
"@vercel/git-hooks": "1.0.0",
Expand Down
18 changes: 15 additions & 3 deletions apps/login/src/components/idps/base-button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use client";

import { clsx } from "clsx";
import { Loader2Icon } from "lucide-react";
import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react";
import { useFormStatus } from "react-dom";

export type SignInWithIdentityProviderProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
Expand All @@ -15,15 +17,25 @@ export const BaseButton = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(function BaseButton(props, ref) {
const formStatus = useFormStatus();

return (
<button
{...props}
type="button"
type="submit"
ref={ref}
disabled={formStatus.pending}
className={clsx(
"transition-all cursor-pointer flex flex-row items-center bg-background-light-400 text-text-light-500 dark:bg-background-dark-500 dark:text-text-dark-500 border border-divider-light hover:border-black dark:border-divider-dark hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500 outline-none rounded-md px-4 text-sm",
"flex-1 transition-all cursor-pointer flex flex-row items-center bg-background-light-400 text-text-light-500 dark:bg-background-dark-500 dark:text-text-dark-500 border border-divider-light hover:border-black dark:border-divider-dark hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500 outline-none rounded-md px-4 text-sm",
props.className,
)}
/>
>
<div className="flex-1 justify-between flex items-center gap-4">
<div className="flex-1 flex flex-row items-center">
{props.children}
</div>
{formStatus.pending && <Loader2Icon className="w-4 h-4 animate-spin" />}
</div>
</button>
);
});
60 changes: 34 additions & 26 deletions apps/login/src/components/idps/sign-in-with-github.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@ import { useTranslations } from "next-intl";
import { forwardRef } from "react";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";

function GitHubLogo() {
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
className="h-8 w-8 hidden dark:block"
>
<path
fill="#fafafa"
fillRule="evenodd"
d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0z"
clipRule="evenodd"
></path>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
className="h-8 w-8 block dark:hidden"
>
<path
fill="#1B1F23"
fillRule="evenodd"
d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0z"
clipRule="evenodd"
></path>
</svg>
</>
);
}

export const SignInWithGithub = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
Expand All @@ -14,32 +47,7 @@ export const SignInWithGithub = forwardRef<
return (
<BaseButton {...restProps} ref={ref}>
<div className="mx-2 my-2 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
className="h-8 w-8 hidden dark:block"
>
<path
fill="#fafafa"
fillRule="evenodd"
d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0z"
clipRule="evenodd"
></path>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
className="h-8 w-8 block dark:hidden"
>
<path
fill="#1B1F23"
fillRule="evenodd"
d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0z"
clipRule="evenodd"
></path>
</svg>
<GitHubLogo />
</div>
{children ? (
children
Expand Down
59 changes: 17 additions & 42 deletions apps/login/src/components/sign-in-with-idp.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
"use client";

import { idpTypeToSlug } from "@/lib/idp";
import { startIDPFlow } from "@/lib/server/idp";
import {
IdentityProvider,
IdentityProviderType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useRouter } from "next/navigation";
import { ReactNode, useCallback, useState } from "react";
import { ReactNode, useActionState } from "react";
import { Alert } from "./alert";
import { SignInWithIdentityProviderProps } from "./idps/base-button";
import { SignInWithApple } from "./idps/sign-in-with-apple";
Expand All @@ -16,6 +14,7 @@ import { SignInWithGeneric } from "./idps/sign-in-with-generic";
import { SignInWithGithub } from "./idps/sign-in-with-github";
import { SignInWithGitlab } from "./idps/sign-in-with-gitlab";
import { SignInWithGoogle } from "./idps/sign-in-with-google";
import { redictToIdpAuction } from "./sign-in-with-idp/server/action";

export interface SignInWithIDPProps {
children?: ReactNode;
Expand All @@ -31,45 +30,10 @@ export function SignInWithIdp({
organization,
linkOnly,
}: Readonly<SignInWithIDPProps>) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();

const startFlow = useCallback(
async (idpId: string, provider: string) => {
setLoading(true);
const params = new URLSearchParams();
if (linkOnly) params.set("link", "true");
if (authRequestId) params.set("authRequestId", authRequestId);
if (organization) params.set("organization", organization);

try {
const response = await startIDPFlow({
idpId,
successUrl: `/idp/${provider}/success?` + params.toString(),
failureUrl: `/idp/${provider}/failure?` + params.toString(),
});

if (response && "error" in response && response?.error) {
setError(response.error);
return;
}

if (response && "redirect" in response && response?.redirect) {
return router.push(response.redirect);
}
} catch {
setError("Could not start IDP flow");
} finally {
setLoading(false);
}
},
[authRequestId, organization, linkOnly, router],
);
const [state, action, _isPending] = useActionState(redictToIdpAuction, {});

const renderIDPButton = (idp: IdentityProvider) => {
const { id, name, type } = idp;
const onClick = () => startFlow(id, idpTypeToSlug(type));

const components: Partial<
Record<
Expand All @@ -92,16 +56,27 @@ export function SignInWithIdp({

const Component = components[type];
return Component ? (
<Component key={id} name={name} onClick={onClick} />
<form action={action} className="flex">
<input type="hidden" name="id" value={id} />
<input type="hidden" name="provider" value={idpTypeToSlug(type)} />
<input type="hidden" name="authRequestId" value={authRequestId} />
<input type="hidden" name="organization" value={organization} />
<input
type="hidden"
name="linkOnly"
value={linkOnly ? "true" : "false"}
/>
<Component key={id} name={name} />
</form>
) : null;
};

return (
<div className="flex flex-col w-full space-y-2 text-sm">
{identityProviders?.map(renderIDPButton)}
{error && (
{state?.error && (
<div className="py-4">
<Alert>{error}</Alert>
<Alert>{state?.error}</Alert>
</div>
)}
</div>
Expand Down
41 changes: 41 additions & 0 deletions apps/login/src/components/sign-in-with-idp/server/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use server";

import { startIDPFlow } from "@/lib/server/idp";
import { redirect } from "next/navigation";

export type RedictToIdpState = { error?: string | null } | undefined;

export async function redictToIdpAuction(
prevState: RedictToIdpState,
formData: FormData,
): Promise<RedictToIdpState> {
const params = new URLSearchParams();

const linkOnly = formData.get("linkOnly") === "true";
const authRequestId = formData.get("authRequestId") as string;
const organization = formData.get("organization") as string;
const idpId = formData.get("id") as string;
const provider = formData.get("provider") as string;

if (linkOnly) params.set("link", "true");
if (authRequestId) params.set("authRequestId", authRequestId);
if (organization) params.set("organization", organization);

try {
const response = await startIDPFlow({
idpId,
successUrl: `/idp/${provider}/success?` + params.toString(),
failureUrl: `/idp/${provider}/failure?` + params.toString(),
});

if (response && "error" in response && response?.error) {
return { error: response.error };
}

if (response && "redirect" in response && response?.redirect) {
redirect(response.redirect);
}
} catch {
return { error: "Could not start IDP flow" };
}
}
Loading

0 comments on commit c09e947

Please sign in to comment.