mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 12:29:04 +00:00
cookie and session handling, password UI
This commit is contained in:
@@ -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 (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<h1>Password</h1>
|
||||
<h1>{sessionFactors.factors.user.displayName}</h1>
|
||||
<p className="ztdl-p mb-6 block">Enter your password.</p>
|
||||
|
||||
<UserAvatar name="max@zitadel.com"></UserAvatar>
|
||||
<UserAvatar loginName={loginName} showDropdown></UserAvatar>
|
||||
|
||||
<div className="w-full">
|
||||
<TextInput type="password" label="Password" />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<Button
|
||||
onClick={() => router.back()}
|
||||
variant={ButtonVariants.Secondary}
|
||||
>
|
||||
back
|
||||
</Button>
|
||||
<Button variant={ButtonVariants.Primary}>continue</Button>
|
||||
</div>
|
||||
<PasswordForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -90,10 +90,24 @@ export function createSession(
|
||||
|
||||
export function setSession(
|
||||
server: ZitadelServer,
|
||||
loginName: string
|
||||
sessionId: string,
|
||||
sessionToken: string,
|
||||
password: string
|
||||
): Promise<any | undefined> {
|
||||
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<any | undefined> {
|
||||
const sessionService = session.getSession(server);
|
||||
return sessionService.getSession({ sessionId, sessionToken }, {});
|
||||
}
|
||||
|
||||
export type AddHumanUserData = {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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<AvatarProps> = ({
|
||||
imageUrl,
|
||||
shadow,
|
||||
}) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
// const { resolvedTheme } = useTheme();
|
||||
let credentials = "";
|
||||
|
||||
if (name) {
|
||||
@@ -46,15 +46,15 @@ export const Avatar: FC<AvatarProps> = ({
|
||||
|
||||
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 (
|
||||
<div
|
||||
@@ -69,7 +69,7 @@ export const Avatar: FC<AvatarProps> = ({
|
||||
? "w-[32px] h-[32px] font-bold"
|
||||
: ""
|
||||
}`}
|
||||
style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark}
|
||||
// style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<img
|
||||
|
||||
@@ -11,7 +11,7 @@ type Inputs = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export default function UsernameForm() {
|
||||
export default function PasswordForm() {
|
||||
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||
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<boolean | void> {
|
||||
return submitUsername(value).then((resp: any) => {
|
||||
return router.push(`/password`);
|
||||
function submitPasswordAndContinue(value: Inputs): Promise<boolean | void> {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
@@ -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 && <Spinner className="h-5 w-5 mr-2" />}
|
||||
continue
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex h-full w-full flex-row items-center rounded-full border p-[1px] dark:border-white/20">
|
||||
<div>
|
||||
<Avatar size={AvatarSize.SMALL} name={name} loginName={name} />
|
||||
<Avatar
|
||||
size={AvatarSize.SMALL}
|
||||
name={loginName}
|
||||
loginName={loginName}
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-4 text-14px">{name}</span>
|
||||
<span className="ml-4 text-14px">{loginName}</span>
|
||||
<span className="flex-grow"></span>
|
||||
{showDropdown && (
|
||||
<Link
|
||||
href="/accounts"
|
||||
className="flex items-center justify-center p-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full mr-1 transition-all"
|
||||
>
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,9 +41,10 @@ export default function UsernameForm() {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function submitAndLink(value: Inputs): Promise<boolean | void> {
|
||||
return submitUsername(value).then((resp: any) => {
|
||||
return router.push(`/password`);
|
||||
function submitUsernameAndContinue(value: Inputs): Promise<boolean | void> {
|
||||
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 && <Spinner className="h-5 w-5 mr-2" />}
|
||||
continue
|
||||
|
||||
152
apps/login/utils/cookies.ts
Normal file
152
apps/login/utils/cookies.ts
Normal file
@@ -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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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() {}
|
||||
Reference in New Issue
Block a user