diff --git a/apps/login/app/(login)/passkey/login/page.tsx b/apps/login/app/(login)/passkey/login/page.tsx index 1808a131f11..8ce7c51d3d6 100644 --- a/apps/login/app/(login)/passkey/login/page.tsx +++ b/apps/login/app/(login)/passkey/login/page.tsx @@ -1,30 +1,30 @@ +import { getSession, server } from "#/lib/zitadel"; import Alert from "#/ui/Alert"; import LoginPasskey from "#/ui/LoginPasskey"; -import { ChallengeKind } from "@zitadel/server"; +import UserAvatar from "#/ui/UserAvatar"; +import { getMostRecentCookieWithLoginname } from "#/utils/cookies"; +// import LoginPasskey from "#/ui/LoginPasskey"; +// import { +// SessionCookie, +// getMostRecentSessionCookie, +// getSessionCookieByLoginName, +// } from "#/utils/cookies"; +// import { setSessionAndUpdateCookie } from "#/utils/session"; +// import { ChallengeKind } from "@zitadel/server"; -async function updateSessionAndCookie(loginName: string) { - const res = await fetch( - `${process.env.VERCEL_URL ?? "http://localhost:3000"}/session`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - challenges: [ChallengeKind.CHALLENGE_KIND_PASSKEY], - }), - next: { revalidate: 0 }, - } - ); - - const response = await res.json(); - - if (!res.ok) { - return Promise.reject(response.details); - } - return response; -} +// async function updateSessionAndCookie(loginName: string) { +// return getSessionCookieByLoginName(loginName).then((recent) => { +// console.log(recent.token); +// return setSessionAndUpdateCookie( +// recent.id, +// recent.token, +// recent.loginName, +// undefined, +// "localhost", +// [ChallengeKind.CHALLENGE_KIND_PASSKEY] +// ); +// }); +// } const title = "Authenticate with a passkey"; const description = @@ -36,20 +36,23 @@ export default async function Page({ searchParams: Record; }) { const { loginName } = searchParams; - if (loginName) { - console.log(loginName); - const session = await updateSessionAndCookie(loginName); - console.log("sess", session); - const challenge = session?.challenges?.passkey; + const sessionFactors = await loadSession(loginName); - console.log(challenge); + async function loadSession(loginName?: string) { + const recent = await getMostRecentCookieWithLoginname(loginName); + return getSession(server, recent.id, recent.token).then((response) => { + if (response?.session) { + return response.session; + } + }); + } - return ( -
-

{title}

+ return ( +
+

{title}

- {/* {sessionFactors && ( + {sessionFactors && ( {description}

- {!sessionFactors && ( -
- - Could not get the context of the user. Make sure to enter the - username first or provide a loginName as searchParam. - -
- )} */} - - {challenge && } -
- ); - } else { - return ( -
-

{title}

- - {/* {sessionFactors && ( - - )} -

{description}

- - {!sessionFactors && ( -
- -
- )} */} + {!sessionFactors &&
} + {!loginName && ( Provide your active session as loginName param -
- ); - } + )} + + {loginName && ( + + )} +
+ ); } diff --git a/apps/login/app/loginnames/route.ts b/apps/login/app/api/loginname/route.ts similarity index 100% rename from apps/login/app/loginnames/route.ts rename to apps/login/app/api/loginname/route.ts diff --git a/apps/login/app/passkeys/route.ts b/apps/login/app/api/passkeys/route.ts similarity index 100% rename from apps/login/app/passkeys/route.ts rename to apps/login/app/api/passkeys/route.ts diff --git a/apps/login/app/passkeys/verify/route.ts b/apps/login/app/api/passkeys/verify/route.ts similarity index 100% rename from apps/login/app/passkeys/verify/route.ts rename to apps/login/app/api/passkeys/verify/route.ts diff --git a/apps/login/app/registeruser/route.ts b/apps/login/app/api/registeruser/route.ts similarity index 100% rename from apps/login/app/registeruser/route.ts rename to apps/login/app/api/registeruser/route.ts diff --git a/apps/login/app/resendverifyemail/route.ts b/apps/login/app/api/resendverifyemail/route.ts similarity index 100% rename from apps/login/app/resendverifyemail/route.ts rename to apps/login/app/api/resendverifyemail/route.ts diff --git a/apps/login/app/session/route.ts b/apps/login/app/api/session/route.ts similarity index 93% rename from apps/login/app/session/route.ts rename to apps/login/app/api/session/route.ts index 7a754329c91..977ce8b45cb 100644 --- a/apps/login/app/session/route.ts +++ b/apps/login/app/api/session/route.ts @@ -1,10 +1,11 @@ -import { server, deleteSession } from "#/lib/zitadel"; +import { server, deleteSession, getSession, setSession } from "#/lib/zitadel"; import { SessionCookie, getMostRecentSessionCookie, getSessionCookieById, getSessionCookieByLoginName, removeSessionFromCookie, + updateSessionCookie, } from "#/utils/cookies"; import { createSessionAndUpdateCookie, @@ -59,9 +60,11 @@ export async function PUT(request: NextRequest) { domain, challenges ).then((session) => { + console.log(session.challenges); return NextResponse.json({ sessionId: session.id, factors: session.factors, + challenges: session.challenges, }); }); }) diff --git a/apps/login/app/verifyemail/route.ts b/apps/login/app/api/verifyemail/route.ts similarity index 100% rename from apps/login/app/verifyemail/route.ts rename to apps/login/app/api/verifyemail/route.ts diff --git a/apps/login/package.json b/apps/login/package.json index 4a286788e22..17ebe8dc072 100644 --- a/apps/login/package.json +++ b/apps/login/package.json @@ -37,6 +37,7 @@ "react-dom": "18.2.0", "react-hook-form": "7.39.5", "sass": "^1.62.0", + "swr": "^2.2.0", "tinycolor2": "1.4.2" }, "devDependencies": { diff --git a/apps/login/ui/LoginPasskey.tsx b/apps/login/ui/LoginPasskey.tsx index c4f78177c7d..82a1332d01b 100644 --- a/apps/login/ui/LoginPasskey.tsx +++ b/apps/login/ui/LoginPasskey.tsx @@ -1,24 +1,48 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import { Challenges_Passkey } from "@zitadel/server"; +import { ChallengeKind, Challenges_Passkey } from "@zitadel/server"; import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64"; import { Button, ButtonVariants } from "./Button"; import Alert from "./Alert"; import { Spinner } from "./Spinner"; type Props = { + loginName: string; challenge: Challenges_Passkey; }; -export default function LoginPasskey({ challenge }: Props) { +export default function LoginPasskey({ loginName, challenge }: Props) { const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); const router = useRouter(); + useEffect(() => { + updateSessionForChallenge(); + }, []); + + async function updateSessionForChallenge() { + setLoading(true); + const res = await fetch("/api/session", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + loginName, + challenges: [1], // request passkey challenge + }), + }); + + setLoading(false); + if (!res.ok) { + throw new Error("Failed to load authentication methods"); + } + return res.json(); + } + async function submitLogin( passkeyId: string, passkeyName: string, @@ -26,7 +50,7 @@ export default function LoginPasskey({ challenge }: Props) { sessionId: string ) { setLoading(true); - const res = await fetch("/passkeys/verify", { + const res = await fetch("/api/passkeys/verify", { method: "POST", headers: { "Content-Type": "application/json", @@ -99,13 +123,12 @@ export default function LoginPasskey({ challenge }: Props) { } return ( -
+
{error && (
{error}
)} -
- +
); } diff --git a/apps/login/ui/PasswordForm.tsx b/apps/login/ui/PasswordForm.tsx index 67a26f05ece..a31f908f708 100644 --- a/apps/login/ui/PasswordForm.tsx +++ b/apps/login/ui/PasswordForm.tsx @@ -30,7 +30,7 @@ export default function PasswordForm({ loginName }: Props) { async function submitPassword(values: Inputs) { setError(""); setLoading(true); - const res = await fetch("/session", { + const res = await fetch("/api/session", { method: "PUT", headers: { "Content-Type": "application/json", diff --git a/apps/login/ui/RegisterForm.tsx b/apps/login/ui/RegisterForm.tsx index 932a7294e99..6357299a1ce 100644 --- a/apps/login/ui/RegisterForm.tsx +++ b/apps/login/ui/RegisterForm.tsx @@ -48,7 +48,7 @@ export default function RegisterForm({ async function submitRegister(values: Inputs) { setLoading(true); - const res = await fetch("/registeruser", { + const res = await fetch("/api/registeruser", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/apps/login/ui/RegisterFormWithoutPassword.tsx b/apps/login/ui/RegisterFormWithoutPassword.tsx index 952b4c82eed..a1980d0174a 100644 --- a/apps/login/ui/RegisterFormWithoutPassword.tsx +++ b/apps/login/ui/RegisterFormWithoutPassword.tsx @@ -36,7 +36,7 @@ export default function RegisterFormWithoutPassword({ legal }: Props) { async function submitAndRegister(values: Inputs) { setLoading(true); - const res = await fetch("/registeruser", { + const res = await fetch("/api/registeruser", { method: "POST", headers: { "Content-Type": "application/json", @@ -56,7 +56,7 @@ export default function RegisterFormWithoutPassword({ legal }: Props) { async function createSessionWithLoginName(loginName: string) { setLoading(true); - const res = await fetch("/session", { + const res = await fetch("/api/session", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/apps/login/ui/RegisterPasskey.tsx b/apps/login/ui/RegisterPasskey.tsx index 800a57879e9..1c0460aeb26 100644 --- a/apps/login/ui/RegisterPasskey.tsx +++ b/apps/login/ui/RegisterPasskey.tsx @@ -29,7 +29,7 @@ export default function RegisterPasskey({ sessionId, isPrompt }: Props) { async function submitRegister() { setError(""); setLoading(true); - const res = await fetch("/passkeys", { + const res = await fetch("/api/passkeys", { method: "POST", headers: { "Content-Type": "application/json", @@ -56,7 +56,7 @@ export default function RegisterPasskey({ sessionId, isPrompt }: Props) { sessionId: string ) { setLoading(true); - const res = await fetch("/passkeys/verify", { + const res = await fetch("/api/passkeys/verify", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/apps/login/ui/SessionItem.tsx b/apps/login/ui/SessionItem.tsx index d766ca0f139..68ec0257755 100644 --- a/apps/login/ui/SessionItem.tsx +++ b/apps/login/ui/SessionItem.tsx @@ -17,7 +17,7 @@ export default function SessionItem({ async function clearSession(id: string) { setLoading(true); - const res = await fetch("/session?" + new URLSearchParams({ id }), { + const res = await fetch("/api/session?" + new URLSearchParams({ id }), { method: "DELETE", headers: { "Content-Type": "application/json", diff --git a/apps/login/ui/SetPasswordForm.tsx b/apps/login/ui/SetPasswordForm.tsx index 55aec7e388a..2efe33b8e54 100644 --- a/apps/login/ui/SetPasswordForm.tsx +++ b/apps/login/ui/SetPasswordForm.tsx @@ -47,7 +47,7 @@ export default function SetPasswordForm({ async function submitRegister(values: Inputs) { setLoading(true); - const res = await fetch("/registeruser", { + const res = await fetch("/api/registeruser", { method: "POST", headers: { "Content-Type": "application/json", @@ -71,7 +71,7 @@ export default function SetPasswordForm({ loginName: string, password: string ) { - const res = await fetch("/session", { + const res = await fetch("/api/session", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/apps/login/ui/UsernameForm.tsx b/apps/login/ui/UsernameForm.tsx index 7b621573dd5..c62199dbaa9 100644 --- a/apps/login/ui/UsernameForm.tsx +++ b/apps/login/ui/UsernameForm.tsx @@ -31,7 +31,7 @@ export default function UsernameForm({ loginSettings, loginName }: Props) { async function submitLoginName(values: Inputs) { setLoading(true); - const res = await fetch("/loginnames", { + const res = await fetch("/api/loginname", { method: "POST", headers: { "Content-Type": "application/json", @@ -53,25 +53,24 @@ export default function UsernameForm({ loginSettings, loginName }: Props) { console.log(response); if (response.authMethodTypes.length == 1) { const method = response.authMethodTypes[0]; - console.log(method); - // switch (method) { - // case AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSWORD: - // return router.push( - // "/password?" + - // new URLSearchParams({ loginName: values.loginName }) - // ); - // case AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY: - // break; - // // return router.push( - // // "/passkey/login?" + - // // new URLSearchParams({ loginName: values.loginName }) - // // ); - // default: - // return router.push( - // "/password?" + - // new URLSearchParams({ loginName: values.loginName }) - // ); - // } + switch (method) { + case 1: //AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSWORD: + return router.push( + "/password?" + + new URLSearchParams({ loginName: values.loginName }) + ); + case 2: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY + return router.push( + "/passkey/login?" + + new URLSearchParams({ loginName: values.loginName }) + ); + default: + return router.push( + "/password?" + + new URLSearchParams({ loginName: values.loginName }) + ); + } + } else { } }); } diff --git a/apps/login/ui/VerifyEmailForm.tsx b/apps/login/ui/VerifyEmailForm.tsx index de4ded3d9d0..b4b81dd50df 100644 --- a/apps/login/ui/VerifyEmailForm.tsx +++ b/apps/login/ui/VerifyEmailForm.tsx @@ -42,7 +42,7 @@ export default function VerifyEmailForm({ userId, code, submit }: Props) { async function submitCode(values: Inputs) { setLoading(true); - const res = await fetch("/verifyemail", { + const res = await fetch("/api/verifyemail", { method: "POST", headers: { "Content-Type": "application/json", @@ -66,7 +66,7 @@ export default function VerifyEmailForm({ userId, code, submit }: Props) { async function resendCode() { setLoading(true); - const res = await fetch("/resendverifyemail", { + const res = await fetch("/api/resendverifyemail", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/apps/login/utils/cookies.ts b/apps/login/utils/cookies.ts index 4a2fa74bd40..1ed14996f92 100644 --- a/apps/login/utils/cookies.ts +++ b/apps/login/utils/cookies.ts @@ -55,9 +55,12 @@ export async function updateSessionCookie( : [session]; const foundIndex = sessions.findIndex((session) => session.id === id); - sessions[foundIndex] = session; - - return setSessionHttpOnlyCookie(sessions); + if (foundIndex > -1) { + sessions[foundIndex] = session; + return setSessionHttpOnlyCookie(sessions); + } else { + throw "updateSessionCookie: session id now found"; + } } export async function removeSessionFromCookie( @@ -119,11 +122,9 @@ export async function getSessionCookieByLoginName( const cookiesList = cookies(); const stringifiedCookie = cookiesList.get("sessions"); - console.log("str", stringifiedCookie); if (stringifiedCookie?.value) { const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); - console.log("sessions", sessions); const found = sessions.find((s) => s.loginName === loginName); if (found) { return found; diff --git a/apps/login/utils/session.ts b/apps/login/utils/session.ts index 79442b800c2..b9182fdf3cc 100644 --- a/apps/login/utils/session.ts +++ b/apps/login/utils/session.ts @@ -86,13 +86,9 @@ export async function setSessionAndUpdateCookie( loginName: session.factors?.user?.loginName ?? "", }; - return updateSessionCookie(sessionCookie.id, newCookie) - .then(() => { - return session; - }) - .catch((error) => { - throw "could not set cookie"; - }); + return updateSessionCookie(sessionCookie.id, newCookie).then(() => { + return session; + }); } else { throw "could not get session or session does not have loginName"; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1464cb7909..89304c3a6af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,7 @@ importers: react-dom: 18.2.0 react-hook-form: 7.39.5 sass: ^1.62.0 + swr: ^2.2.0 tailwindcss: 3.2.4 tinycolor2: 1.4.2 ts-jest: ^29.1.0 @@ -84,6 +85,7 @@ importers: react-dom: 18.2.0_react@18.2.0 react-hook-form: 7.39.5_react@18.2.0 sass: 1.62.0 + swr: 2.2.0_react@18.2.0 tinycolor2: 1.4.2 devDependencies: '@bufbuild/buf': 1.15.0 @@ -6841,6 +6843,15 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /swr/2.2.0_react@18.2.0: + resolution: {integrity: sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0_react@18.2.0 + dev: false + /symbol-tree/3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: true @@ -7491,6 +7502,14 @@ packages: requires-port: 1.0.0 dev: true + /use-sync-external-store/1.2.0_react@18.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /util-deprecate/1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}