mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 06:52:24 +00:00
show already setupped methods
This commit is contained in:
@@ -2,6 +2,8 @@ import {
|
|||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getSession,
|
getSession,
|
||||||
|
getUserByID,
|
||||||
|
listAuthenticationMethodTypes,
|
||||||
server,
|
server,
|
||||||
} from "#/lib/zitadel";
|
} from "#/lib/zitadel";
|
||||||
import Alert from "#/ui/Alert";
|
import Alert from "#/ui/Alert";
|
||||||
@@ -34,8 +36,15 @@ export default async function Page({
|
|||||||
organization
|
organization
|
||||||
);
|
);
|
||||||
return getSession(server, recent.id, recent.token).then((response) => {
|
return getSession(server, recent.id, recent.token).then((response) => {
|
||||||
if (response?.session) {
|
if (response?.session && response.session.factors?.user?.id) {
|
||||||
return response.session;
|
return listAuthenticationMethodTypes(
|
||||||
|
response.session.factors.user.id
|
||||||
|
).then((methods) => {
|
||||||
|
return {
|
||||||
|
factors: response.session?.factors,
|
||||||
|
authMethods: methods.authMethodTypes ?? [],
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -43,8 +52,15 @@ export default async function Page({
|
|||||||
async function loadSessionById(sessionId: string, organization?: string) {
|
async function loadSessionById(sessionId: string, organization?: string) {
|
||||||
const recent = await getSessionCookieById(sessionId, organization);
|
const recent = await getSessionCookieById(sessionId, organization);
|
||||||
return getSession(server, recent.id, recent.token).then((response) => {
|
return getSession(server, recent.id, recent.token).then((response) => {
|
||||||
if (response?.session) {
|
if (response?.session && response.session.factors?.user?.id) {
|
||||||
return response.session;
|
return listAuthenticationMethodTypes(
|
||||||
|
response.session.factors.user.id
|
||||||
|
).then((methods) => {
|
||||||
|
return {
|
||||||
|
factors: response.session?.factors,
|
||||||
|
authMethods: methods.authMethodTypes ?? [],
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -67,19 +83,18 @@ export default async function Page({
|
|||||||
></UserAvatar>
|
></UserAvatar>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!sessionFactors && <div className="py-4"></div>}
|
|
||||||
|
|
||||||
{!(loginName || sessionId) && (
|
{!(loginName || sessionId) && (
|
||||||
<Alert>Provide your active session as loginName param</Alert>
|
<Alert>Provide your active session as loginName param</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loginSettings ? (
|
{loginSettings && sessionFactors ? (
|
||||||
<ChooseSecondFactorToSetup
|
<ChooseSecondFactorToSetup
|
||||||
loginName={loginName}
|
loginName={loginName}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
authRequestId={authRequestId}
|
authRequestId={authRequestId}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
loginSettings={loginSettings}
|
loginSettings={loginSettings}
|
||||||
|
userMethods={sessionFactors.authMethods ?? []}
|
||||||
></ChooseSecondFactorToSetup>
|
></ChooseSecondFactorToSetup>
|
||||||
) : (
|
) : (
|
||||||
<Alert>No second factors available to setup.</Alert>
|
<Alert>No second factors available to setup.</Alert>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getBrandingSettings, getLoginSettings, server } from "#/lib/zitadel";
|
import { getBrandingSettings, getLoginSettings, server } from "#/lib/zitadel";
|
||||||
import DynamicTheme from "#/ui/DynamicTheme";
|
import DynamicTheme from "#/ui/DynamicTheme";
|
||||||
import LoginOTP from "#/ui/LoginOTP";
|
import LoginOTP from "#/ui/LoginOTP";
|
||||||
|
import LoginPasskey from "#/ui/LoginPasskey";
|
||||||
import VerifyU2F from "#/ui/VerifyU2F";
|
import VerifyU2F from "#/ui/VerifyU2F";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page({
|
||||||
@@ -22,12 +23,13 @@ export default async function Page({
|
|||||||
|
|
||||||
<p className="ztdl-p">Verify your account with your device.</p>
|
<p className="ztdl-p">Verify your account with your device.</p>
|
||||||
|
|
||||||
<VerifyU2F
|
<LoginPasskey
|
||||||
loginName={loginName}
|
loginName={loginName}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
authRequestId={authRequestId}
|
authRequestId={authRequestId}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
></VerifyU2F>
|
altPassword={false}
|
||||||
|
></LoginPasskey>
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ let colors = {
|
|||||||
text: { light: { contrast: {} }, dark: { contrast: {} } },
|
text: { light: { contrast: {} }, dark: { contrast: {} } },
|
||||||
link: { light: { contrast: {} }, dark: { contrast: {} } },
|
link: { light: { contrast: {} }, dark: { contrast: {} } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const shades = [
|
const shades = [
|
||||||
"50",
|
"50",
|
||||||
"100",
|
"100",
|
||||||
@@ -49,7 +50,51 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors,
|
colors: {
|
||||||
|
...colors,
|
||||||
|
state: {
|
||||||
|
success: {
|
||||||
|
light: {
|
||||||
|
background: "#cbf4c9",
|
||||||
|
color: "#0e6245",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: "#68cf8340",
|
||||||
|
color: "#cbf4c9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
light: {
|
||||||
|
background: "#ffc1c1",
|
||||||
|
color: "#620e0e",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: "#af455359",
|
||||||
|
color: "#ffc1c1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
neutral: {
|
||||||
|
light: {
|
||||||
|
background: "#e4e7e4",
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: "#1a253c",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
alert: {
|
||||||
|
light: {
|
||||||
|
background: "#fbbf24",
|
||||||
|
color: "#92400e",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: "#92400e50",
|
||||||
|
color: "#fbbf24",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
animation: {
|
animation: {
|
||||||
shake: "shake .8s cubic-bezier(.36,.07,.19,.97) both;",
|
shake: "shake .8s cubic-bezier(.36,.07,.19,.97) both;",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { LoginSettings } from "@zitadel/server";
|
import { AuthenticationMethodType, LoginSettings } from "@zitadel/server";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { BadgeState, StateBadge } from "./StateBadge";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
loginName?: string;
|
loginName?: string;
|
||||||
@@ -9,6 +11,7 @@ type Props = {
|
|||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
loginSettings: LoginSettings;
|
loginSettings: LoginSettings;
|
||||||
|
userMethods: AuthenticationMethodType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ChooseSecondFactorToSetup({
|
export default function ChooseSecondFactorToSetup({
|
||||||
@@ -17,140 +20,181 @@ export default function ChooseSecondFactorToSetup({
|
|||||||
authRequestId,
|
authRequestId,
|
||||||
organization,
|
organization,
|
||||||
loginSettings,
|
loginSettings,
|
||||||
|
userMethods,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const cardClasses = (alreadyAdded: boolean) =>
|
||||||
|
clsx(
|
||||||
|
"bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 border border-divider-light dark:border-divider-dark transition-all ",
|
||||||
|
alreadyAdded ? "opacity-50" : "hover:shadow-lg hover:dark:bg-white/10"
|
||||||
|
);
|
||||||
|
|
||||||
|
const TOTP = (alreadyAdded: boolean) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={userMethods.includes(4) ? "" : "/otp/time-based/set"}
|
||||||
|
className={cardClasses(alreadyAdded)}
|
||||||
|
>
|
||||||
|
<div className="font-medium flex items-center">
|
||||||
|
<svg
|
||||||
|
className="h-9 w-9 transform -translate-x-[2px] mr-4"
|
||||||
|
version="1.1"
|
||||||
|
baseProfile="basic"
|
||||||
|
id="Layer_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#1A73E8"
|
||||||
|
d="M440,255.99997v0.00006C440,273.12085,426.12085,287,409.00003,287H302l-46-93.01001l49.6507-85.9951
|
||||||
|
c8.56021-14.82629,27.51834-19.9065,42.34518-11.34724l0.00586,0.0034c14.82776,8.55979,19.90875,27.51928,11.34857,42.34682
|
||||||
|
L309.70001,225h99.30002C426.12085,225,440,238.87917,440,255.99997z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M348.00174,415.34897l-0.00586,0.00339c-14.82684,8.55927-33.78497,3.47903-42.34518-11.34723L256,318.01001
|
||||||
|
l-49.65065,85.99509c-8.5602,14.82629-27.51834,19.90652-42.34517,11.34729l-0.00591-0.00342
|
||||||
|
c-14.82777-8.55978-19.90875-27.51929-11.34859-42.34683L202.29999,287L256,285l53.70001,2l49.6503,86.00214
|
||||||
|
C367.91049,387.82968,362.8295,406.78918,348.00174,415.34897z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC04"
|
||||||
|
d="M256,193.98999L242,232l-39.70001-7l-49.6503-86.00212
|
||||||
|
c-8.56017-14.82755-3.47919-33.78705,11.34859-42.34684l0.00591-0.00341c14.82683-8.55925,33.78497-3.47903,42.34517,11.34726
|
||||||
|
L256,193.98999z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M248,225l-36,62H102.99997C85.87916,287,72,273.12085,72,256.00003v-0.00006
|
||||||
|
C72,238.87917,85.87916,225,102.99997,225H248z"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
fill="#185DB7"
|
||||||
|
points="309.70001,287 202.29999,287 256,193.98999 "
|
||||||
|
/>
|
||||||
|
</svg>{" "}
|
||||||
|
<span>Authenticator App</span>
|
||||||
|
{alreadyAdded && (
|
||||||
|
<>
|
||||||
|
<span className="flex-1"></span>
|
||||||
|
<Setup />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const U2F = (alreadyAdded: boolean) => {
|
||||||
|
return (
|
||||||
|
<Link href="/u2f/set" className={cardClasses(alreadyAdded)}>
|
||||||
|
<div className="font-medium flex items-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-8 h-8 mr-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Universal Second Factor</span>
|
||||||
|
{alreadyAdded && (
|
||||||
|
<>
|
||||||
|
<span className="flex-1"></span>
|
||||||
|
<Setup />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMAIL = (alreadyAdded: boolean) => {
|
||||||
|
return (
|
||||||
|
<Link href="/otp/email/set" className={cardClasses(alreadyAdded)}>
|
||||||
|
<div className="font-medium flex items-center">
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8 mr-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span>Code via Email</span>
|
||||||
|
{alreadyAdded && (
|
||||||
|
<>
|
||||||
|
<span className="flex-1"></span>
|
||||||
|
<Setup />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SMS = (alreadyAdded: boolean) => {
|
||||||
|
return (
|
||||||
|
<Link href="/otp/sms/set" className={cardClasses(alreadyAdded)}>
|
||||||
|
<div className="font-medium flex items-center">
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8 mr-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Code via SMS</span>
|
||||||
|
{alreadyAdded && (
|
||||||
|
<>
|
||||||
|
<span className="flex-1"></span>
|
||||||
|
<Setup />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
||||||
{loginSettings.secondFactors.map((factor, i) => {
|
{loginSettings.secondFactors.map((factor, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={"method-" + i}>
|
<div key={"method-" + i}>
|
||||||
{factor === 1 && (
|
{factor === 1 && TOTP(userMethods.includes(4))}
|
||||||
<Link
|
{factor === 2 && U2F(userMethods.includes(5))}
|
||||||
href="/otp/time-based/set"
|
{factor === 3 && EMAIL(userMethods.includes(7))}
|
||||||
className="bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 hover:shadow-lg hover:dark:bg-white/10 border border-divider-light dark:border-divider-dark transition-all "
|
{factor === 4 && SMS(userMethods.includes(6))}
|
||||||
>
|
|
||||||
<div className="font-medium flex items-center">
|
|
||||||
<svg
|
|
||||||
className="h-9 w-9 transform -translate-x-[2px] mr-4"
|
|
||||||
version="1.1"
|
|
||||||
baseProfile="basic"
|
|
||||||
id="Layer_1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
||||||
x="0px"
|
|
||||||
y="0px"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
xmlSpace="preserve"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="#1A73E8"
|
|
||||||
d="M440,255.99997v0.00006C440,273.12085,426.12085,287,409.00003,287H302l-46-93.01001l49.6507-85.9951
|
|
||||||
c8.56021-14.82629,27.51834-19.9065,42.34518-11.34724l0.00586,0.0034c14.82776,8.55979,19.90875,27.51928,11.34857,42.34682
|
|
||||||
L309.70001,225h99.30002C426.12085,225,440,238.87917,440,255.99997z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#EA4335"
|
|
||||||
d="M348.00174,415.34897l-0.00586,0.00339c-14.82684,8.55927-33.78497,3.47903-42.34518-11.34723L256,318.01001
|
|
||||||
l-49.65065,85.99509c-8.5602,14.82629-27.51834,19.90652-42.34517,11.34729l-0.00591-0.00342
|
|
||||||
c-14.82777-8.55978-19.90875-27.51929-11.34859-42.34683L202.29999,287L256,285l53.70001,2l49.6503,86.00214
|
|
||||||
C367.91049,387.82968,362.8295,406.78918,348.00174,415.34897z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#FBBC04"
|
|
||||||
d="M256,193.98999L242,232l-39.70001-7l-49.6503-86.00212
|
|
||||||
c-8.56017-14.82755-3.47919-33.78705,11.34859-42.34684l0.00591-0.00341c14.82683-8.55925,33.78497-3.47903,42.34517,11.34726
|
|
||||||
L256,193.98999z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#34A853"
|
|
||||||
d="M248,225l-36,62H102.99997C85.87916,287,72,273.12085,72,256.00003v-0.00006
|
|
||||||
C72,238.87917,85.87916,225,102.99997,225H248z"
|
|
||||||
/>
|
|
||||||
<polygon
|
|
||||||
fill="#185DB7"
|
|
||||||
points="309.70001,287 202.29999,287 256,193.98999 "
|
|
||||||
/>
|
|
||||||
</svg>{" "}
|
|
||||||
<span>Authenticator App</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{factor === 2 && (
|
|
||||||
<Link
|
|
||||||
href="/u2f/set"
|
|
||||||
className="bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 hover:shadow-lg hover:dark:bg-white/10 border border-divider-light dark:border-divider-dark transition-all "
|
|
||||||
>
|
|
||||||
<div className="font-medium flex items-center">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
className="w-8 h-8 mr-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Universal Second Factor</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{factor === 3 && (
|
|
||||||
<Link
|
|
||||||
href="/otp/email/set"
|
|
||||||
className="bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 hover:shadow-lg hover:dark:bg-white/10 border border-divider-light dark:border-divider-dark transition-all "
|
|
||||||
>
|
|
||||||
<div className="font-medium flex items-center">
|
|
||||||
<svg
|
|
||||||
className="w-8 h-8 mr-4"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span>Code via Email</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{factor === 4 && (
|
|
||||||
<Link
|
|
||||||
href="/otp/sms/set"
|
|
||||||
className="bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 hover:shadow-lg hover:dark:bg-white/10 border border-divider-light dark:border-divider-dark transition-all "
|
|
||||||
>
|
|
||||||
<div className="font-medium flex items-center">
|
|
||||||
<svg
|
|
||||||
className="w-8 h-8 mr-4"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Code via SMS</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Setup() {
|
||||||
|
return <StateBadge state={BadgeState.Success}>Setup</StateBadge>;
|
||||||
|
}
|
||||||
|
|||||||
35
apps/login/ui/StateBadge.tsx
Normal file
35
apps/login/ui/StateBadge.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
export enum BadgeState {
|
||||||
|
Info = "info",
|
||||||
|
Error = "error",
|
||||||
|
Success = "success",
|
||||||
|
Alert = "alert",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StateBadgeProps = {
|
||||||
|
state: BadgeState;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBadgeClasses = (state: BadgeState) =>
|
||||||
|
clsx({
|
||||||
|
"w-fit border-box h-18.5px flex flex-row items-center whitespace-nowrap tracking-wider leading-4 items-center justify-center px-2 py-2px text-12px rounded-full shadow-sm":
|
||||||
|
true,
|
||||||
|
"bg-state-success-light-background text-state-success-light-color dark:bg-state-success-dark-background dark:text-state-success-dark-color ":
|
||||||
|
state === BadgeState.Success,
|
||||||
|
"bg-state-neutral-light-background text-state-neutral-light-color dark:bg-state-neutral-dark-background dark:text-state-neutral-dark-color":
|
||||||
|
state === BadgeState.Info,
|
||||||
|
"bg-state-error-light-background text-state-error-light-color dark:bg-state-error-dark-background dark:text-state-error-dark-color":
|
||||||
|
state === BadgeState.Error,
|
||||||
|
"bg-state-alert-light-background text-state-alert-light-color dark:bg-state-alert-dark-background dark:text-state-alert-dark-color":
|
||||||
|
state === BadgeState.Alert,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function StateBadge({
|
||||||
|
state = BadgeState.Success,
|
||||||
|
children,
|
||||||
|
}: StateBadgeProps) {
|
||||||
|
return <span className={`${getBadgeClasses(state)}`}>{children}</span>;
|
||||||
|
}
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64";
|
|
||||||
import { Button, ButtonVariants } from "./Button";
|
|
||||||
import Alert from "./Alert";
|
|
||||||
import { Spinner } from "./Spinner";
|
|
||||||
import { Checks } from "@zitadel/server";
|
|
||||||
|
|
||||||
// either loginName or sessionId must be provided
|
|
||||||
type Props = {
|
|
||||||
loginName?: string;
|
|
||||||
sessionId?: string;
|
|
||||||
authRequestId?: string;
|
|
||||||
organization?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function VerifyU2F({
|
|
||||||
loginName,
|
|
||||||
sessionId,
|
|
||||||
authRequestId,
|
|
||||||
organization,
|
|
||||||
}: Props) {
|
|
||||||
const [error, setError] = useState<string>("");
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const initialized = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!initialized.current) {
|
|
||||||
initialized.current = true;
|
|
||||||
setLoading(true);
|
|
||||||
updateSessionForChallenge()
|
|
||||||
.then((response) => {
|
|
||||||
const pK =
|
|
||||||
response.challenges.webAuthN.publicKeyCredentialRequestOptions
|
|
||||||
.publicKey;
|
|
||||||
if (pK) {
|
|
||||||
submitLoginAndContinue(pK)
|
|
||||||
.then(() => {
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setError(error);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setError("Could not request passkey challenge");
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setError(error);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function updateSessionForChallenge() {
|
|
||||||
setLoading(true);
|
|
||||||
const res = await fetch("/api/session", {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
loginName,
|
|
||||||
sessionId,
|
|
||||||
organization,
|
|
||||||
challenges: {
|
|
||||||
webAuthN: {
|
|
||||||
domain: "",
|
|
||||||
userVerificationRequirement: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
authRequestId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = await res.json();
|
|
||||||
throw error.details.details;
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitLogin(data: any) {
|
|
||||||
setLoading(true);
|
|
||||||
const res = await fetch("/api/session", {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
loginName,
|
|
||||||
sessionId,
|
|
||||||
organization,
|
|
||||||
checks: {
|
|
||||||
webAuthN: { credentialAssertionData: data },
|
|
||||||
} as Checks,
|
|
||||||
authRequestId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await res.json();
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
if (!res.ok) {
|
|
||||||
setError(response.details);
|
|
||||||
return Promise.reject(response.details);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitLoginAndContinue(
|
|
||||||
publicKey: any
|
|
||||||
): Promise<boolean | void> {
|
|
||||||
publicKey.challenge = coerceToArrayBuffer(
|
|
||||||
publicKey.challenge,
|
|
||||||
"publicKey.challenge"
|
|
||||||
);
|
|
||||||
publicKey.allowCredentials.map((listItem: any) => {
|
|
||||||
listItem.id = coerceToArrayBuffer(
|
|
||||||
listItem.id,
|
|
||||||
"publicKey.allowCredentials.id"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
navigator.credentials
|
|
||||||
.get({
|
|
||||||
publicKey,
|
|
||||||
})
|
|
||||||
.then((assertedCredential: any) => {
|
|
||||||
if (assertedCredential) {
|
|
||||||
const authData = new Uint8Array(
|
|
||||||
assertedCredential.response.authenticatorData
|
|
||||||
);
|
|
||||||
const clientDataJSON = new Uint8Array(
|
|
||||||
assertedCredential.response.clientDataJSON
|
|
||||||
);
|
|
||||||
const rawId = new Uint8Array(assertedCredential.rawId);
|
|
||||||
const sig = new Uint8Array(assertedCredential.response.signature);
|
|
||||||
const userHandle = new Uint8Array(
|
|
||||||
assertedCredential.response.userHandle
|
|
||||||
);
|
|
||||||
const data = {
|
|
||||||
id: assertedCredential.id,
|
|
||||||
rawId: coerceToBase64Url(rawId, "rawId"),
|
|
||||||
type: assertedCredential.type,
|
|
||||||
response: {
|
|
||||||
authenticatorData: coerceToBase64Url(authData, "authData"),
|
|
||||||
clientDataJSON: coerceToBase64Url(
|
|
||||||
clientDataJSON,
|
|
||||||
"clientDataJSON"
|
|
||||||
),
|
|
||||||
signature: coerceToBase64Url(sig, "sig"),
|
|
||||||
userHandle: coerceToBase64Url(userHandle, "userHandle"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return submitLogin(data).then((resp) => {
|
|
||||||
if (authRequestId && resp && resp.sessionId) {
|
|
||||||
return router.push(
|
|
||||||
`/login?` +
|
|
||||||
new URLSearchParams({
|
|
||||||
sessionId: resp.sessionId,
|
|
||||||
authRequest: authRequestId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return router.push(
|
|
||||||
`/signedin?` +
|
|
||||||
new URLSearchParams(
|
|
||||||
authRequestId
|
|
||||||
? {
|
|
||||||
loginName: resp.factors.user.loginName,
|
|
||||||
authRequestId,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
loginName: resp.factors.user.loginName,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
setError("An error on retrieving passkey");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
setLoading(false);
|
|
||||||
// setError(error);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
{error && (
|
|
||||||
<div className="py-4">
|
|
||||||
<Alert>{error}</Alert>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="mt-8 flex w-full flex-row items-center">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={ButtonVariants.Secondary}
|
|
||||||
onClick={() => router.back()}
|
|
||||||
>
|
|
||||||
back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<span className="flex-grow"></span>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="self-end"
|
|
||||||
variant={ButtonVariants.Primary}
|
|
||||||
disabled={loading}
|
|
||||||
onClick={() => updateSessionForChallenge()}
|
|
||||||
>
|
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
|
||||||
continue
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user