Merge branch 'main' into idp-linking

This commit is contained in:
peintnermax
2024-08-21 09:40:17 +02:00
149 changed files with 2826 additions and 4470 deletions

View File

@@ -1 +1,2 @@
ZITADEL_API_URL=http://localhost:22222
ZITADEL_API_URL=http://localhost:22222
DEBUG=true

View File

@@ -2,7 +2,7 @@ import { stub } from "../support/mock";
describe("login", () => {
beforeEach(() => {
stub("zitadel.session.v2beta.SessionService", "CreateSession", {
stub("zitadel.session.v2.SessionService", "CreateSession", {
data: {
details: {
sequence: 859,
@@ -16,7 +16,7 @@ describe("login", () => {
},
});
stub("zitadel.session.v2beta.SessionService", "GetSession", {
stub("zitadel.session.v2.SessionService", "GetSession", {
data: {
session: {
id: "221394658884845598",
@@ -37,7 +37,7 @@ describe("login", () => {
},
});
stub("zitadel.settings.v2beta.SettingsService", "GetLoginSettings", {
stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", {
data: {
settings: {
passkeysType: 1,
@@ -47,7 +47,7 @@ describe("login", () => {
});
describe("password login", () => {
beforeEach(() => {
stub("zitadel.user.v2beta.UserService", "ListUsers", {
stub("zitadel.user.v2.UserService", "ListUsers", {
data: {
details: {
totalResult: 1,
@@ -79,7 +79,7 @@ describe("login", () => {
],
},
});
stub("zitadel.user.v2beta.UserService", "ListAuthenticationMethodTypes", {
stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", {
data: {
authMethodTypes: [1], // 1 for password authentication
},
@@ -91,7 +91,7 @@ describe("login", () => {
});
describe("with passkey prompt", () => {
beforeEach(() => {
stub("zitadel.session.v2beta.SessionService", "SetSession", {
stub("zitadel.session.v2.SessionService", "SetSession", {
data: {
details: {
sequence: 859,
@@ -118,7 +118,7 @@ describe("login", () => {
});
describe("passkey login", () => {
beforeEach(() => {
stub("zitadel.user.v2beta.UserService", "ListUsers", {
stub("zitadel.user.v2.UserService", "ListUsers", {
data: {
details: {
totalResult: 1,
@@ -150,7 +150,7 @@ describe("login", () => {
],
},
});
stub("zitadel.user.v2beta.UserService", "ListAuthenticationMethodTypes", {
stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", {
data: {
authMethodTypes: [2], // 2 for passwordless authentication
},

View File

@@ -4,7 +4,7 @@ const IDP_URL = "https://example.com/idp/url";
describe("register idps", () => {
beforeEach(() => {
stub("zitadel.user.v2beta.UserService", "StartIdentityProviderIntent", {
stub("zitadel.user.v2.UserService", "StartIdentityProviderIntent", {
data: {
authUrl: IDP_URL,
},

View File

@@ -2,7 +2,7 @@ import { stub } from "../support/mock";
describe("register", () => {
beforeEach(() => {
stub("zitadel.user.v2beta.UserService", "AddHumanUser", {
stub("zitadel.user.v2.UserService", "AddHumanUser", {
data: {
userId: "123",
},

View File

@@ -2,12 +2,12 @@ import { stub } from "../support/mock";
describe("/verify", () => {
it("redirects after successful email verification", () => {
stub("zitadel.user.v2beta.UserService", "VerifyEmail");
stub("zitadel.user.v2.UserService", "VerifyEmail");
cy.visit("/verify?userId=123&code=abc&submit=true");
cy.location("pathname", { timeout: 10_000 }).should("eq", "/loginname");
});
it("shows an error if validation failed", () => {
stub("zitadel.user.v2beta.UserService", "VerifyEmail", {
stub("zitadel.user.v2.UserService", "VerifyEmail", {
code: 3,
error: "error validating code",
});

View File

@@ -1,11 +1,11 @@
[
{
"service": "zitadel.settings.v2beta.SettingsService",
"service": "zitadel.settings.v2.SettingsService",
"method": "GetBrandingSettings",
"out": {}
},
{
"service": "zitadel.settings.v2beta.SettingsService",
"service": "zitadel.settings.v2.SettingsService",
"method": "GetLegalAndSupportSettings",
"out": {
"data": {
@@ -18,7 +18,7 @@
}
},
{
"service": "zitadel.settings.v2beta.SettingsService",
"service": "zitadel.settings.v2.SettingsService",
"method": "GetActiveIdentityProviders",
"out": {
"data": {
@@ -33,7 +33,7 @@
}
},
{
"service": "zitadel.settings.v2beta.SettingsService",
"service": "zitadel.settings.v2.SettingsService",
"method": "GetPasswordComplexitySettings",
"out": {
"data": {

View File

@@ -1,6 +1,6 @@
zitadel/user/v2beta/user_service.proto
zitadel/session/v2beta/session_service.proto
zitadel/settings/v2beta/settings_service.proto
zitadel/user/v2/user_service.proto
zitadel/session/v2/session_service.proto
zitadel/settings/v2/settings_service.proto
zitadel/management.proto
zitadel/auth.proto
zitadel/admin.proto

View File

@@ -33,9 +33,6 @@ const secureHeaders = [
const nextConfig = {
reactStrictMode: true, // Recommended for the `pages` directory, default in `app`.
swcMinify: true,
experimental: {
serverActions: true,
},
images: {
remotePatterns: [
{

View File

@@ -36,51 +36,52 @@
"@heroicons/react": "2.1.3",
"@tailwindcss/forms": "0.5.7",
"@vercel/analytics": "^1.2.2",
"@zitadel/proto": "workspace:*",
"@zitadel/client2": "workspace:*",
"@zitadel/react": "workspace:*",
"@zitadel/client": "workspace:*",
"@zitadel/next": "workspace:*",
"@zitadel/node": "workspace:*",
"@zitadel/proto": "workspace:*",
"@zitadel/react": "workspace:*",
"clsx": "1.2.1",
"copy-to-clipboard": "^3.3.3",
"moment": "^2.29.4",
"next": "14.2.3",
"next": "14.2.5",
"next-themes": "^0.2.1",
"nice-grpc": "2.0.1",
"qrcode.react": "^3.1.0",
"react": "18.3.1",
"react": "^18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "7.39.5",
"swr": "^2.2.0",
"tinycolor2": "1.4.2"
},
"devDependencies": {
"@bufbuild/buf": "^1.14.0",
"@bufbuild/buf": "^1.36.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/ms": "0.7.31",
"@types/node": "18.11.9",
"@types/react": "18.2.8",
"@types/react-dom": "18.0.9",
"@testing-library/react": "^16.0.0",
"@types/ms": "0.7.34",
"@types/node": "22.1.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/tinycolor2": "1.4.3",
"@types/uuid": "^9.0.1",
"@types/uuid": "^10.0.0",
"@vercel/git-hooks": "1.0.0",
"@zitadel/prettier-config": "workspace:*",
"@zitadel/tsconfig": "workspace:*",
"autoprefixer": "10.4.13",
"autoprefixer": "10.4.20",
"concurrently": "^8.1.0",
"cypress": "^13.9.0",
"del-cli": "5.0.0",
"cypress": "^13.13.2",
"del-cli": "5.1.0",
"env-cmd": "^10.1.0",
"eslint-config-zitadel": "workspace:*",
"grpc-tools": "1.11.3",
"lint-staged": "13.0.3",
"make-dir-cli": "3.0.0",
"nodemon": "^2.0.22",
"postcss": "8.4.21",
"prettier-plugin-tailwindcss": "0.1.13",
"grpc-tools": "1.12.4",
"lint-staged": "15.2.8",
"make-dir-cli": "4.0.0",
"nodemon": "^3.1.4",
"postcss": "8.4.41",
"prettier-plugin-tailwindcss": "0.6.5",
"sass": "^1.77.1",
"start-server-and-test": "^2.0.0",
"tailwindcss": "3.2.4",
"tailwindcss": "3.4.9",
"ts-proto": "^1.139.0",
"typescript": "^5.4.5",
"zitadel-tailwind-config": "workspace:*"

View File

@@ -1,5 +1,5 @@
import { getBrandingSettings, listSessions } from "@/lib/zitadel";
import { getAllSessionCookieIds } from "@/utils/cookies";
import { getAllSessionCookieIds } from "@zitadel/next";
import { UserPlusIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import SessionsList from "@/ui/SessionsList";

View File

@@ -1,14 +1,7 @@
import { ProviderSlug } from "@/lib/demos";
import { getBrandingSettings } from "@/lib/zitadel";
import { getBrandingSettings, PROVIDER_NAME_MAPPING } from "@/lib/zitadel";
import DynamicTheme from "@/ui/DynamicTheme";
const PROVIDER_NAME_MAPPING: {
[provider: string]: string;
} = {
[ProviderSlug.GOOGLE]: "Google",
[ProviderSlug.GITHUB]: "GitHub",
};
export default async function Page({
searchParams,
params,

View File

@@ -8,6 +8,104 @@ import {
import Alert, { AlertType } from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import IdpSignin from "@/ui/IdpSignin";
import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { IDPInformation, IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import { PartialMessage } from "@zitadel/client";
const PROVIDER_MAPPING: {
[provider: string]: (
rI: IDPInformation,
) => PartialMessage<AddHumanUserRequest>;
} = {
[ProviderSlug.GOOGLE]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation?.toJson() as {
User: {
email: string;
name?: string;
given_name?: string;
family_name?: string;
};
};
const idpLink: PartialMessage<IDPLink> = {
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
};
const req: PartialMessage<AddHumanUserRequest> = {
username: idp.userName,
email: {
email: rawInfo.User?.email,
verification: { case: "isVerified", value: true },
},
// organisation: Organisation | undefined;
profile: {
displayName: rawInfo.User?.name ?? "",
givenName: rawInfo.User?.given_name ?? "",
familyName: rawInfo.User?.family_name ?? "",
},
idpLinks: [idpLink],
};
return req;
},
[ProviderSlug.AZURE]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation?.toJson() as {
mail: string;
displayName?: string;
givenName?: string;
surname?: string;
};
const idpLink: PartialMessage<IDPLink> = {
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
};
const req: PartialMessage<AddHumanUserRequest> = {
username: idp.userName,
email: {
email: rawInfo?.mail,
verification: { case: "isVerified", value: true },
},
// organisation: Organisation | undefined;
profile: {
displayName: rawInfo?.displayName ?? "",
givenName: rawInfo?.givenName ?? "",
familyName: rawInfo?.surname ?? "",
},
idpLinks: [idpLink],
};
return req;
},
[ProviderSlug.GITHUB]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation?.toJson() as {
email: string;
name: string;
};
const idpLink: PartialMessage<IDPLink> = {
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
};
const req: PartialMessage<AddHumanUserRequest> = {
username: idp.userName,
email: {
email: rawInfo?.email,
verification: { case: "isVerified", value: true },
},
// organisation: Organisation | undefined;
profile: {
displayName: rawInfo?.name ?? "",
givenName: rawInfo?.name ?? "",
familyName: rawInfo?.name ?? "",
},
idpLinks: [idpLink],
};
return req;
},
};
export default async function Page({
searchParams,

View File

@@ -5,7 +5,7 @@ import {
} from "@/lib/zitadel";
import DynamicTheme from "@/ui/DynamicTheme";
import { SignInWithIDP } from "@/ui/SignInWithIDP";
import { makeReqCtx } from "@zitadel/client2/v2beta";
import { makeReqCtx } from "@zitadel/client/v2";
function getIdentityProviders(orgId?: string) {
return settingsService

View File

@@ -7,7 +7,7 @@ import {
import DynamicTheme from "@/ui/DynamicTheme";
import { SignInWithIDP } from "@/ui/SignInWithIDP";
import UsernameForm from "@/ui/UsernameForm";
import { makeReqCtx } from "@zitadel/client2/v2beta";
import { makeReqCtx } from "@zitadel/client/v2";
function getIdentityProviders(orgId?: string) {
return settingsService
@@ -51,16 +51,16 @@ export default async function Page({
organization={organization}
submit={submit}
allowRegister={!!loginSettings?.allowRegister}
/>
{legal && identityProviders && process.env.ZITADEL_API_URL && (
<SignInWithIDP
host={host}
identityProviders={identityProviders}
authRequestId={authRequestId}
organization={organization}
></SignInWithIDP>
)}
>
{legal && identityProviders && process.env.ZITADEL_API_URL && (
<SignInWithIDP
host={host}
identityProviders={identityProviders}
authRequestId={authRequestId}
organization={organization}
></SignInWithIDP>
)}
</UsernameForm>
</div>
</DynamicTheme>
);

View File

@@ -2,16 +2,14 @@ import {
getBrandingSettings,
getSession,
listAuthenticationMethodTypes,
sessionService,
} from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import BackButton from "@/ui/BackButton";
import ChooseSecondFactor from "@/ui/ChooseSecondFactor";
import DynamicTheme from "@/ui/DynamicTheme";
import UserAvatar from "@/ui/UserAvatar";
import {
getMostRecentCookieWithLoginname,
getSessionCookieById,
} from "@/utils/cookies";
import { getSessionCookieById, loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,
@@ -29,26 +27,25 @@ export default async function Page({
loginName?: string,
organization?: string,
) {
const recent = await getMostRecentCookieWithLoginname(
return loadMostRecentSession(sessionService, {
loginName,
organization,
);
return getSession(recent.id, recent.token).then((response) => {
if (response?.session && response.session.factors?.user?.id) {
return listAuthenticationMethodTypes(
response.session.factors.user.id,
).then((methods) => {
return {
factors: response.session?.factors,
authMethods: methods.authMethodTypes ?? [],
};
});
}).then((session) => {
if (session && session.factors?.user?.id) {
return listAuthenticationMethodTypes(session.factors.user.id).then(
(methods) => {
return {
factors: session?.factors,
authMethods: methods.authMethodTypes ?? [],
};
},
);
}
});
}
async function loadSessionById(sessionId: string, organization?: string) {
const recent = await getSessionCookieById(sessionId, organization);
const recent = await getSessionCookieById({ sessionId, organization });
return getSession(recent.id, recent.token).then((response) => {
if (response?.session && response.session.factors?.user?.id) {
return listAuthenticationMethodTypes(

View File

@@ -4,16 +4,14 @@ import {
getSession,
getUserByID,
listAuthenticationMethodTypes,
sessionService,
} from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import BackButton from "@/ui/BackButton";
import ChooseSecondFactorToSetup from "@/ui/ChooseSecondFactorToSetup";
import DynamicTheme from "@/ui/DynamicTheme";
import UserAvatar from "@/ui/UserAvatar";
import {
getMostRecentCookieWithLoginname,
getSessionCookieById,
} from "@/utils/cookies";
import { getSessionCookieById, loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,
@@ -31,13 +29,12 @@ export default async function Page({
loginName?: string,
organization?: string,
) {
const recent = await getMostRecentCookieWithLoginname(
return loadMostRecentSession(sessionService, {
loginName,
organization,
);
return getSession(recent.id, recent.token).then((response) => {
if (response?.session && response.session.factors?.user?.id) {
const userId = response.session.factors.user.id;
}).then((session) => {
if (session && session.factors?.user?.id) {
const userId = session.factors.user.id;
return listAuthenticationMethodTypes(userId).then((methods) => {
return getUserByID(userId).then((user) => {
const humanUser =
@@ -46,7 +43,7 @@ export default async function Page({
: undefined;
return {
factors: response.session?.factors,
factors: session?.factors,
authMethods: methods.authMethodTypes ?? [],
phoneVerified: humanUser?.phone?.isVerified ?? false,
emailVerified: humanUser?.email?.isVerified ?? false,
@@ -58,7 +55,7 @@ export default async function Page({
}
async function loadSessionById(sessionId: string, organization?: string) {
const recent = await getSessionCookieById(sessionId, organization);
const recent = await getSessionCookieById({ sessionId, organization });
return getSession(recent.id, recent.token).then((response) => {
if (response?.session && response.session.factors?.user?.id) {
const userId = response.session.factors.user.id;

View File

@@ -1,13 +1,9 @@
import {
getBrandingSettings,
getLoginSettings,
getSession,
} from "@/lib/zitadel";
import { getBrandingSettings, sessionService } from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import LoginOTP from "@/ui/LoginOTP";
import UserAvatar from "@/ui/UserAvatar";
import { getMostRecentCookieWithLoginname } from "@/utils/cookies";
import { loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,
@@ -21,21 +17,13 @@ export default async function Page({
const { method } = params;
const { session, token } = await loadSession(loginName, organization);
const session = await loadMostRecentSession(sessionService, {
loginName,
organization,
});
const branding = await getBrandingSettings(organization);
async function loadSession(loginName?: string, organization?: string) {
const recent = await getMostRecentCookieWithLoginname(
loginName,
organization,
);
return getSession(recent.id, recent.token).then((response) => {
return { session: response?.session, token: recent.token };
});
}
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">

View File

@@ -2,19 +2,18 @@ import {
addOTPEmail,
addOTPSMS,
getBrandingSettings,
getSession,
registerTOTP,
sessionService,
} from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import BackButton from "@/ui/BackButton";
import { Button, ButtonVariants } from "@/ui/Button";
import DynamicTheme from "@/ui/DynamicTheme";
import { Spinner } from "@/ui/Spinner";
import TOTPRegister from "@/ui/TOTPRegister";
import UserAvatar from "@/ui/UserAvatar";
import { getMostRecentCookieWithLoginname } from "@/utils/cookies";
import Link from "next/link";
import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2beta/user_service_pb";
import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,
@@ -28,7 +27,10 @@ export default async function Page({
const { method } = params;
const branding = await getBrandingSettings(organization);
const { session, token } = await loadSession(loginName, organization);
const session = await loadMostRecentSession(sessionService, {
loginName,
organization,
});
let totpResponse: RegisterTOTPResponse | undefined,
totpError: Error | undefined;
@@ -56,17 +58,6 @@ export default async function Page({
throw new Error("No session found");
}
async function loadSession(loginName?: string, organization?: string) {
const recent = await getMostRecentCookieWithLoginname(
loginName,
organization,
);
return getSession(recent.id, recent.token).then((response) => {
return { session: response?.session, token: recent.token };
});
}
const paramsToContinue = new URLSearchParams({});
let urlToContinue = "/accounts";

View File

@@ -0,0 +1,45 @@
import { demos } from "@/lib/demos";
import Link from "next/link";
export default function Page() {
return (
<div className="rounded-lg bg-vc-border-gradient dark:bg-dark-vc-border-gradient p-px shadow-lg shadow-black/5 dark:shadow-black/20 mb-10">
<div className="rounded-lg bg-background-light-400 dark:bg-background-dark-500 px-8 py-12">
<div className="space-y-8">
<h1 className="text-xl font-medium">Pages</h1>
<div className="space-y-10">
{demos.map((section) => {
return (
<div key={section.name} className="space-y-5">
<div className="text-xs font-semibold uppercase tracking-wider text-gray-500">
{section.name}
</div>
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
{section.items.map((item) => {
return (
<Link
href={`/${item.slug}`}
key={item.name}
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">{item.name}</div>
{item.description ? (
<div className="line-clamp-3 text-sm text-text-light-secondary-500 dark:text-text-dark-secondary-500">
{item.description}
</div>
) : null}
</Link>
);
})}
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,9 +1,9 @@
import { getBrandingSettings, getSession } from "@/lib/zitadel";
import { getBrandingSettings, sessionService } from "@/lib/zitadel";
import Alert, { AlertType } from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import RegisterPasskey from "@/ui/RegisterPasskey";
import UserAvatar from "@/ui/UserAvatar";
import { getMostRecentCookieWithLoginname } from "@/utils/cookies";
import { loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,
@@ -13,19 +13,11 @@ export default async function Page({
const { loginName, promptPasswordless, organization, authRequestId } =
searchParams;
const sessionFactors = await loadSession(loginName);
const session = await loadMostRecentSession(sessionService, {
loginName,
organization,
});
async function loadSession(loginName?: string) {
const recent = await getMostRecentCookieWithLoginname(
loginName,
organization,
);
return getSession(recent.id, recent.token).then((response) => {
if (response?.session) {
return response.session;
}
});
}
const title = !!promptPasswordless
? "Authenticate with a passkey"
: "Use your passkey to confirm it's really you";
@@ -40,10 +32,10 @@ export default async function Page({
<div className="flex flex-col items-center space-y-4">
<h1>{title}</h1>
{sessionFactors && (
{session && (
<UserAvatar
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
displayName={sessionFactors.factors?.user?.displayName}
loginName={loginName ?? session.factors?.user?.loginName}
displayName={session.factors?.user?.displayName}
showDropdown
searchParams={searchParams}
></UserAvatar>
@@ -64,7 +56,7 @@ export default async function Page({
</span>
</Alert>
{!sessionFactors && (
{!session && (
<div className="py-4">
<Alert>
Could not get the context of the user. Make sure to enter the
@@ -73,9 +65,9 @@ export default async function Page({
</div>
)}
{sessionFactors?.id && (
{session?.id && (
<RegisterPasskey
sessionId={sessionFactors.id}
sessionId={session.id}
isPrompt={!!promptPasswordless}
organization={organization}
authRequestId={authRequestId}

View File

@@ -1,12 +1,9 @@
import { getBrandingSettings, getSession } from "@/lib/zitadel";
import { getBrandingSettings, getSession, sessionService } from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import LoginPasskey from "@/ui/LoginPasskey";
import UserAvatar from "@/ui/UserAvatar";
import {
getMostRecentCookieWithLoginname,
getSessionCookieById,
} from "@/utils/cookies";
import { getSessionCookieById, loadMostRecentSession } from "@zitadel/next";
const title = "Authenticate with a passkey";
const description =
@@ -22,25 +19,10 @@ export default async function Page({
const sessionFactors = sessionId
? await loadSessionById(sessionId, organization)
: await loadSessionByLoginname(loginName, organization);
async function loadSessionByLoginname(
loginName?: string,
organization?: string,
) {
const recent = await getMostRecentCookieWithLoginname(
loginName,
organization,
);
return getSession(recent.id, recent.token).then((response) => {
if (response?.session) {
return response.session;
}
});
}
: await loadMostRecentSession(sessionService, { loginName, organization });
async function loadSessionById(sessionId: string, organization?: string) {
const recent = await getSessionCookieById(sessionId, organization);
const recent = await getSessionCookieById({ sessionId, organization });
return getSession(recent.id, recent.token).then((response) => {
if (response?.session) {
return response.session;

View File

@@ -1,13 +1,13 @@
import {
getBrandingSettings,
getLoginSettings,
getSession,
sessionService,
} from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import PasswordForm from "@/ui/PasswordForm";
import UserAvatar from "@/ui/UserAvatar";
import { getMostRecentCookieWithLoginname } from "@/utils/cookies";
import { loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,
@@ -16,20 +16,11 @@ export default async function Page({
}) {
const { loginName, organization, promptPasswordless, authRequestId, alt } =
searchParams;
const sessionFactors = await loadSession(loginName, organization);
async function loadSession(loginName?: string, organization?: string) {
const recent = await getMostRecentCookieWithLoginname(
loginName,
organization,
);
return getSession(recent.id, recent.token).then((response) => {
if (response?.session) {
return response.session;
}
});
}
const sessionFactors = await loadMostRecentSession(sessionService, {
loginName,
organization,
});
const branding = await getBrandingSettings(organization);
const loginSettings = await getLoginSettings(organization);

View File

@@ -51,6 +51,9 @@ export default async function Page({
<RegisterFormWithoutPassword
legal={legal}
organization={organization}
firstname={firstname}
lastname={lastname}
email={email}
authRequestId={authRequestId}
></RegisterFormWithoutPassword>
)}

View File

@@ -1,11 +1,11 @@
import { createCallback, getBrandingSettings, getSession } from "@/lib/zitadel";
import DynamicTheme from "@/ui/DynamicTheme";
import UserAvatar from "@/ui/UserAvatar";
import { getMostRecentCookieWithLoginname } from "@/utils/cookies";
import { getMostRecentCookieWithLoginname } from "@zitadel/next";
import { redirect } from "next/navigation";
async function loadSession(loginName: string, authRequestId?: string) {
const recent = await getMostRecentCookieWithLoginname(`${loginName}`);
const recent = await getMostRecentCookieWithLoginname({ loginName });
if (authRequestId) {
return createCallback({

View File

@@ -1,23 +1,14 @@
import {
getBrandingSettings,
getLoginSettings,
getSession,
} from "@/lib/zitadel";
import { getBrandingSettings, getSession, sessionService } from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import LoginPasskey from "@/ui/LoginPasskey";
import UserAvatar from "@/ui/UserAvatar";
import {
getMostRecentCookieWithLoginname,
getSessionCookieById,
} from "@/utils/cookies";
import { getSessionCookieById, loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,
params,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
params: Record<string | number | symbol, string | undefined>;
}) {
const { loginName, authRequestId, sessionId, organization } = searchParams;
@@ -25,25 +16,10 @@ export default async function Page({
const sessionFactors = sessionId
? await loadSessionById(sessionId, organization)
: await loadSessionByLoginname(loginName, organization);
async function loadSessionByLoginname(
loginName?: string,
organization?: string,
) {
const recent = await getMostRecentCookieWithLoginname(
loginName,
organization,
);
return getSession(recent.id, recent.token).then((response) => {
if (response?.session) {
return response.session;
}
});
}
: await loadMostRecentSession(sessionService, { loginName, organization });
async function loadSessionById(sessionId: string, organization?: string) {
const recent = await getSessionCookieById(sessionId, organization);
const recent = await getSessionCookieById({ sessionId, organization });
return getSession(recent.id, recent.token).then((response) => {
if (response?.session) {
return response.session;

View File

@@ -1,10 +1,9 @@
import { getBrandingSettings, getSession } from "@/lib/zitadel";
import Alert, { AlertType } from "@/ui/Alert";
import { getBrandingSettings, sessionService } from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import RegisterPasskey from "@/ui/RegisterPasskey";
import RegisterU2F from "@/ui/RegisterU2F";
import UserAvatar from "@/ui/UserAvatar";
import { getMostRecentCookieWithLoginname } from "@/utils/cookies";
import { loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,
@@ -13,19 +12,11 @@ export default async function Page({
}) {
const { loginName, organization, authRequestId } = searchParams;
const sessionFactors = await loadSession(loginName);
const sessionFactors = await loadMostRecentSession(sessionService, {
loginName,
organization,
});
async function loadSession(loginName?: string) {
const recent = await getMostRecentCookieWithLoginname(
loginName,
organization,
);
return getSession(recent.id, recent.token).then((response) => {
if (response?.session) {
return response.session;
}
});
}
const title = "Use your passkey to confirm it's really you";
const description =
"Your device will ask for your fingerprint, face, or screen lock";

View File

@@ -4,16 +4,8 @@ import VerifyEmailForm from "@/ui/VerifyEmailForm";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
export default async function Page({ searchParams }: { searchParams: any }) {
const {
userId,
sessionId,
code,
submit,
organization,
authRequestId,
loginname,
passwordset,
} = searchParams;
const { userId, sessionId, code, submit, organization, authRequestId } =
searchParams;
const branding = await getBrandingSettings(organization);

View File

@@ -1,12 +1,20 @@
import { listAuthenticationMethodTypes, listUsers } from "@/lib/zitadel";
import { ProviderSlug } from "@/lib/demos";
import {
getActiveIdentityProviders,
getLoginSettings,
listAuthenticationMethodTypes,
listUsers,
startIdentityProviderFlow,
} from "@/lib/zitadel";
import { createSessionForUserIdAndUpdateCookie } from "@/utils/session";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { loginName, authRequestId, organization } = body;
return listUsers(loginName, organization).then((users) => {
return listUsers(loginName, organization).then(async (users) => {
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
const userId = users.result[0].userId;
return createSessionForUserIdAndUpdateCookie(
@@ -37,6 +45,102 @@ export async function POST(request: NextRequest) {
return NextResponse.json(error, { status: 500 });
});
} else {
const loginSettings = await getLoginSettings(organization);
// TODO: check if allowDomainDiscovery has to be allowed too, to redirect to the register page
// user not found, check if register is enabled on organization
if (
loginSettings?.allowRegister &&
!loginSettings?.allowUsernamePassword
) {
// TODO redirect to loginname page with idp hint
const identityProviders = await getActiveIdentityProviders(
organization,
).then((resp) => {
return resp.identityProviders;
});
if (identityProviders.length === 1) {
const host = request.nextUrl.origin;
const identityProviderType = identityProviders[0].type;
let provider: string;
switch (identityProviderType) {
case IdentityProviderType.GITHUB:
provider = "github";
break;
case IdentityProviderType.GOOGLE:
provider = "google";
break;
case IdentityProviderType.AZURE_AD:
provider = "azure";
break;
case IdentityProviderType.SAML:
provider = "saml";
break;
case IdentityProviderType.OIDC:
provider = "oidc";
break;
default:
provider = "oidc";
break;
}
const params = new URLSearchParams();
if (authRequestId) {
params.set("authRequestId", authRequestId);
}
if (organization) {
params.set("organization", organization);
}
return startIdentityProviderFlow({
idpId: identityProviders[0].id,
urls: {
successUrl:
`${host}/idp/${provider}/success?` +
new URLSearchParams(params),
failureUrl:
`${host}/idp/${provider}/failure?` +
new URLSearchParams(params),
},
}).then((resp: any) => {
if (resp.authUrl) {
return NextResponse.json({ nextStep: resp.authUrl });
}
});
} else {
return NextResponse.json(
{ message: "Could not find user" },
{ status: 404 },
);
}
} else if (
loginSettings?.allowRegister &&
loginSettings?.allowUsernamePassword
) {
const params: any = { organization };
if (authRequestId) {
params.authRequestId = authRequestId;
}
if (loginName) {
params.email = loginName;
}
const registerUrl = new URL(
"/register?" + new URLSearchParams(params),
request.url,
);
return NextResponse.json({
nextStep: registerUrl,
status: 200,
});
}
return NextResponse.json(
{ message: "Could not find user" },
{ status: 404 },

View File

@@ -1,13 +1,12 @@
import {
SessionCookie,
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
} from "@/utils/cookies";
} from "@zitadel/next";
import { setSessionAndUpdateCookie } from "@/utils/session";
import { NextRequest, NextResponse, userAgent } from "next/server";
import { Checks } from "@zitadel/proto/zitadel/session/v2beta/session_service_pb";
import { PlainMessage } from "@zitadel/client2";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PlainMessage } from "@zitadel/client";
export async function POST(request: NextRequest) {
const body = await request.json();
@@ -16,12 +15,12 @@ export async function POST(request: NextRequest) {
const { loginName, sessionId, organization, authRequestId, code, method } =
body;
const recentPromise: Promise<SessionCookie> = sessionId
? getSessionCookieById(sessionId).catch((error) => {
const recentPromise = sessionId
? getSessionCookieById({ sessionId }).catch((error) => {
return Promise.reject(error);
})
: loginName
? getSessionCookieByLoginName(loginName, organization).catch(
? getSessionCookieByLoginName({ loginName, organization }).catch(
(error) => {
return Promise.reject(error);
},

View File

@@ -3,7 +3,7 @@ import {
getSession,
registerPasskey,
} from "@/lib/zitadel";
import { getSessionCookieById } from "@/utils/cookies";
import { getSessionCookieById } from "@zitadel/next";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
@@ -11,7 +11,7 @@ export async function POST(request: NextRequest) {
if (body) {
const { sessionId } = body;
const sessionCookie = await getSessionCookieById(sessionId);
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);

View File

@@ -1,5 +1,5 @@
import { getSession, verifyPasskeyRegistration } from "@/lib/zitadel";
import { getSessionCookieById } from "@/utils/cookies";
import { getSessionCookieById } from "@zitadel/next";
import { NextRequest, NextResponse, userAgent } from "next/server";
export async function POST(request: NextRequest) {
@@ -13,7 +13,7 @@ export async function POST(request: NextRequest) {
device.vendor || device.model ? ", " : ""
}${os.name}${os.name ? ", " : ""}${browser.name}`;
}
const sessionCookie = await getSessionCookieById(sessionId);
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);

View File

@@ -5,12 +5,11 @@ import {
listAuthenticationMethodTypes,
} from "@/lib/zitadel";
import {
SessionCookie,
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
removeSessionFromCookie,
} from "@/utils/cookies";
} from "@zitadel/next";
import {
createSessionAndUpdateCookie,
createSessionForIdpAndUpdateCookie,
@@ -76,12 +75,12 @@ export async function PUT(request: NextRequest) {
challenges,
} = body;
const recentPromise: Promise<SessionCookie> = sessionId
const recentPromise = sessionId
? getSessionCookieById(sessionId).catch((error) => {
return Promise.reject(error);
})
: loginName
? getSessionCookieByLoginName(loginName, organization).catch(
? getSessionCookieByLoginName({ loginName, organization }).catch(
(error) => {
return Promise.reject(error);
},
@@ -166,9 +165,9 @@ export async function PUT(request: NextRequest) {
*/
export async function DELETE(request: NextRequest) {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (id) {
const session = await getSessionCookieById(id);
const sessionId = searchParams.get("id");
if (sessionId) {
const session = await getSessionCookieById({ sessionId });
return deleteSession(session.id, session.token)
.then(() => {

View File

@@ -4,7 +4,7 @@ import {
registerPasskey,
registerU2F,
} from "@/lib/zitadel";
import { getSessionCookieById } from "@/utils/cookies";
import { getSessionCookieById } from "@zitadel/next";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
@@ -12,7 +12,7 @@ export async function POST(request: NextRequest) {
if (body) {
const { sessionId } = body;
const sessionCookie = await getSessionCookieById(sessionId);
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);

View File

@@ -1,8 +1,8 @@
import { getSession, verifyU2FRegistration } from "@/lib/zitadel";
import { getSessionCookieById } from "@/utils/cookies";
import { getSessionCookieById } from "@zitadel/next";
import { NextRequest, NextResponse, userAgent } from "next/server";
import { VerifyU2FRegistrationRequest } from "@zitadel/proto/zitadel/user/v2beta/user_service_pb";
import { PlainMessage } from "@zitadel/client2";
import { VerifyU2FRegistrationRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { PlainMessage } from "@zitadel/client";
export async function POST(request: NextRequest) {
const body = await request.json();
@@ -15,7 +15,7 @@ export async function POST(request: NextRequest) {
device.vendor || device.model ? ", " : ""
}${os.name}${os.name ? ", " : ""}${browser.name}`;
}
const sessionCookie = await getSessionCookieById(sessionId);
const sessionCookie = await getSessionCookieById({ sessionId });
const session = await getSession(sessionCookie.id, sessionCookie.token);

View File

@@ -1,16 +1,23 @@
export const dynamic = "force-dynamic";
export const revalidate = false;
export const fetchCache = "default-no-store";
import {
createCallback,
getActiveIdentityProviders,
getAuthRequest,
getOrgByDomain,
listSessions,
startIdentityProviderFlow,
} from "@/lib/zitadel";
import { SessionCookie, getAllSessions } from "@/utils/cookies";
import { getAllSessions } from "@zitadel/next";
import { NextRequest, NextResponse } from "next/server";
import { Session } from "@zitadel/proto/zitadel/session/v2beta/session_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import {
AuthRequest,
Prompt,
} from "@zitadel/proto/zitadel/oidc/v2beta/authorization_pb";
} from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
async function loadSessions(ids: string[]): Promise<Session[]> {
const response = await listSessions(
@@ -22,6 +29,7 @@ async function loadSessions(ids: string[]): Promise<Session[]> {
const ORG_SCOPE_REGEX = /urn:zitadel:iam:org:id:([0-9]+)/;
const ORG_DOMAIN_SCOPE_REGEX = /urn:zitadel:iam:org:domain:primary:(.+)/; // TODO: check regex for all domain character options
const IDP_SCOPE_REGEX = /urn:zitadel:iam:org:idp:id:(.+)/;
function findSession(
sessions: Session[],
@@ -48,7 +56,13 @@ export async function GET(request: NextRequest) {
const authRequestId = searchParams.get("authRequest");
const sessionId = searchParams.get("sessionId");
const sessionCookies: SessionCookie[] = await getAllSessions();
// TODO: find a better way to handle _rsc (react server components) requests and block them to avoid conflicts when creating oidc callback
const _rsc = searchParams.get("_rsc");
if (_rsc) {
return NextResponse.json({ error: "No _rsc supported" }, { status: 500 });
}
const sessionCookies = await getAllSessions();
const ids = sessionCookies.map((s) => s.id);
let sessions: Session[] = [];
if (ids && ids.length) {
@@ -76,36 +90,50 @@ export async function GET(request: NextRequest) {
);
if (cookie && cookie.id && cookie.token) {
console.log(`Found sessioncookie ${cookie.id}`);
const session = {
sessionId: cookie?.id,
sessionToken: cookie?.token,
};
const { callbackUrl } = await createCallback({
authRequestId,
callbackKind: {
case: "session",
value: session,
},
});
return NextResponse.redirect(callbackUrl);
// works not with _rsc request
try {
const { callbackUrl } = await createCallback({
authRequestId,
callbackKind: {
case: "session",
value: session,
},
});
if (callbackUrl) {
return NextResponse.redirect(callbackUrl);
} else {
return NextResponse.json(
{ error: "An error occurred!" },
{ status: 500 },
);
}
} catch (error) {
return NextResponse.json({ error }, { status: 500 });
}
}
}
}
if (authRequestId) {
console.log(`Login with authRequest: ${authRequestId}`);
const { authRequest } = await getAuthRequest({ authRequestId });
let organization = "";
let idpId = "";
if (authRequest?.scope) {
const orgScope = authRequest.scope.find((s: string) =>
ORG_SCOPE_REGEX.test(s),
);
const idpScope = authRequest.scope.find((s: string) =>
IDP_SCOPE_REGEX.test(s),
);
if (orgScope) {
const matched = ORG_SCOPE_REGEX.exec(orgScope);
organization = matched?.[1] ?? "";
@@ -123,6 +151,76 @@ export async function GET(request: NextRequest) {
}
}
}
if (idpScope) {
const matched = IDP_SCOPE_REGEX.exec(idpScope);
idpId = matched?.[1] ?? "";
const identityProviders = await getActiveIdentityProviders(
organization ? organization : undefined,
).then((resp) => {
return resp.identityProviders;
});
const idp = identityProviders.find((idp) => idp.id === idpId);
if (idp) {
const host = request.nextUrl.origin;
const identityProviderType = identityProviders[0].type;
let provider: string;
switch (identityProviderType) {
case IdentityProviderType.GITHUB:
provider = "github";
break;
case IdentityProviderType.GOOGLE:
provider = "google";
break;
case IdentityProviderType.AZURE_AD:
provider = "azure";
break;
case IdentityProviderType.SAML:
provider = "saml";
break;
case IdentityProviderType.OIDC:
provider = "oidc";
break;
default:
provider = "oidc";
break;
}
const params = new URLSearchParams();
if (authRequestId) {
params.set("authRequestId", authRequestId);
}
if (organization) {
params.set("organization", organization);
}
return startIdentityProviderFlow({
idpId,
urls: {
successUrl:
`${host}/idp/${provider}/success?` +
new URLSearchParams(params),
failureUrl:
`${host}/idp/${provider}/failure?` +
new URLSearchParams(params),
},
}).then((resp) => {
if (
resp.nextStep.value &&
typeof resp.nextStep.value === "string"
) {
return NextResponse.redirect(resp.nextStep.value);
}
});
}
}
}
const gotoAccounts = (): NextResponse<unknown> => {
@@ -227,6 +325,9 @@ export async function GET(request: NextRequest) {
if (callbackUrl) {
return NextResponse.redirect(callbackUrl);
} else {
console.log(
"could not create callback, redirect user to choose other account",
);
return gotoAccounts();
}
} catch (error) {

View File

@@ -1,54 +1,8 @@
import { demos } from "@/lib/demos";
import Link from "next/link";
import { redirect } from "next/navigation";
export default function Page() {
return (
<div className="rounded-lg bg-vc-border-gradient dark:bg-dark-vc-border-gradient p-px shadow-lg shadow-black/5 dark:shadow-black/20 mb-10">
<div className="rounded-lg bg-background-light-400 dark:bg-background-dark-500 px-8 py-12">
<div className="space-y-8">
<h1 className="text-xl font-medium">Pages</h1>
<div className="space-y-10">
{demos.map((section) => {
return (
<div key={section.name} className="space-y-5">
<div className="text-xs font-semibold uppercase tracking-wider text-gray-500">
{section.name}
</div>
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
{section.items.map((item) => {
return (
<Link
href={`/${item.slug}`}
key={item.name}
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">{item.name}</div>
{item.description ? (
<div className="line-clamp-3 text-sm text-text-light-secondary-500 dark:text-text-dark-secondary-500">
{item.description}
</div>
) : null}
</Link>
);
})}
</div>
</div>
);
})}
</div>
<div className="flex flex-col">
<div className="mb-5 text-xs font-semibold uppercase tracking-wider text-gray-500">
Deploy your own on Vercel
</div>
<a href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzitadel%2Ftypescript&root-directory=apps/login&env=ZITADEL_API_URL,ZITADEL_SERVICE_USER_ID,ZITADEL_SERVICE_USER_TOKEN&envDescription=Setup%20a%20service%20account%20with%20IAM_OWNER%20membership%20on%20your%20instance%20and%20provide%20its%20id%20and%20personal%20access%20token.&project-name=zitadel-login&repository-name=zitadel-login">
<img src="https://vercel.com/button" alt="Deploy with Vercel" />
</a>
</div>
</div>
</div>
</div>
);
// automatically redirect to loginname
if (process.env.DEBUG !== "true") {
redirect("/loginname");
}
}

View File

@@ -1,6 +1,6 @@
import { listSessions } from "@/lib/zitadel";
import { SessionCookie, getAllSessions } from "@/utils/cookies";
import { Session } from "@zitadel/proto/zitadel/session/v2beta/session_pb";
import { getAllSessions } from "@zitadel/next";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { NextRequest, NextResponse } from "next/server";
async function loadSessions(ids: string[]): Promise<Session[]> {
@@ -12,7 +12,7 @@ async function loadSessions(ids: string[]): Promise<Session[]> {
}
export async function GET(request: NextRequest) {
const sessionCookies: SessionCookie[] = await getAllSessions();
const sessionCookies = await getAllSessions();
const ids = sessionCookies.map((s) => s.id);
let sessions: Session[] = [];
if (ids && ids.length) {

View File

@@ -7,6 +7,7 @@ export type Item = {
export enum ProviderSlug {
GOOGLE = "google",
GITHUB = "github",
AZURE = "microsoft",
}
export const demos: { name: string; items: Item[] }[] = [

View File

@@ -1,24 +1,21 @@
"use server";
import { getMostRecentCookieWithLoginname } from "@/utils/cookies";
import { getSession, verifyTOTPRegistration } from "./zitadel";
import { loadMostRecentSession } from "@zitadel/next";
import { sessionService, verifyTOTPRegistration } from "./zitadel";
export async function verifyTOTP(
code: string,
loginName?: string,
organization?: string,
) {
return getMostRecentCookieWithLoginname(loginName, organization)
.then((recent) => {
return getSession(recent.id, recent.token).then((response) => {
return { session: response?.session, token: recent.token };
});
})
.then(({ session, token }) => {
if (session?.factors?.user?.id) {
return verifyTOTPRegistration(code, session.factors.user.id, token);
} else {
throw Error("No user id found in session.");
}
});
return loadMostRecentSession(sessionService, {
loginName,
organization,
}).then((session) => {
if (session?.factors?.user?.id) {
return verifyTOTPRegistration(code, session.factors.user.id);
} else {
throw Error("No user id found in session.");
}
});
}

View File

@@ -4,26 +4,21 @@ import {
createSettingsServiceClient,
createUserServiceClient,
makeReqCtx,
} from "@zitadel/client2/v2beta";
import { createManagementServiceClient } from "@zitadel/client2/v1";
} from "@zitadel/client/v2";
import { createManagementServiceClient } from "@zitadel/client/v1";
import { createServerTransport } from "@zitadel/node";
import { Checks } from "@zitadel/proto/zitadel/session/v2beta/session_service_pb";
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2beta/challenge_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import {
RetrieveIdentityProviderIntentRequest,
VerifyU2FRegistrationRequest,
} from "@zitadel/proto/zitadel/user/v2beta/user_service_pb";
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2beta/oidc_service_pb";
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2beta/object_pb";
import type { RedirectURLs } from "@zitadel/proto/zitadel/user/v2beta/idp_pb";
import { PlainMessage } from "@zitadel/client2";
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
import type { RedirectURLs } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import { ProviderSlug } from "./demos";
import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2beta/user_service_pb";
import {
IDPInformation,
IDPLink,
} from "@zitadel/proto/zitadel/user/v2beta/idp_pb";
import { PartialMessage } from "@zitadel/client2";
import { PlainMessage } from "@zitadel/client";
const SESSION_LIFETIME_S = 3000;
@@ -230,24 +225,7 @@ export async function addHumanUser({
});
}
export async function verifyTOTPRegistration(
code: string,
userId: string,
token?: string,
) {
// let userService;
// if (token) {
// const authConfig: ZitadelServerOptions = {
// name: "zitadel login",
// apiUrl: process.env.ZITADEL_API_URL ?? "",
// token: token,
// };
//
// const sessionUser = initializeServer(authConfig);
// userService = user.getUser(sessionUser);
// } else {
// userService = user.getUser(server);
// }
export async function verifyTOTPRegistration(code: string, userId: string) {
return userService.verifyTOTPRegistration({ code, userId }, {});
}
@@ -298,6 +276,14 @@ export async function getOrgByDomain(domain: string) {
return managementService.getOrgByDomainGlobal({ domain }, {});
}
export const PROVIDER_NAME_MAPPING: {
[provider: string]: string;
} = {
[ProviderSlug.GOOGLE]: "Google",
[ProviderSlug.GITHUB]: "GitHub",
[ProviderSlug.AZURE]: "Microft",
};
export async function startIdentityProviderFlow({
idpId,
urls,
@@ -535,6 +521,13 @@ export async function verifyU2FRegistration(
return userService.verifyU2FRegistration(request, {});
}
export async function getActiveIdentityProviders(orgId?: string) {
return settingsService.getActiveIdentityProviders(
{ ctx: makeReqCtx(orgId) },
{},
);
}
/**
*
* @param userId the id of the user where the email should be set

View File

@@ -19,6 +19,10 @@ export function middleware(request: NextRequest) {
// this is a workaround for the next.js server not forwarding the host header
requestHeaders.set("x-zitadel-forwarded", `host="${request.nextUrl.host}"`);
// requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`);
// this is a workaround for the next.js server not forwarding the host header
// requestHeaders.set("x-zitadel-instance-host", `${INSTANCE}`);
const responseHeaders = new Headers();
responseHeaders.set("Access-Control-Allow-Origin", "*");

View File

@@ -2,6 +2,7 @@
import { ColorShade, getColorHash } from "@/utils/colors";
import { useTheme } from "next-themes";
import Image from "next/image";
interface AvatarProps {
name: string | null | undefined;
@@ -71,14 +72,17 @@ export function Avatar({
: size === "base"
? "w-[38px] h-[38px] font-bold"
: size === "small"
? "w-[32px] h-[32px] font-bold text-[13px]"
: ""
? "!w-[32px] !h-[32px] font-bold text-[13px]"
: "w-12 h-12"
}`}
style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark}
>
{imageUrl ? (
<img
className="border border-divider-light dark:border-divider-dark rounded-full w-12 h-12"
<Image
height={48}
width={48}
alt="avatar"
className="w-full h-full border border-divider-light dark:border-divider-dark rounded-full"
src={imageUrl}
/>
) : (

View File

@@ -5,7 +5,7 @@ import { BadgeState, StateBadge } from "./StateBadge";
import clsx from "clsx";
import { CheckIcon } from "@heroicons/react/24/outline";
import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2beta/user_service_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
type Props = {
loginName?: string;

View File

@@ -1,8 +1,8 @@
"use client";
import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2beta/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2beta/user_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
type Props = {
loginName?: string;

View File

@@ -4,7 +4,7 @@ import React from "react";
import { Logo } from "@/ui/Logo";
import ThemeWrapper from "./ThemeWrapper";
import { LayoutProviders } from "./LayoutProviders";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2beta/branding_settings_pb";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
export default function DynamicTheme({
branding,

View File

@@ -1,4 +1,5 @@
"use client";
import { ZitadelReactProvider } from "@zitadel/react";
import { useTheme } from "next-themes";

View File

@@ -8,9 +8,9 @@ import { Spinner } from "./Spinner";
import { useForm } from "react-hook-form";
import { TextInput } from "./Input";
import BackButton from "./BackButton";
import { Checks } from "@zitadel/proto/zitadel/session/v2beta/session_service_pb";
import { PlainMessage } from "@zitadel/client2";
import { Challenges } from "@zitadel/proto/zitadel/session/v2beta/challenge_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PlainMessage } from "@zitadel/client";
import { Challenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
// either loginName or sessionId must be provided
type Props = {
@@ -236,7 +236,9 @@ export default function LoginOTP({
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit((e) => setCodeAndContinue(e, organization))}
onClick={handleSubmit((e) => {
setCodeAndContinue(e, organization);
})}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
continue

View File

@@ -7,7 +7,7 @@ import { Button, ButtonVariants } from "./Button";
import Alert from "./Alert";
import { Spinner } from "./Spinner";
import BackButton from "./BackButton";
import { Checks } from "@zitadel/proto/zitadel/session/v2beta/session_service_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
// either loginName or sessionId must be provided
type Props = {

View File

@@ -4,7 +4,7 @@ import {
symbolValidator,
upperCaseValidator,
} from "@/utils/validators";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2beta/password_settings_pb";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
type Props = {
passwordComplexitySettings: PasswordComplexitySettings;

View File

@@ -8,9 +8,9 @@ import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner";
import Alert from "./Alert";
import BackButton from "./BackButton";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2beta/login_settings_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2beta/session_service_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2beta/user_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
type Inputs = {
password: string;

View File

@@ -2,7 +2,7 @@
import React, { useState } from "react";
import Link from "next/link";
import { Checkbox } from "./Checkbox";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2beta/legal_settings_pb";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
type Props = {
legal: LegalAndSupportSettings;

View File

@@ -12,7 +12,8 @@ import AuthenticationMethodRadio, {
} from "./AuthenticationMethodRadio";
import Alert from "./Alert";
import BackButton from "./BackButton";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2beta/legal_settings_pb";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
import { first } from "node_modules/cypress/types/lodash";
type Inputs =
| {
@@ -24,17 +25,28 @@ type Inputs =
type Props = {
legal: LegalAndSupportSettings;
firstname?: string;
lastname?: string;
email?: string;
organization?: string;
authRequestId?: string;
};
export default function RegisterFormWithoutPassword({
legal,
email,
firstname,
lastname,
organization,
authRequestId,
}: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
defaultValues: {
email: email ?? "",
firstName: firstname ?? "",
lastname: lastname ?? "",
},
});
const [loading, setLoading] = useState<boolean>(false);

View File

@@ -8,7 +8,7 @@ import { Spinner } from "./Spinner";
import Alert from "./Alert";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/utils/base64";
import BackButton from "./BackButton";
import { RegisterPasskeyResponse } from "@zitadel/proto/zitadel/user/v2beta/user_service_pb";
import { RegisterPasskeyResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
type Inputs = {};

View File

@@ -8,7 +8,7 @@ import { Spinner } from "./Spinner";
import Alert from "./Alert";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/utils/base64";
import BackButton from "./BackButton";
import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2beta/user_service_pb";
import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
type Inputs = {};

View File

@@ -5,7 +5,7 @@ import { useState } from "react";
import { Avatar } from "./Avatar";
import moment from "moment";
import { XCircleIcon } from "@heroicons/react/24/outline";
import { Session } from "@zitadel/proto/zitadel/session/v2beta/session_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
export default function SessionItem({
session,
@@ -54,33 +54,40 @@ export default function SessionItem({
return (
<Link
prefetch={false}
href={
validUser
validUser && authRequestId
? `/login?` +
new URLSearchParams(
authRequestId
? {
// loginName: session.factors?.user?.loginName as string,
sessionId: session.id,
authRequest: authRequestId,
}
: {
loginName: session.factors?.user?.loginName as string,
},
)
: `/loginname?` +
new URLSearchParams(
authRequestId
? {
loginName: session.factors?.user?.loginName as string,
submit: "true",
authRequestId,
}
: {
loginName: session.factors?.user?.loginName as string,
submit: "true",
},
)
new URLSearchParams({
// loginName: session.factors?.user?.loginName as string,
sessionId: session.id,
authRequest: authRequestId,
})
: !validUser
? `/loginname?` +
new URLSearchParams(
authRequestId
? {
loginName: session.factors?.user?.loginName as string,
submit: "true",
authRequestId,
}
: {
loginName: session.factors?.user?.loginName as string,
submit: "true",
},
)
: "/signedin?" +
new URLSearchParams(
authRequestId
? {
loginName: session.factors?.user?.loginName as string,
authRequestId,
}
: {
loginName: session.factors?.user?.loginName as string,
},
)
}
className="group flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light hover:shadow-lg dark:hover:bg-white/10 py-2 px-4 rounded-md transition-all"
>
@@ -114,12 +121,11 @@ export default function SessionItem({
<XCircleIcon
className="hidden group-hover:block h-5 w-5 transition-all opacity-50 hover:opacity-100"
onClick={(event) => {
event.preventDefault();
onClick={() =>
clearSession(session.id).then(() => {
reload();
});
}}
})
}
/>
</div>
</Link>

View File

@@ -3,7 +3,7 @@
import SessionItem from "./SessionItem";
import Alert from "./Alert";
import { useEffect, useState } from "react";
import { Session } from "@zitadel/proto/zitadel/session/v2beta/session_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
type Props = {
sessions: Session[];

View File

@@ -14,7 +14,7 @@ import {
import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner";
import Alert from "./Alert";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2beta/password_settings_pb";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
type Inputs =
| {
@@ -42,6 +42,11 @@ export default function SetPasswordForm({
}: Props) {
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
mode: "onBlur",
defaultValues: {
email: email ?? "",
firstname: firstname ?? "",
lastname: lastname ?? "",
},
});
const [loading, setLoading] = useState<boolean>(false);

View File

@@ -11,7 +11,7 @@ import { useRouter } from "next/navigation";
import { ProviderSlug } from "@/lib/demos";
import Alert from "./Alert";
import BackButton from "./BackButton";
import { IdentityProvider } from "@zitadel/proto/zitadel/settings/v2beta/login_settings_pb";
import { IdentityProvider } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
export interface SignInWithIDPProps {
children?: ReactNode;
@@ -19,18 +19,13 @@ export interface SignInWithIDPProps {
identityProviders: IdentityProvider[];
authRequestId?: string;
organization?: string;
startIDPFlowPath?: (idpId: string) => string;
}
const START_IDP_FLOW_PATH = (idpId: string) =>
`/v2beta/users/idps/${idpId}/start`;
export function SignInWithIDP({
host,
identityProviders,
authRequestId,
organization,
startIDPFlowPath = START_IDP_FLOW_PATH,
}: SignInWithIDPProps) {
// TODO: remove casting when bufbuild/protobuf-es@v2 is released
identityProviders = identityProviders.map((idp) =>
@@ -107,7 +102,13 @@ export function SignInWithIDP({
return (
<SignInWithAzureAD
key={`idp-${i}`}
onClick={() => alert("TODO: unimplemented")}
onClick={() =>
startFlow(idp.id, ProviderSlug.AZURE).then(
({ authUrl }) => {
router.push(authUrl);
},
)
}
></SignInWithAzureAD>
);
case 10: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_GOOGLE:
@@ -148,10 +149,6 @@ export function SignInWithIDP({
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center pt-4">
<BackButton />
<span className="flex-grow"></span>
</div>
</div>
);
}

View File

@@ -2,8 +2,8 @@
import { setTheme } from "@/utils/colors";
import { useEffect } from "react";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2beta/branding_settings_pb";
import { PartialMessage } from "@zitadel/client2";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { PartialMessage } from "@zitadel/client";
type Props = {
branding: PartialMessage<BrandingSettings> | undefined;

View File

@@ -10,7 +10,8 @@ import Alert from "./Alert";
import {
LoginSettings,
PasskeysType,
} from "@zitadel/proto/zitadel/settings/v2beta/login_settings_pb";
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import BackButton from "./BackButton";
type Inputs = {
loginName: string;
@@ -23,6 +24,7 @@ type Props = {
organization?: string;
submit: boolean;
allowRegister: boolean;
children?: React.ReactNode;
};
export default function UsernameForm({
@@ -32,6 +34,7 @@ export default function UsernameForm({
organization,
submit,
allowRegister,
children,
}: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
@@ -82,9 +85,10 @@ export default function UsernameForm({
values: Inputs,
organization?: string,
) {
console.log(loginSettings);
return submitLoginName(values, organization).then((response) => {
if (response.authMethodTypes.length == 1) {
if (response.nextStep) {
return router.push(response.nextStep);
} else if (response.authMethodTypes.length == 1) {
const method = response.authMethodTypes[0];
switch (method) {
case 1: // user has only password as auth method
@@ -92,8 +96,11 @@ export default function UsernameForm({
loginName: response.factors.user.loginName,
};
if (organization) {
paramsPassword.organization = organization;
// TODO: does this have to be checked in loginSettings.allowDomainDiscovery
if (organization || response.factors.user.organizationId) {
paramsPassword.organization =
organization ?? response.factors.user.organizationId;
}
if (
@@ -117,8 +124,10 @@ export default function UsernameForm({
if (authRequestId) {
paramsPasskey.authRequestId = authRequestId;
}
if (organization) {
paramsPasskey.organization = organization;
if (organization || response.factors.user.organizationId) {
paramsPasskey.organization =
organization ?? response.factors.user.organizationId;
}
return router.push(
@@ -134,8 +143,10 @@ export default function UsernameForm({
if (authRequestId) {
paramsPasskeyDefault.authRequestId = authRequestId;
}
if (organization) {
paramsPasskeyDefault.organization = organization;
if (organization || response.factors.user.organizationId) {
paramsPasskeyDefault.organization =
organization ?? response.factors.user.organizationId;
}
return router.push(
@@ -161,8 +172,9 @@ export default function UsernameForm({
passkeyParams.authRequestId = authRequestId;
}
if (organization) {
passkeyParams.organization = organization;
if (organization || response.factors.user.organizationId) {
passkeyParams.organization =
organization ?? response.factors.user.organizationId;
}
return router.push(
@@ -180,8 +192,9 @@ export default function UsernameForm({
paramsPasswordDefault.authRequestId = authRequestId;
}
if (organization) {
paramsPasswordDefault.organization = organization;
if (organization || response.factors.user.organizationId) {
paramsPasswordDefault.organization =
organization ?? response.factors.user.organizationId;
}
return router.push(
@@ -210,6 +223,16 @@ export default function UsernameForm({
{...register("loginName", { required: "This field is required" })}
label="Loginname"
/>
{allowRegister && (
<button
className="transition-all text-sm hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={() => router.push("/register")}
type="button"
disabled={loading}
>
Register new user
</button>
)}
</div>
{error && (
@@ -218,17 +241,10 @@ export default function UsernameForm({
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
{allowRegister && (
<Button
type="button"
className="self-end"
variant={ButtonVariants.Secondary}
onClick={() => router.push("/register")}
>
register
</Button>
)}
<div className="pt-6 pb-4">{children}</div>
<div className="mt-4 flex w-full flex-row items-center">
<BackButton />
<span className="flex-grow"></span>
<Button
type="submit"

View File

@@ -1,6 +1,6 @@
import tinycolor from "tinycolor2";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2beta/branding_settings_pb";
import { PartialMessage } from "@zitadel/client2";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { PartialMessage } from "@zitadel/client";
export interface Color {
name: string;

View File

@@ -1,284 +0,0 @@
"use server";
import { cookies } from "next/headers";
export type SessionCookie = {
id: string;
token: string;
loginName: string;
organization?: string;
creationDate: string;
expirationDate: string;
changeDate: string;
authRequestId?: string; // if its linked to an OIDC flow
};
function setSessionHttpOnlyCookie(sessions: SessionCookie[]) {
const cookiesList = cookies();
// @ts-ignore
return cookiesList.set({
name: "sessions",
value: JSON.stringify(sessions),
httpOnly: true,
path: "/",
});
}
export async function addSessionToCookie(
session: SessionCookie,
cleanup: boolean = false,
): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
let currentSessions: SessionCookie[] = stringifiedCookie?.value
? JSON.parse(stringifiedCookie?.value)
: [];
const index = currentSessions.findIndex(
(s) => s.loginName === session.loginName,
);
if (index > -1) {
currentSessions[index] = session;
} else {
currentSessions = [...currentSessions, session];
}
if (cleanup) {
const now = new Date();
const filteredSessions = currentSessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true,
);
return setSessionHttpOnlyCookie(filteredSessions);
} else {
return setSessionHttpOnlyCookie(currentSessions);
}
}
export async function updateSessionCookie(
id: string,
session: SessionCookie,
cleanup: boolean = false,
): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
const sessions: SessionCookie[] = stringifiedCookie?.value
? JSON.parse(stringifiedCookie?.value)
: [session];
const foundIndex = sessions.findIndex((session) => session.id === id);
if (foundIndex > -1) {
sessions[foundIndex] = session;
if (cleanup) {
const now = new Date();
const filteredSessions = sessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true,
);
return setSessionHttpOnlyCookie(filteredSessions);
} else {
return setSessionHttpOnlyCookie(sessions);
}
} else {
throw "updateSessionCookie: session id now found";
}
}
export async function removeSessionFromCookie(
session: SessionCookie,
cleanup: boolean = false,
): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
const sessions: SessionCookie[] = stringifiedCookie?.value
? JSON.parse(stringifiedCookie?.value)
: [session];
const reducedSessions = sessions.filter((s) => s.id !== session.id);
if (cleanup) {
const now = new Date();
const filteredSessions = reducedSessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true,
);
return setSessionHttpOnlyCookie(filteredSessions);
} else {
return setSessionHttpOnlyCookie(reducedSessions);
}
}
export async function getMostRecentSessionCookie(): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value);
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("no session cookie found");
}
}
export async function getSessionCookieById(
id: string,
organization?: string,
): Promise<SessionCookie> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value);
const found = sessions.find((s) =>
organization
? s.organization === organization && s.id === id
: s.id === id,
);
if (found) {
return found;
} else {
return Promise.reject();
}
} else {
return Promise.reject();
}
}
export async function getSessionCookieByLoginName(
loginName: string,
organization?: string,
): Promise<SessionCookie> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value);
const found = sessions.find((s) =>
organization
? s.organization === organization && s.loginName === loginName
: s.loginName === loginName,
);
if (found) {
return found;
} else {
return Promise.reject("no cookie found with loginName: " + loginName);
}
} else {
return Promise.reject("no session cookie found");
}
}
/**
*
* @param cleanup when true, removes all expired sessions, default true
* @returns Session Cookies
*/
export async function getAllSessionCookieIds(
cleanup: boolean = false,
): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value);
if (cleanup) {
const now = new Date();
return sessions
.filter((session) =>
session.expirationDate
? new Date(session.expirationDate) > now
: true,
)
.map((session) => session.id);
} else {
return sessions.map((session) => session.id);
}
} else {
return [];
}
}
/**
*
* @param cleanup when true, removes all expired sessions, default true
* @returns Session Cookies
*/
export async function getAllSessions(
cleanup: boolean = false,
): Promise<SessionCookie[]> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value);
if (cleanup) {
const now = new Date();
return sessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true,
);
} else {
return sessions;
}
} else {
return [];
}
}
/**
* Returns most recent session filtered by optinal loginName
* @param loginName
* @returns most recent session
*/
export async function getMostRecentCookieWithLoginname(
loginName?: string,
organization?: string,
): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value);
let filtered = sessions.filter((cookie) => {
return !!loginName ? cookie.loginName === loginName : true;
});
if (organization) {
filtered = filtered.filter((cookie) => {
return cookie.organization === organization;
});
}
const latest =
filtered && filtered.length
? filtered.reduce((prev, current) => {
return new Date(prev.changeDate).getTime() >
new Date(current.changeDate).getTime()
? prev
: current;
})
: undefined;
if (latest) {
return latest;
} else {
console.error("sessions", sessions, loginName, organization);
return Promise.reject("Could not get the context or retrieve a session");
}
} else {
return Promise.reject("Could not read session cookie");
}
}
export async function clearSessions() {}

View File

@@ -6,18 +6,25 @@ import {
getSession,
setSession,
} from "@/lib/zitadel";
import {
SessionCookie,
addSessionToCookie,
updateSessionCookie,
} from "./cookies";
import { addSessionToCookie, updateSessionCookie } from "@zitadel/next";
import {
Challenges,
RequestChallenges,
} from "@zitadel/proto/zitadel/session/v2beta/challenge_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2beta/session_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2beta/session_service_pb";
import { PlainMessage } from "@zitadel/client2";
} from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PlainMessage } from "@zitadel/client";
type CustomCookieData = {
id: string;
token: string;
loginName: string;
organization?: string;
creationDate: string;
expirationDate: string;
changeDate: string;
authRequestId?: string; // if its linked to an OIDC flow
};
export async function createSessionAndUpdateCookie(
loginName: string,
@@ -43,7 +50,7 @@ export async function createSessionAndUpdateCookie(
createdSession.sessionToken,
).then((response) => {
if (response?.session && response.session?.factors?.user?.loginName) {
const sessionCookie: SessionCookie = {
const sessionCookie: any = {
id: createdSession.sessionId,
token: createdSession.sessionToken,
creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`,
@@ -61,7 +68,7 @@ export async function createSessionAndUpdateCookie(
sessionCookie.organization = organization;
}
return addSessionToCookie(sessionCookie).then(() => {
return addSessionToCookie<CustomCookieData>(sessionCookie).then(() => {
return response.session as Session;
});
} else {
@@ -91,13 +98,12 @@ export async function createSessionForUserIdAndUpdateCookie(
);
if (createdSession) {
console.log("cs", createdSession);
return getSession(
createdSession.sessionId,
createdSession.sessionToken,
).then((response) => {
if (response?.session && response.session?.factors?.user?.loginName) {
const sessionCookie: SessionCookie = {
const sessionCookie: any = {
id: createdSession.sessionId,
token: createdSession.sessionToken,
creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`,
@@ -147,7 +153,7 @@ export async function createSessionForIdpAndUpdateCookie(
createdSession.sessionToken,
).then((response) => {
if (response?.session && response.session?.factors?.user?.loginName) {
const sessionCookie: SessionCookie = {
const sessionCookie: any = {
id: createdSession.sessionId,
token: createdSession.sessionToken,
creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`,
@@ -182,7 +188,7 @@ export type SessionWithChallenges = Session & {
};
export async function setSessionAndUpdateCookie(
recentCookie: SessionCookie,
recentCookie: CustomCookieData,
checks: PlainMessage<Checks>,
challenges: RequestChallenges | undefined,
authRequestId: string | undefined,
@@ -194,7 +200,7 @@ export async function setSessionAndUpdateCookie(
checks,
).then((updatedSession) => {
if (updatedSession) {
const sessionCookie: SessionCookie = {
const sessionCookie: CustomCookieData = {
id: recentCookie.id,
token: updatedSession.sessionToken,
creationDate: recentCookie.creationDate,
@@ -212,7 +218,7 @@ export async function setSessionAndUpdateCookie(
(response) => {
if (response?.session && response.session.factors?.user?.loginName) {
const { session } = response;
const newCookie: SessionCookie = {
const newCookie: CustomCookieData = {
id: sessionCookie.id,
token: updatedSession.sessionToken,
creationDate: sessionCookie.creationDate,

View File

@@ -1,4 +1,4 @@
const sharedConfig = require("zitadel-tailwind-config/tailwind.config.js");
const sharedConfig = require("zitadel-tailwind-config/tailwind.config.mjs");
let colors = {
background: { light: { contrast: {} }, dark: { contrast: {} } },

View File

@@ -1,6 +1,6 @@
{
"extends": ["//"],
"pipeline": {
"tasks": {
"build": {
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"dependsOn": ["^build"]
@@ -8,29 +8,33 @@
"test": {
"dependsOn": [
"@zitadel/node#build",
"@zitadel/client2#build",
"@zitadel/react#build"
"@zitadel/client#build",
"@zitadel/react#build",
"@zitadel/next#build"
]
},
"test:integration": {
"dependsOn": [
"@zitadel/node#build",
"@zitadel/client2#build",
"@zitadel/react#build"
"@zitadel/client#build",
"@zitadel/react#build",
"@zitadel/next#build"
]
},
"test:unit": {
"dependsOn": [
"@zitadel/node#build",
"@zitadel/client2#build",
"@zitadel/react#build"
"@zitadel/client#build",
"@zitadel/react#build",
"@zitadel/next#build"
]
},
"test:watch": {
"dependsOn": [
"@zitadel/node#build",
"@zitadel/client2#build",
"@zitadel/react#build"
"@zitadel/client#build",
"@zitadel/react#build",
"@zitadel/next#build"
]
}
}