diff --git a/apps/login/app/(login)/password/page.tsx b/apps/login/app/(login)/password/page.tsx index c0c86096e8c..c6a36ea1ca3 100644 --- a/apps/login/app/(login)/password/page.tsx +++ b/apps/login/app/(login)/password/page.tsx @@ -1,32 +1,51 @@ -"use client"; -import { Button, ButtonVariants } from "#/ui/Button"; -import { TextInput } from "#/ui/Input"; +import { getSession, server } from "#/lib/zitadel"; +import PasswordForm from "#/ui/PasswordForm"; import UserAvatar from "#/ui/UserAvatar"; -import { useRouter } from "next/navigation"; +import { getMostRecentCookieWithLoginname } from "#/utils/cookies"; -export default function Page() { - const router = useRouter(); +async function loadSession(loginName: string) { + const recent = await getMostRecentCookieWithLoginname(loginName); + console.log("found recent cookie: ", recent); + + return getSession(server, recent.id, recent.token).then(({ session }) => { + console.log(session); + return session; + }); + // const res = await fetch( + // `http://localhost:3000/session?` + + // new URLSearchParams({ + // loginName: loginName, + // }), + // { + // method: "GET", + // headers: { + // "Content-Type": "application/json", + // }, + // } + // ); + + // if (!res.ok) { + // throw new Error("Failed to load session"); + // } + + // return res.json(); +} + +export default async function Page({ searchParams }: { searchParams: any }) { + const { loginName } = searchParams; + console.log(loginName); + + const sessionFactors = await loadSession(loginName); + console.log(sessionFactors); return (
-

Password

+

{sessionFactors.factors.user.displayName}

Enter your password.

- + -
- -
- -
- - -
+
); } diff --git a/apps/login/app/(login)/username/page.tsx b/apps/login/app/(login)/username/page.tsx index a68482ef906..156949dab9c 100644 --- a/apps/login/app/(login)/username/page.tsx +++ b/apps/login/app/(login)/username/page.tsx @@ -1,7 +1,4 @@ "use client"; - -import { Button, ButtonVariants } from "#/ui/Button"; -import IdentityProviders from "#/ui/IdentityProviders"; import UsernameForm from "#/ui/UsernameForm"; export default function Page() { diff --git a/apps/login/app/session/route.ts b/apps/login/app/session/route.ts index 0ee1f7a8368..934bdccb1c7 100644 --- a/apps/login/app/session/route.ts +++ b/apps/login/app/session/route.ts @@ -1,4 +1,11 @@ -import { createSession, server, setSession } from "#/lib/zitadel"; +import { createSession, getSession, server, setSession } from "#/lib/zitadel"; +import { + SessionCookie, + addSessionToCookie, + getMostRecentCookieWithLoginname, + getMostRecentSessionCookie, + updateSessionCookie, +} from "#/utils/cookies"; import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { @@ -6,21 +13,93 @@ export async function POST(request: NextRequest) { if (body) { const { loginName } = body; - const session = await createSession(server, loginName); - return NextResponse.json(session); + const createdSession = await createSession(server, loginName); + + return getSession( + server, + createdSession.sessionId, + createdSession.sessionToken + ).then(({ session }) => { + console.log(session); + const sessionCookie: SessionCookie = { + id: createdSession.sessionId, + token: createdSession.sessionToken, + changeDate: session.changeDate, + loginName: session.factors.user.loginName, + }; + return addSessionToCookie(sessionCookie).then(() => { + return NextResponse.json({ factors: session.factors }); + }); + }); } else { return NextResponse.error(); } } +/** + * + * @param request password for the most recent session + * @returns the updated most recent Session with the added password + */ export async function PUT(request: NextRequest) { const body = await request.json(); if (body) { - const { loginName } = body; + const { password } = body; - const session = await setSession(server, loginName); - return NextResponse.json(session); + const recent = await getMostRecentSessionCookie(); + console.log("found recent cookie: ", recent); + const session = await setSession(server, recent.id, recent.token, password); + console.log("updatedsession", session); + + const sessionCookie: SessionCookie = { + id: recent.id, + token: session.sessionToken, + changeDate: session.details.changeDate, + loginName: recent.loginName, + }; + + return getSession(server, sessionCookie.id, sessionCookie.token).then( + ({ session }) => { + console.log(session); + const newCookie: SessionCookie = { + id: sessionCookie.id, + token: sessionCookie.token, + changeDate: session.changeDate, + loginName: session.factors.user.loginName, + }; + // return addSessionToCookie(sessionCookie).then(() => { + // return NextResponse.json({ factors: session.factors }); + // }); + return updateSessionCookie(sessionCookie.id, sessionCookie).then(() => { + console.log("updatedRecent:", sessionCookie); + return NextResponse.json({ factors: session.factors }); + }); + } + ); } else { return NextResponse.error(); } } + +// /** +// * +// * @param request loginName of a session +// * @returns the session +// */ +// export async function GET(request: NextRequest) { +// console.log(request); +// if (request) { +// const { loginName } = request.params; + +// const recent = await getMostRecentCookieWithLoginname(loginName); +// console.log("found recent cookie: ", recent); + +// return getSession(server, recent.id, recent.token).then(({ session }) => { +// console.log(session); + +// return NextResponse.json({ factors: session.factors }); +// }); +// } else { +// return NextResponse.error(); +// } +// } diff --git a/apps/login/lib/zitadel.ts b/apps/login/lib/zitadel.ts index 96476e8e407..6311d3ef6a6 100644 --- a/apps/login/lib/zitadel.ts +++ b/apps/login/lib/zitadel.ts @@ -90,10 +90,24 @@ export function createSession( export function setSession( server: ZitadelServer, - loginName: string + sessionId: string, + sessionToken: string, + password: string ): Promise { const sessionService = session.getSession(server); - return sessionService.setSession({ checks: { user: { loginName } } }, {}); + return sessionService.setSession( + { sessionId, sessionToken, checks: { password: { password } } }, + {} + ); +} + +export function getSession( + server: ZitadelServer, + sessionId: string, + sessionToken: string +): Promise { + const sessionService = session.getSession(server); + return sessionService.getSession({ sessionId, sessionToken }, {}); } export type AddHumanUserData = { diff --git a/apps/login/next.config.js b/apps/login/next.config.js index 2f60c3de82e..47304cc9a2d 100755 --- a/apps/login/next.config.js +++ b/apps/login/next.config.js @@ -3,8 +3,7 @@ const nextConfig = { reactStrictMode: true, // Recommended for the `pages` directory, default in `app`. swcMinify: true, experimental: { - // Required: - appDir: true, + serverActions: true, }, images: { remotePatterns: [ diff --git a/apps/login/ui/Avatar.tsx b/apps/login/ui/Avatar.tsx index 7275da2b00f..25778cdc5eb 100644 --- a/apps/login/ui/Avatar.tsx +++ b/apps/login/ui/Avatar.tsx @@ -1,4 +1,4 @@ -import { Color, ColorShade, getColorHash } from "#/utils/colors"; +import { ColorShade, getColorHash } from "#/utils/colors"; import { useTheme } from "next-themes"; import { FC } from "react"; @@ -23,7 +23,7 @@ export const Avatar: FC = ({ imageUrl, shadow, }) => { - const { resolvedTheme } = useTheme(); + // const { resolvedTheme } = useTheme(); let credentials = ""; if (name) { @@ -46,15 +46,15 @@ export const Avatar: FC = ({ const color: ColorShade = getColorHash(loginName); - const avatarStyleDark = { - backgroundColor: color[900], - color: color[200], - }; + // const avatarStyleDark = { + // backgroundColor: color[900], + // color: color[200], + // }; - const avatarStyleLight = { - backgroundColor: color[200], - color: color[900], - }; + // const avatarStyleLight = { + // backgroundColor: color[200], + // color: color[900], + // }; return (
= ({ ? "w-[32px] h-[32px] font-bold" : "" }`} - style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark} + // style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark} > {imageUrl ? ( ({ mode: "onBlur", }); @@ -22,7 +22,7 @@ export default function UsernameForm() { const router = useRouter(); - async function submitUsername(values: Inputs) { + async function submitPassword(values: Inputs) { setLoading(true); const res = await fetch("/session", { method: "PUT", @@ -43,9 +43,10 @@ export default function UsernameForm() { return res.json(); } - function submitAndLink(value: Inputs): Promise { - return submitUsername(value).then((resp: any) => { - return router.push(`/password`); + function submitPasswordAndContinue(value: Inputs): Promise { + console.log(value); + return submitPassword(value).then((resp: any) => { + return router.push(`/success`); }); } @@ -58,7 +59,7 @@ export default function UsernameForm() { type="password" autoComplete="password" {...register("password", { required: "This field is required" })} - label="Loginname" + label="Password" // error={errors.username?.message as string} />
@@ -73,7 +74,7 @@ export default function UsernameForm() { className="self-end" variant={ButtonVariants.Primary} disabled={loading || !formState.isValid} - onClick={handleSubmit(submitAndLink)} + onClick={handleSubmit(submitPasswordAndContinue)} > {loading && } continue diff --git a/apps/login/ui/UserAvatar.tsx b/apps/login/ui/UserAvatar.tsx index fbf3b3886e5..d64679367f5 100644 --- a/apps/login/ui/UserAvatar.tsx +++ b/apps/login/ui/UserAvatar.tsx @@ -1,16 +1,32 @@ import { Avatar, AvatarSize } from "#/ui/Avatar"; +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import Link from "next/link"; type Props = { - name: string; + loginName: string; + showDropdown: boolean; }; -export default function UserAvatar({ name }: Props) { +export default function UserAvatar({ loginName, showDropdown }: Props) { return (
- +
- {name} + {loginName} + + {showDropdown && ( + + + + )}
); } diff --git a/apps/login/ui/UsernameForm.tsx b/apps/login/ui/UsernameForm.tsx index fe05c8b8c4e..4c2d9cf1b55 100644 --- a/apps/login/ui/UsernameForm.tsx +++ b/apps/login/ui/UsernameForm.tsx @@ -41,9 +41,10 @@ export default function UsernameForm() { return res.json(); } - function submitAndLink(value: Inputs): Promise { - return submitUsername(value).then((resp: any) => { - return router.push(`/password`); + function submitUsernameAndContinue(value: Inputs): Promise { + return submitUsername(value).then(({ factors }) => { + console.log(factors); + return router.push(`/password?loginName=${factors.user.loginName}`); }); } @@ -71,7 +72,7 @@ export default function UsernameForm() { className="self-end" variant={ButtonVariants.Primary} disabled={loading || !formState.isValid} - onClick={handleSubmit(submitAndLink)} + onClick={handleSubmit(submitUsernameAndContinue)} > {loading && } continue diff --git a/apps/login/utils/cookies.ts b/apps/login/utils/cookies.ts new file mode 100644 index 00000000000..3dad52bb67e --- /dev/null +++ b/apps/login/utils/cookies.ts @@ -0,0 +1,152 @@ +"use server"; + +import { cookies } from "next/headers"; + +export type SessionCookie = { + id: string; + token: string; + loginName: string; + changeDate: string; +}; + +async function set(sessions: SessionCookie[]) { + const cookiesList = cookies(); + // @ts-ignore + cookiesList.set({ + name: "sessions", + value: JSON.stringify(sessions), + httpOnly: true, + path: "/", + }); +} + +export async function addSessionToCookie(session: SessionCookie): Promise { + const cookiesList = cookies(); + // const hasSessions = cookiesList.has("sessions"); + // if (hasSessions) { + const stringifiedCookie = cookiesList.get("sessions"); + + const currentSessions: SessionCookie[] = stringifiedCookie?.value + ? JSON.parse(stringifiedCookie?.value) + : []; + + // @ts-ignore + return cookiesList.set({ + name: "sessions", + value: JSON.stringify([...currentSessions, session]), + httpOnly: true, + path: "/", + }); + // } else { + // return set([session]); + // } +} + +export async function updateSessionCookie( + id: string, + session: SessionCookie +): Promise { + const cookiesList = cookies(); + // const hasSessions = cookiesList.has("sessions"); + // if (hasSessions) { + const stringifiedCookie = cookiesList.get("sessions"); + + const sessions: SessionCookie[] = stringifiedCookie?.value + ? JSON.parse(stringifiedCookie?.value) + : [session]; + + const foundIndex = sessions.findIndex((session) => session.id === id); + sessions[foundIndex] = session; + + // @ts-ignore + return cookiesList.set({ + name: "sessions", + value: JSON.stringify(sessions), + httpOnly: true, + path: "/", + }); + // } else { + // return Promise.reject(); + // } +} + +export async function removeSessionFromCookie( + session: SessionCookie +): Promise { + const cookiesList = cookies(); + // const hasSessions = cookiesList.has("sessions"); + // if (hasSessions) { + const stringifiedCookie = cookiesList.get("sessions"); + + const sessions: SessionCookie[] = stringifiedCookie?.value + ? JSON.parse(stringifiedCookie?.value) + : [session]; + + const filteredSessions = sessions.filter( + (session) => session.id !== session.id + ); + + // @ts-ignore + return cookiesList.set({ + name: "sessions", + value: JSON.stringify(filteredSessions), + httpOnly: true, + path: "/", + }); + // } else { + // return Promise.reject(); + // } +} + +export async function getMostRecentSessionCookie(): Promise { + const cookiesList = cookies(); + // const hasSessions = cookiesList.has("sessions"); + // if (hasSessions) { + const stringifiedCookie = cookiesList.get("sessions"); + + if (stringifiedCookie?.value) { + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + + console.log(sessions); + const latest = sessions.reduce((prev, current) => { + return new Date(prev.changeDate).getTime() > + new Date(current.changeDate).getTime() + ? prev + : current; + }); + + return latest; + } else { + return Promise.reject(); + } + // } else { + // return Promise.reject(); + // } +} + +export async function getMostRecentCookieWithLoginname( + loginName: string +): Promise { + const cookiesList = cookies(); + + const stringifiedCookie = cookiesList.get("sessions"); + + if (stringifiedCookie?.value) { + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + + const latest = sessions + .filter((cookie) => cookie.loginName === loginName) + .reduce((prev, current) => { + return new Date(prev.changeDate).getTime() > + new Date(current.changeDate).getTime() + ? prev + : current; + }); + + return latest; + } else { + return Promise.reject(); + } +} + +export async function clearSessions() {} diff --git a/packages/zitadel-server/src/v2/session/session.ts b/packages/zitadel-server/src/v2/session/session.ts index 493cf4091a6..4569fa4db8d 100644 --- a/packages/zitadel-server/src/v2/session/session.ts +++ b/packages/zitadel-server/src/v2/session/session.ts @@ -8,7 +8,6 @@ import { import { ZitadelServer, createClient, getServers } from "../../server"; export const getSession = (server?: string | ZitadelServer) => { - console.log("init session"); let config; if (server && typeof server === "string") { const apps = getServers();