cookie and session handling, password UI

This commit is contained in:
Max Peintner
2023-05-17 13:46:44 +02:00
parent 8a190e28c6
commit d3562b1f63
11 changed files with 338 additions and 61 deletions

View File

@@ -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>
);
}

View File

@@ -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() {

View File

@@ -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();
// }
// }

View File

@@ -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 = {

View File

@@ -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: [

View File

@@ -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

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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
View 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() {}

View File

@@ -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();