Merge branch 'main' into main

This commit is contained in:
Max Peintner
2024-11-27 15:47:49 +01:00
committed by GitHub
79 changed files with 1920 additions and 1039 deletions

View File

@@ -5,12 +5,12 @@ const passwordConfirmField = "password-confirm-text-input";
export async function registerUserScreenPassword(page: Page, firstname: string, lastname: string, email: string) {
await registerUserScreen(page, firstname, lastname, email);
await page.getByTestId("Password-radio").click();
await page.getByTestId("password-radio").click();
}
export async function registerUserScreenPasskey(page: Page, firstname: string, lastname: string, email: string) {
await registerUserScreen(page, firstname, lastname, email);
await page.getByTestId("Passkeys-radio").click();
await page.getByTestId("passkey-radio").click();
}
export async function registerPasswordScreen(page: Page, password1: string, password2: string) {

View File

@@ -10,6 +10,16 @@ describe("register", () => {
result: [{ id: "256088834543534543" }],
},
});
stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", {
data: {
settings: {
passkeysType: 1,
allowRegister: true,
allowUsernamePassword: true,
defaultRedirectUri: "",
},
},
});
stub("zitadel.user.v2.UserService", "AddHumanUser", {
data: {
userId: "221394658884845598",
@@ -53,9 +63,11 @@ describe("register", () => {
it("should redirect a user who selects passwordless on register to /passkey/set", () => {
cy.visit("/register");
cy.get('input[autocomplete="firstname"]').focus().type("John");
cy.get('input[autocomplete="lastname"]').focus().type("Doe");
cy.get('input[autocomplete="email"]').focus().type("john@zitadel.com");
cy.get('input[data-testid="firstname-text-input"]').focus().type("John");
cy.get('input[data-testid="lastname-text-input"]').focus().type("Doe");
cy.get('input[data-testid="email-text-input"]')
.focus()
.type("john@zitadel.com");
cy.get('input[type="checkbox"][value="privacypolicy"]').check();
cy.get('input[type="checkbox"][value="tos"]').check();
cy.get('button[type="submit"]').click();

View File

@@ -122,6 +122,18 @@
}
},
"register": {
"methods": {
"passkey": "Passkey",
"password": "Password"
},
"disabled": {
"title": "Registrierung deaktiviert",
"description": "Die Registrierung ist deaktiviert. Bitte wenden Sie sich an den Administrator."
},
"missingdata": {
"title": "Registrierung fehlgeschlagen",
"description": "Einige Daten fehlen. Bitte überprüfen Sie Ihre Eingaben."
},
"title": "Registrieren",
"description": "Erstellen Sie Ihr ZITADEL-Konto.",
"selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten",
@@ -151,7 +163,8 @@
},
"signedin": {
"title": "Willkommen {user}!",
"description": "Sie sind angemeldet."
"description": "Sie sind angemeldet.",
"continue": "Weiter"
},
"verify": {
"userIdMissing": "Keine Benutzer-ID angegeben!",

View File

@@ -122,6 +122,18 @@
}
},
"register": {
"methods": {
"passkey": "Passkey",
"password": "Password"
},
"disabled": {
"title": "Registration disabled",
"description": "The registration is disabled. Please contact your administrator."
},
"missingdata": {
"title": "Missing data",
"description": "Provide email, first and last name to register."
},
"title": "Register",
"description": "Create your ZITADEL account.",
"selectMethod": "Select the method you would like to authenticate",
@@ -151,7 +163,8 @@
},
"signedin": {
"title": "Welcome {user}!",
"description": "You are signed in."
"description": "You are signed in.",
"continue": "Continue"
},
"verify": {
"userIdMissing": "No userId provided!",

View File

@@ -122,6 +122,18 @@
}
},
"register": {
"methods": {
"passkey": "Clave de acceso",
"password": "Contraseña"
},
"disabled": {
"title": "Registro deshabilitado",
"description": "Registrarse está deshabilitado en este momento."
},
"missingdata": {
"title": "Datos faltantes",
"description": "No se proporcionaron datos suficientes para el registro."
},
"title": "Registrarse",
"description": "Crea tu cuenta ZITADEL.",
"selectMethod": "Selecciona el método con el que deseas autenticarte",
@@ -151,7 +163,8 @@
},
"signedin": {
"title": "¡Bienvenido {user}!",
"description": "Has iniciado sesión."
"description": "Has iniciado sesión.",
"continue": "Continuar"
},
"verify": {
"userIdMissing": "¡No se proporcionó userId!",

View File

@@ -122,6 +122,18 @@
}
},
"register": {
"methods": {
"passkey": "Passkey",
"password": "Password"
},
"disabled": {
"title": "Registration disabled",
"description": "Registrazione disabilitata. Contatta l'amministratore di sistema per assistenza."
},
"missingdata": {
"title": "Registrazione",
"description": "Inserisci i tuoi dati per registrarti."
},
"title": "Registrati",
"description": "Crea il tuo account ZITADEL.",
"selectMethod": "Seleziona il metodo con cui desideri autenticarti",
@@ -151,7 +163,8 @@
},
"signedin": {
"title": "Benvenuto {user}!",
"description": "Sei connesso."
"description": "Sei connesso.",
"continue": "Continua"
},
"verify": {
"userIdMissing": "Nessun userId fornito!",

View File

@@ -2,7 +2,15 @@
{
"service": "zitadel.settings.v2.SettingsService",
"method": "GetBrandingSettings",
"out": {}
"out": {
"data": {
"settings": {
"darkTheme": {
"backgroundColor": "#ff0000"
}
}
}
}
},
{
"service": "zitadel.settings.v2.SettingsService",

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

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

View File

@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"dev": "next dev --turbopack",
"test": "concurrently --timings --kill-others-on-fail 'npm:test:unit' 'npm:test:integration'",
"test:watch": "concurrently --kill-others 'npm:test:unit:watch' 'npm:test:integration:watch'",
"test:unit": "vitest",
@@ -45,13 +45,13 @@
"copy-to-clipboard": "^3.3.3",
"deepmerge": "^4.3.1",
"moment": "^2.29.4",
"next": "14.2.14",
"next-intl": "^3.20.0",
"next": "15.0.4-canary.23",
"next-intl": "^3.25.1",
"next-themes": "^0.2.1",
"nice-grpc": "2.0.1",
"qrcode.react": "^3.1.0",
"react": "^18.3.1",
"react-dom": "18.3.1",
"react": "19.0.0-rc-66855b96-20241106",
"react-dom": "19.0.0-rc-66855b96-20241106",
"react-hook-form": "7.39.5",
"swr": "^2.2.0",
"tinycolor2": "1.4.2"
@@ -62,8 +62,8 @@
"@testing-library/react": "^16.0.1",
"@types/ms": "0.7.34",
"@types/node": "22.9.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@types/tinycolor2": "1.4.3",
"@types/uuid": "^10.0.0",
"@vercel/git-hooks": "1.0.0",

View File

@@ -116,7 +116,7 @@ If the user has set up an additional **single** second factor, it is redirected
**NO MFA, FORCE MFA:** If no MFA method is available, and the settings force MFA, the user is sent to `/mfa/set` which prompts to setup a second factor.
**PROMPT PASSKEY** If the settings do not enforce MFA, we check if passkeys are allowed with `loginSettings?.passkeysType === PasskeysType.ALLOWED` and redirect the user to `/passkey/set` if no passkeys are setup. This step can be skipped.
**PROMPT PASSKEY** If the settings do not enforce MFA, we check if passkeys are allowed with `loginSettings?.passkeysType == PasskeysType.ALLOWED` and redirect the user to `/passkey/set` if no passkeys are setup. This step can be skipped.
If none of the previous conditions apply, we continue to sign in.
@@ -386,3 +386,10 @@ In future, self service options to jump to are shown below, like:
- logout
> NOTE: This page has to be explicitly enabled or act as a fallback if no default redirect is set.
## Currently NOT Supported
- loginSettings.disableLoginWithEmail
- loginSettings.disableLoginWithPhone
- loginSettings.allowExternalIdp - this will be deprecated with the new login as it can be determined by the available IDPs
- loginSettings.forceMfaLocalOnly

View File

@@ -20,11 +20,10 @@ async function loadSessions() {
}
}
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "accounts" });

View File

@@ -15,11 +15,10 @@ import {
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "authenticator" });
const tError = await getTranslations({ locale, namespace: "error" });

View File

@@ -12,13 +12,11 @@ const PROVIDER_NAME_MAPPING: {
[IdentityProviderType.AZURE_AD]: "Microsoft",
};
export default async function Page({
searchParams,
params,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
params: { provider: string };
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
params: Promise<{ provider: string }>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });

View File

@@ -29,13 +29,12 @@ async function loginFailed(branding?: BrandingSettings) {
</DynamicTheme>
);
}
export default async function Page({
searchParams,
params,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
params: { provider: string };
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
params: Promise<{ provider: string }>;
}) {
const params = await props.params;
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
const { id, token, authRequestId, organization } = searchParams;

View File

@@ -12,11 +12,10 @@ function getIdentityProviders(orgId?: string) {
});
}
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });

View File

@@ -9,11 +9,10 @@ import {
} from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "invite" });

View File

@@ -7,11 +7,10 @@ import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale, getTranslations } from "next-intl/server";
import Link from "next/link";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "invite" });

View File

@@ -1,39 +1,46 @@
import "@/styles/globals.scss";
import { LanguageProvider } from "@/components/language-provider";
import { LanguageSwitcher } from "@/components/language-switcher";
import { Skeleton } from "@/components/skeleton";
import { Theme } from "@/components/theme";
import { ThemeProvider } from "@/components/theme-provider";
import { Analytics } from "@vercel/analytics/react";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages } from "next-intl/server";
import { Lato } from "next/font/google";
import { ReactNode } from "react";
import { ReactNode, Suspense } from "react";
const lato = Lato({
weight: ["400", "700", "900"],
subsets: ["latin"],
});
export const revalidate = 60; // revalidate every minute
export default async function RootLayout({
children,
}: {
children: ReactNode;
}) {
const locale = await getLocale();
const messages = await getMessages();
return (
<html
lang={locale}
className={`${lato.className}`}
suppressHydrationWarning
>
<html className={`${lato.className}`} suppressHydrationWarning>
<head />
<body>
<ThemeProvider>
<NextIntlClientProvider messages={messages}>
<Suspense
fallback={
<div
className={`relative min-h-screen bg-background-light-600 dark:bg-background-dark-600 flex flex-col justify-center`}
>
<div className="relative mx-auto max-w-[440px] py-8 w-full">
<Skeleton>
<div className="h-40"></div>
</Skeleton>
<div className="flex flex-row justify-end py-4 items-center space-x-4">
<Theme />
</div>
</div>
</div>
}
>
<LanguageProvider>
<div
className={`relative min-h-screen bg-background-light-600 dark:bg-background-dark-600 flex flex-col justify-center`}
>
@@ -45,10 +52,10 @@ export default async function RootLayout({
</div>
</div>
</div>
<Analytics />
</NextIntlClientProvider>
</LanguageProvider>
</Suspense>
</ThemeProvider>
<Analytics />
</body>
</html>
);

View File

@@ -19,11 +19,10 @@ function getIdentityProviders(orgId?: string) {
});
}
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "loginname" });

View File

@@ -12,11 +12,10 @@ import {
} from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "mfa" });
const tError = await getTranslations({ locale, namespace: "error" });

View File

@@ -32,11 +32,10 @@ function isSessionValid(session: Partial<Session>): {
return { valid, verifiedAt };
}
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "mfa" });
const tError = await getTranslations({ locale, namespace: "error" });

View File

@@ -3,16 +3,15 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { LoginOTP } from "@/components/login-otp";
import { UserAvatar } from "@/components/user-avatar";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings } from "@/lib/zitadel";
import { getBrandingSettings, getLoginSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
params,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
params: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
params: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const params = await props.params;
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "otp" });
const tError = await getTranslations({ locale, namespace: "error" });
@@ -29,6 +28,8 @@ export default async function Page({
const branding = await getBrandingSettings(organization);
const loginSettings = await getLoginSettings(organization);
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
@@ -65,6 +66,7 @@ export default async function Page({
authRequestId={authRequestId}
organization={organization}
method={method}
loginSettings={loginSettings}
></LoginOTP>
)}
</div>

View File

@@ -9,6 +9,7 @@ import {
addOTPEmail,
addOTPSMS,
getBrandingSettings,
getLoginSettings,
registerTOTP,
} from "@/lib/zitadel";
import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
@@ -16,13 +17,12 @@ import { getLocale, getTranslations } from "next-intl/server";
import Link from "next/link";
import { redirect } from "next/navigation";
export default async function Page({
searchParams,
params,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
params: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
params: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const params = await props.params;
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "otp" });
const tError = await getTranslations({ locale, namespace: "error" });
@@ -32,6 +32,8 @@ export default async function Page({
const { method } = params;
const branding = await getBrandingSettings(organization);
const loginSettings = await getLoginSettings(organization);
const session = await loadMostRecentSession({
loginName,
organization,
@@ -137,6 +139,7 @@ export default async function Page({
authRequestId={authRequestId}
organization={organization}
checkAfter={checkAfter === "true"}
loginSettings={loginSettings}
></TotpRegister>
</div>{" "}
</>

View File

@@ -4,14 +4,17 @@ import { LoginPasskey } from "@/components/login-passkey";
import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession } from "@/lib/zitadel";
import {
getBrandingSettings,
getLoginSettings,
getSession,
} from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "passkey" });
const tError = await getTranslations({ locale, namespace: "error" });
@@ -37,6 +40,8 @@ export default async function Page({
const branding = await getBrandingSettings(organization);
const loginSettings = await getLoginSettings(organization);
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
@@ -61,6 +66,7 @@ export default async function Page({
authRequestId={authRequestId}
altPassword={altPassword === "true"}
organization={organization}
loginSettings={loginSettings}
/>
)}
</div>

View File

@@ -6,11 +6,10 @@ import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "passkey" });
const tError = await getTranslations({ locale, namespace: "error" });

View File

@@ -10,11 +10,10 @@ import {
} from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });

View File

@@ -12,11 +12,10 @@ import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });
@@ -83,7 +82,7 @@ export default async function Page({
organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user
loginSettings={loginSettings}
promptPasswordless={
loginSettings?.passkeysType === PasskeysType.ALLOWED
loginSettings?.passkeysType == PasskeysType.ALLOWED
}
isAlternative={alt === "true"}
/>

View File

@@ -13,11 +13,10 @@ import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });

View File

@@ -1,20 +1,19 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { RegisterFormWithoutPassword } from "@/components/register-form-without-password";
import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
import { RegisterForm } from "@/components/register-form";
import {
getBrandingSettings,
getDefaultOrg,
getLegalAndSupportSettings,
getLoginSettings,
getPasswordComplexitySettings,
} from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "register" });
@@ -28,47 +27,41 @@ export default async function Page({
}
}
const setPassword = !!(firstname && lastname && email);
const legal = await getLegalAndSupportSettings(organization);
const passwordComplexitySettings =
await getPasswordComplexitySettings(organization);
const branding = await getBrandingSettings(organization);
return setPassword ? (
const loginSettings = await getLoginSettings(organization);
if (!loginSettings?.allowRegister) {
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("password.title")}</h1>
<p className="ztdl-p">{t("description")}</p>
{legal && passwordComplexitySettings && (
<SetRegisterPasswordForm
passwordComplexitySettings={passwordComplexitySettings}
email={email}
firstname={firstname}
lastname={lastname}
organization={organization}
authRequestId={authRequestId}
></SetRegisterPasswordForm>
)}
<h1>{t("disabled.title")}</h1>
<p className="ztdl-p">{t("disabled.description")}</p>
</div>
</DynamicTheme>
) : (
);
}
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1>
<p className="ztdl-p">{t("description")}</p>
{legal && passwordComplexitySettings && (
<RegisterFormWithoutPassword
<RegisterForm
legal={legal}
organization={organization}
firstname={firstname}
lastname={lastname}
email={email}
authRequestId={authRequestId}
></RegisterFormWithoutPassword>
loginSettings={loginSettings}
></RegisterForm>
)}
</div>
</DynamicTheme>

View File

@@ -0,0 +1,73 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
import {
getBrandingSettings,
getDefaultOrg,
getLegalAndSupportSettings,
getLoginSettings,
getPasswordComplexitySettings,
} from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "register" });
let { firstname, lastname, email, organization, authRequestId } =
searchParams;
if (!organization) {
const org: Organization | null = await getDefaultOrg();
if (org) {
organization = org.id;
}
}
const missingData = !firstname || !lastname || !email;
const legal = await getLegalAndSupportSettings(organization);
const passwordComplexitySettings =
await getPasswordComplexitySettings(organization);
const branding = await getBrandingSettings(organization);
const loginSettings = await getLoginSettings(organization);
return missingData ? (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("missingdata.title")}</h1>
<p className="ztdl-p">{t("missingdata.description")}</p>
</div>
</DynamicTheme>
) : loginSettings?.allowRegister && loginSettings.allowUsernamePassword ? (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("password.title")}</h1>
<p className="ztdl-p">{t("description")}</p>
{legal && passwordComplexitySettings && (
<SetRegisterPasswordForm
passwordComplexitySettings={passwordComplexitySettings}
email={email}
firstname={firstname}
lastname={lastname}
organization={organization}
authRequestId={authRequestId}
></SetRegisterPasswordForm>
)}
</div>
</DynamicTheme>
) : (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("disabled.title")}</h1>
<p className="ztdl-p">{t("disabled.description")}</p>
</div>
</DynamicTheme>
);
}

View File

@@ -1,14 +1,21 @@
import { Button, ButtonVariants } from "@/components/button";
import { DynamicTheme } from "@/components/dynamic-theme";
import { SelfServiceMenu } from "@/components/self-service-menu";
import { UserAvatar } from "@/components/user-avatar";
import { getMostRecentCookieWithLoginname } from "@/lib/cookies";
import { createCallback, getBrandingSettings, getSession } from "@/lib/zitadel";
import {
createCallback,
getBrandingSettings,
getLoginSettings,
getSession,
} from "@/lib/zitadel";
import { create } from "@zitadel/client";
import {
CreateCallbackRequestSchema,
SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import Link from "next/link";
import { redirect } from "next/navigation";
async function loadSession(loginName: string, authRequestId?: string) {
@@ -39,7 +46,8 @@ async function loadSession(loginName: string, authRequestId?: string) {
);
}
export default async function Page({ searchParams }: { searchParams: any }) {
export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "signedin" });
@@ -48,6 +56,11 @@ export default async function Page({ searchParams }: { searchParams: any }) {
const branding = await getBrandingSettings(organization);
let loginSettings;
if (!authRequestId) {
loginSettings = await getLoginSettings(organization);
}
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
@@ -66,6 +79,22 @@ export default async function Page({ searchParams }: { searchParams: any }) {
{sessionFactors?.id && (
<SelfServiceMenu sessionId={sessionFactors?.id} />
)}
{loginSettings?.defaultRedirectUri && (
<div className="mt-8 flex w-full flex-row items-center">
<span className="flex-grow"></span>
<Link href={loginSettings?.defaultRedirectUri}>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
>
{t("continue")}
</Button>
</Link>
</div>
)}
</div>
</DynamicTheme>
);

View File

@@ -7,11 +7,10 @@ import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "u2f" });
const tError = await getTranslations({ locale, namespace: "error" });

View File

@@ -6,11 +6,10 @@ import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "u2f" });
const tError = await getTranslations({ locale, namespace: "error" });

View File

@@ -12,7 +12,8 @@ import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({ searchParams }: { searchParams: any }) {
export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "verify" });
const tError = await getTranslations({ locale, namespace: "error" });

View File

@@ -1,16 +1,16 @@
"use client";
import { RadioGroup } from "@headlessui/react";
import { useTranslations } from "next-intl";
export enum AuthenticationMethod {
Passkey = "passkey",
Password = "password",
}
export const methods = [
{
name: "Passkeys",
description: "Authenticate with your device.",
},
{
name: "Password",
description: "Authenticate with a password",
},
AuthenticationMethod.Passkey,
AuthenticationMethod.Password,
];
export function AuthenticationMethodRadio({
@@ -20,58 +20,68 @@ export function AuthenticationMethodRadio({
selected: any;
selectionChanged: (value: any) => void;
}) {
const t = useTranslations("register");
return (
<div className="w-full">
<div className="mx-auto w-full max-w-md">
<RadioGroup value={selected} onChange={selectionChanged}>
<RadioGroup.Label className="sr-only">Server size</RadioGroup.Label>
<div className="grid grid-cols-2 space-x-2">
<div className="flex flex-row space-x-4">
{methods.map((method) => (
<RadioGroup.Option
key={method.name}
key={method}
value={method}
data-testid={method.name + "-radio"}
data-testid={method + "-radio"}
className={({ active, checked }) =>
`${
active
? "h-full ring-2 ring-opacity-60 ring-primary-light-500 dark:ring-white/20"
: "h-full "
? "ring-2 ring-opacity-60 ring-primary-light-500 dark:ring-white/20"
: ""
}
${
checked
? "bg-background-light-400 dark:bg-background-dark-400"
? "bg-background-light-400 dark:bg-background-dark-400 ring-2 ring-primary-light-500 dark:ring-primary-dark-500"
: "bg-background-light-400 dark:bg-background-dark-400"
}
relative border boder-divider-light dark:border-divider-dark flex cursor-pointer rounded-lg px-5 py-4 focus:outline-none hover:shadow-lg dark:hover:bg-white/10`
h-full flex-1 relative border boder-divider-light dark:border-divider-dark flex cursor-pointer rounded-lg px-5 py-4 focus:outline-none hover:shadow-lg dark:hover:bg-white/10`
}
>
{({ active, checked }) => (
<>
<div className="flex w-full items-center justify-between">
<div className="flex items-center">
<div className="text-sm">
<div className="flex flex-col items-center w-full text-sm">
{method === "passkey" && (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="w-8 h-8 mb-3"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33"
/>
</svg>
)}
{method === "password" && (
<svg
className="w-8 h-8 mb-3 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<title>form-textbox-password</title>
<path d="M17,7H22V17H17V19A1,1 0 0,0 18,20H20V22H17.5C16.95,22 16,21.55 16,21C16,21.55 15.05,22 14.5,22H12V20H14A1,1 0 0,0 15,19V5A1,1 0 0,0 14,4H12V2H14.5C15.05,2 16,2.45 16,3C16,2.45 16.95,2 17.5,2H20V4H18A1,1 0 0,0 17,5V7M2,7H13V9H4V15H13V17H2V7M20,15V9H17V15H20M8.5,12A1.5,1.5 0 0,0 7,10.5A1.5,1.5 0 0,0 5.5,12A1.5,1.5 0 0,0 7,13.5A1.5,1.5 0 0,0 8.5,12M13,10.89C12.39,10.33 11.44,10.38 10.88,11C10.32,11.6 10.37,12.55 11,13.11C11.55,13.63 12.43,13.63 13,13.11V10.89Z" />
</svg>
)}
<RadioGroup.Label
as="p"
className={`font-medium ${checked ? "" : ""}`}
>
{method.name}
{t(`methods.${method}`)}
</RadioGroup.Label>
<RadioGroup.Description
as="span"
className={`text-xs text-opacity-80 dark:text-opacity-80 inline ${
checked ? "" : ""
}`}
>
{method.description}
<span aria-hidden="true">&middot;</span>{" "}
</RadioGroup.Description>
</div>
</div>
{checked && (
<div className="shrink-0 text-white">
<CheckIcon className="h-6 w-6" />
</div>
)}
</div>
</>
)}
@@ -83,24 +93,3 @@ export function AuthenticationMethodRadio({
</div>
);
}
function CheckIcon(props: any) {
return (
<svg viewBox="0 0 24 24" fill="none" {...props}>
<circle
className="fill-current text-black/50 dark:text-white/50"
cx={12}
cy={12}
r={12}
opacity="0.2"
/>
<path
d="M7 13l3 3 7-7"
className="stroke-black dark:stroke-white"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

View File

@@ -12,7 +12,6 @@ import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useTranslations } from "next-intl";
import { redirect } from "next/navigation";
import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form";
import { Alert } from "./alert";
@@ -65,6 +64,7 @@ export function ChangePasswordForm({
})
.catch(() => {
setError("Could not change password");
return;
})
.finally(() => {
setLoading(false);
@@ -107,10 +107,6 @@ export function ChangePasswordForm({
return;
}
if (passwordResponse && passwordResponse.nextStep) {
return redirect(passwordResponse.nextStep);
}
return;
}

View File

@@ -25,7 +25,7 @@ export function ChooseAuthenticatorToSetup({
} else {
return (
<>
{loginSettings.passkeysType === PasskeysType.NOT_ALLOWED &&
{loginSettings.passkeysType == PasskeysType.NOT_ALLOWED &&
!loginSettings.allowUsernamePassword && (
<Alert type={AlertType.ALERT}>{t("noMethodsAvailable")}</Alert>
)}
@@ -35,7 +35,7 @@ export function ChooseAuthenticatorToSetup({
loginSettings.allowUsernamePassword &&
PASSWORD(false, "/password/set?" + params)}
{!authMethods.includes(AuthenticationMethodType.PASSKEY) &&
loginSettings.passkeysType === PasskeysType.ALLOWED &&
loginSettings.passkeysType == PasskeysType.ALLOWED &&
PASSKEYS(false, "/passkey/set?" + params)}
</div>
</>

View File

@@ -35,30 +35,18 @@ export function IdpSignin({
},
authRequestId,
})
.then((session) => {
if (authRequestId && session && session.id) {
return router.push(
`/login?` +
new URLSearchParams({
sessionId: session.id,
authRequest: authRequestId,
}),
);
} else {
const params = new URLSearchParams({});
if (session.factors?.user?.loginName) {
params.set("loginName", session.factors?.user?.loginName);
.then((response) => {
if (response && "error" in response && response?.error) {
setError(response?.error);
return;
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
}
return router.push(`/signedin?` + params);
if (response && "redirect" in response && response?.redirect) {
return router.push(response.redirect);
}
})
.catch((error) => {
setError(error.message);
.catch(() => {
setError("An internal error occurred");
return;
})
.finally(() => {

View File

@@ -1,4 +1,5 @@
"use client";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import { clsx } from "clsx";
import {
@@ -64,6 +65,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
{label} {required && "*"}
</span>
<input
suppressHydrationWarning
ref={ref}
className={styles(!!error, !!disabled)}
defaultValue={defaultValue}

View File

@@ -0,0 +1,13 @@
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { ReactNode } from "react";
export async function LanguageProvider({ children }: { children: ReactNode }) {
const messages = await getMessages();
return (
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
);
}

View File

@@ -1,9 +1,11 @@
"use client";
import { getNextUrl } from "@/lib/client";
import { updateSession } from "@/lib/server/session";
import { create } from "@zitadel/client";
import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
@@ -22,6 +24,7 @@ type Props = {
organization?: string;
method: string;
code?: string;
loginSettings?: LoginSettings;
};
type Inputs = {
@@ -35,6 +38,7 @@ export function LoginOTP({
organization,
method,
code,
loginSettings,
}: Props) {
const t = useTranslations("otp");
@@ -59,6 +63,7 @@ export function LoginOTP({
updateSessionForOTPChallenge()
.catch((error) => {
setError(error);
return;
})
.finally(() => {
setLoading(false);
@@ -91,6 +96,7 @@ export function LoginOTP({
})
.catch((error) => {
setError(error.message ?? "Could not request OTP challenge");
return;
})
.finally(() => {
setLoading(false);
@@ -152,41 +158,30 @@ export function LoginOTP({
}
function setCodeAndContinue(values: Inputs, organization?: string) {
return submitCode(values, organization).then((response) => {
return submitCode(values, organization).then(async (response) => {
if (response) {
if (authRequestId && response && response.sessionId) {
const params = new URLSearchParams({
const url =
authRequestId && response.sessionId
? await getNextUrl(
{
sessionId: response.sessionId,
authRequest: authRequestId,
});
authRequestId: authRequestId,
organization: response.factors?.user?.organizationId,
},
loginSettings?.defaultRedirectUri,
)
: response.factors?.user
? await getNextUrl(
{
loginName: response.factors.user.loginName,
organization: response.factors?.user?.organizationId,
},
loginSettings?.defaultRedirectUri,
)
: null;
if (organization) {
params.append("organization", organization);
}
if (authRequestId) {
params.append("authRequest", authRequestId);
}
if (sessionId) {
params.append("sessionId", sessionId);
}
return router.push(`/login?` + params);
} else {
const params = new URLSearchParams();
if (response?.factors?.user?.loginName) {
params.append("loginName", response.factors.user.loginName);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
}
if (organization) {
params.append("organization", organization);
}
return router.push(`/signedin?` + params);
if (url) {
router.push(url);
}
}
});
@@ -209,6 +204,7 @@ export function LoginOTP({
updateSessionForOTPChallenge()
.catch((error) => {
setError(error);
return;
})
.finally(() => {
setLoading(false);

View File

@@ -1,6 +1,7 @@
"use client";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
import { getNextUrl } from "@/lib/client";
import { updateSession } from "@/lib/server/session";
import { create } from "@zitadel/client";
import {
@@ -8,6 +9,7 @@ import {
UserVerificationRequirement,
} from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
@@ -24,6 +26,7 @@ type Props = {
altPassword: boolean;
login?: boolean;
organization?: string;
loginSettings?: LoginSettings;
};
export function LoginPasskey({
@@ -33,6 +36,7 @@ export function LoginPasskey({
altPassword,
organization,
login = true,
loginSettings,
}: Props) {
const t = useTranslations("passkey");
@@ -63,6 +67,7 @@ export function LoginPasskey({
return submitLoginAndContinue(pK)
.catch((error) => {
setError(error);
return;
})
.finally(() => {
setLoading(false);
@@ -70,6 +75,7 @@ export function LoginPasskey({
})
.catch((error) => {
setError(error);
return;
})
.finally(() => {
setLoading(false);
@@ -98,6 +104,7 @@ export function LoginPasskey({
})
.catch(() => {
setError("Could not request passkey challenge");
return;
})
.finally(() => {
setLoading(false);
@@ -119,6 +126,7 @@ export function LoginPasskey({
})
.catch(() => {
setError("Could not verify passkey");
return;
})
.finally(() => {
setLoading(false);
@@ -147,7 +155,6 @@ export function LoginPasskey({
})
.then((assertedCredential: any) => {
if (!assertedCredential) {
setLoading(false);
setError("An error on retrieving passkey");
return;
}
@@ -175,28 +182,34 @@ export function LoginPasskey({
},
};
return submitLogin(data).then((resp) => {
if (authRequestId && resp && resp.sessionId) {
return router.push(
`/login?` +
new URLSearchParams({
return submitLogin(data).then(async (resp) => {
const url =
authRequestId && resp?.sessionId
? await getNextUrl(
{
sessionId: resp.sessionId,
authRequest: authRequestId,
}),
);
} else {
const params = new URLSearchParams({});
authRequestId: authRequestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: resp?.factors?.user?.loginName
? await getNextUrl(
{
loginName: resp.factors.user.loginName,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: null;
if (authRequestId) {
params.set("authRequestId", authRequestId);
}
if (resp?.factors?.user?.loginName) {
params.set("loginName", resp.factors.user.loginName);
}
return router.push(`/signedin?` + params);
if (url) {
router.push(url);
}
});
})
.finally(() => {
setLoading(false);
});
}
@@ -268,6 +281,7 @@ export function LoginPasskey({
return submitLoginAndContinue(pK)
.catch((error) => {
setError(error);
return;
})
.finally(() => {
setLoading(false);

View File

@@ -74,8 +74,8 @@ export function PasswordForm({
return;
}
if (response && response.nextStep) {
return router.push(response.nextStep);
if (response && "redirect" in response && response.redirect) {
return router.push(response.redirect);
}
}

View File

@@ -2,12 +2,17 @@
import { registerUser } from "@/lib/server/register";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
import {
LoginSettings,
PasskeysType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form";
import { Alert } from "./alert";
import {
AuthenticationMethod,
AuthenticationMethodRadio,
methods,
} from "./authentication-method-radio";
@@ -32,15 +37,17 @@ type Props = {
email?: string;
organization?: string;
authRequestId?: string;
loginSettings?: LoginSettings;
};
export function RegisterFormWithoutPassword({
export function RegisterForm({
legal,
email,
firstname,
lastname,
organization,
authRequestId,
loginSettings,
}: Props) {
const t = useTranslations("register");
@@ -54,7 +61,7 @@ export function RegisterFormWithoutPassword({
});
const [loading, setLoading] = useState<boolean>(false);
const [selected, setSelected] = useState(methods[0]);
const [selected, setSelected] = useState<AuthenticationMethod>(methods[0]);
const [error, setError] = useState<string>("");
const router = useRouter();
@@ -76,11 +83,15 @@ export function RegisterFormWithoutPassword({
setLoading(false);
});
if (response && "error" in response) {
if (response && "error" in response && response.error) {
setError(response.error);
return;
}
if (response && "redirect" in response && response.redirect) {
return router.push(response.redirect);
}
return response;
}
@@ -98,8 +109,11 @@ export function RegisterFormWithoutPassword({
registerParams.authRequestId = authRequestId;
}
// redirect user to /register/password if password is chosen
if (withPassword) {
return router.push(`/register?` + new URLSearchParams(registerParams));
return router.push(
`/register/password?` + new URLSearchParams(registerParams),
);
} else {
return submitAndRegister(value);
}
@@ -108,7 +122,6 @@ export function RegisterFormWithoutPassword({
const { errors } = formState;
const [tosAndPolicyAccepted, setTosAndPolicyAccepted] = useState(false);
return (
<form className="w-full">
<div className="grid grid-cols-2 gap-4 mb-4">
@@ -146,38 +159,45 @@ export function RegisterFormWithoutPassword({
/>
</div>
</div>
{legal && (
<PrivacyPolicyCheckboxes
legal={legal}
onChange={setTosAndPolicyAccepted}
/>
)}
<p className="mt-4 ztdl-p mb-6 block text-left">{t("selectMethod")}</p>
{/* show chooser if both methods are allowed */}
{loginSettings &&
loginSettings.allowUsernamePassword &&
loginSettings.passkeysType == PasskeysType.ALLOWED && (
<div className="pb-4">
<AuthenticationMethodRadio
selected={selected}
selectionChanged={setSelected}
/>
</div>
)}
{error && (
<div className="py-4">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center justify-between">
<BackButton data-testid="back-button" />
<Button
type="submit"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid || !tosAndPolicyAccepted}
onClick={handleSubmit((values) =>
submitAndContinue(values, !(selected.name === methods[0].name)),
)}
onClick={handleSubmit((values) => {
const usePasswordToContinue: boolean =
loginSettings?.allowUsernamePassword &&
loginSettings?.passkeysType == PasskeysType.ALLOWED
? !!!(selected === methods[0]) // choose selection if both available
: !!loginSettings?.allowUsernamePassword; // if password is chosen
// set password as default if only password is allowed
return submitAndContinue(values, usePasswordToContinue);
})}
data-testid="submit-button"
>
{loading && <Spinner className="h-5 w-5 mr-2" />}

View File

@@ -53,6 +53,7 @@ export function RegisterPasskey({
})
.catch(() => {
setError("Could not verify Passkey");
return;
})
.finally(() => {
setLoading(false);
@@ -68,6 +69,7 @@ export function RegisterPasskey({
})
.catch(() => {
setError("Could not register passkey");
return;
})
.finally(() => {
setLoading(false);

View File

@@ -1,7 +1,9 @@
"use client";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
import { getNextUrl } from "@/lib/client";
import { addU2F, verifyU2F } from "@/lib/server/u2f";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
@@ -17,6 +19,7 @@ type Props = {
authRequestId?: string;
organization?: string;
checkAfter: boolean;
loginSettings?: LoginSettings;
};
export function RegisterU2f({
@@ -25,6 +28,7 @@ export function RegisterU2f({
organization,
authRequestId,
checkAfter,
loginSettings,
}: Props) {
const t = useTranslations("u2f");
@@ -50,6 +54,7 @@ export function RegisterU2f({
})
.catch(() => {
setError("An error on verifying passkey occurred");
return;
})
.finally(() => {
setLoading(false);
@@ -57,12 +62,13 @@ export function RegisterU2f({
if (response && "error" in response && response?.error) {
setError(response?.error);
return;
}
return response;
}
async function submitRegisterAndContinue(): Promise<boolean | void> {
async function submitRegisterAndContinue(): Promise<boolean | void | null> {
setError("");
setLoading(true);
const response = await addU2F({
@@ -70,6 +76,7 @@ export function RegisterU2f({
})
.catch(() => {
setError("An error on registering passkey");
return;
})
.finally(() => {
setLoading(false);
@@ -77,6 +84,7 @@ export function RegisterU2f({
if (response && "error" in response && response?.error) {
setError(response?.error);
return;
}
if (!response || !("u2fId" in response)) {
@@ -146,38 +154,47 @@ export function RegisterU2f({
return;
}
if (checkAfter) {
const paramsToContinue = new URLSearchParams({});
let urlToContinue = "/accounts";
if (sessionId) {
paramsToContinue.append("sessionId", sessionId);
}
if (loginName) {
paramsToContinue.append("loginName", loginName);
}
if (organization) {
paramsToContinue.append("organization", organization);
}
if (checkAfter) {
if (authRequestId) {
paramsToContinue.append("authRequestId", authRequestId);
}
urlToContinue = `/u2f?` + paramsToContinue;
} else if (authRequestId && sessionId) {
if (authRequestId) {
paramsToContinue.append("authRequest", authRequestId);
}
urlToContinue = `/login?` + paramsToContinue;
} else if (loginName) {
if (authRequestId) {
paramsToContinue.append("authRequestId", authRequestId);
}
urlToContinue = `/signedin?` + paramsToContinue;
}
router.push(urlToContinue);
return router.push(`/u2f?` + paramsToContinue);
} else {
const url =
authRequestId && sessionId
? await getNextUrl(
{
sessionId: sessionId,
authRequestId: authRequestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: loginName
? await getNextUrl(
{
loginName: loginName,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: null;
if (url) {
return router.push(url);
}
}
}
}

View File

@@ -1,11 +1,12 @@
"use client";
import { cleanupSession } from "@/lib/server/session";
import { sendLoginname } from "@/lib/server/loginname";
import { cleanupSession, continueWithSession } from "@/lib/server/session";
import { XCircleIcon } from "@heroicons/react/24/outline";
import { Timestamp, timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import moment from "moment";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Avatar } from "./avatar";
@@ -43,6 +44,7 @@ export function SessionItem({
})
.catch((error) => {
setError(error.message);
return;
})
.finally(() => {
setLoading(false);
@@ -55,43 +57,41 @@ export function SessionItem({
const [error, setError] = useState<string | null>(null);
const router = useRouter();
return (
<Link
prefetch={false}
href={
valid && authRequestId
? `/login?` +
new URLSearchParams({
// loginName: session.factors?.user?.loginName as string,
sessionId: session.id,
authRequest: authRequestId,
<button
onClick={async () => {
if (valid && session?.factors?.user) {
return continueWithSession({
...session,
authRequestId: authRequestId,
});
} else if (session.factors?.user) {
setLoading(true);
const res = await sendLoginname({
loginName: session.factors?.user?.loginName,
organization: session.factors.user.organizationId,
authRequestId: authRequestId,
})
: !valid
? `/loginname?` +
new URLSearchParams(
authRequestId
? {
loginName: session.factors?.user?.loginName as string,
submit: "true",
authRequestId,
.catch(() => {
setError("An internal error occurred");
return;
})
.finally(() => {
setLoading(false);
});
if (res?.redirect) {
return router.push(res.redirect);
}
: {
loginName: session.factors?.user?.loginName as string,
submit: "true",
},
)
: "/signedin?" +
new URLSearchParams(
authRequestId
? {
loginName: session.factors?.user?.loginName as string,
authRequestId,
if (res?.error) {
setError(res.error);
return;
}
: {
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"
>
<div className="pr-4">
@@ -132,6 +132,6 @@ export function SessionItem({
}}
/>
</div>
</Link>
</button>
);
}

View File

@@ -1,5 +1,6 @@
"use client";
import { timestampMs } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { useTranslations } from "next-intl";
import { useState } from "react";
@@ -11,6 +12,14 @@ type Props = {
authRequestId?: string;
};
function sortFc(a: Session, b: Session) {
if (a.changeDate && b.changeDate) {
return timestampMs(a.changeDate) - timestampMs(b.changeDate);
} else {
return 0;
}
}
export function SessionsList({ sessions, authRequestId }: Props) {
const t = useTranslations("accounts");
const [list, setList] = useState<Session[]>(sessions);
@@ -18,6 +27,7 @@ export function SessionsList({ sessions, authRequestId }: Props) {
<div className="flex flex-col space-y-2">
{list
.filter((session) => session?.factors?.user?.loginName)
.sort(sortFc)
.map((session, index) => {
return (
<SessionItem

View File

@@ -11,7 +11,7 @@ import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useTranslations } from "next-intl";
import { redirect } from "next/navigation";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form";
import { Alert } from "./alert";
@@ -60,6 +60,8 @@ export function SetPasswordForm({
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const router = useRouter();
async function submitPassword(values: Inputs) {
setLoading(true);
let payload: { userId: string; password: string; code?: string } = {
@@ -127,8 +129,12 @@ export function SetPasswordForm({
return;
}
if (passwordResponse && passwordResponse.nextStep) {
return redirect(passwordResponse.nextStep);
if (
passwordResponse &&
"redirect" in passwordResponse &&
passwordResponse.redirect
) {
return router.push(passwordResponse.redirect);
}
return;

View File

@@ -9,6 +9,7 @@ import {
import { registerUser } from "@/lib/server/register";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form";
import { Alert } from "./alert";
@@ -56,6 +57,8 @@ export function SetRegisterPasswordForm({
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const router = useRouter();
async function submitRegister(values: Inputs) {
setLoading(true);
const response = await registerUser({
@@ -68,15 +71,20 @@ export function SetRegisterPasswordForm({
})
.catch(() => {
setError("Could not register user");
return;
})
.finally(() => {
setLoading(false);
});
if (response && "error" in response) {
if (response && "error" in response && response.error) {
setError(response.error);
return;
}
if (response && "redirect" in response && response.redirect) {
return router.push(response.redirect);
}
}
const { errors } = formState;

View File

@@ -56,22 +56,14 @@ export function SignInWithIdp({
})
.catch(() => {
setError("Could not start IDP flow");
return;
})
.finally(() => {
setLoading(false);
});
return response;
}
async function navigateToAuthUrl(id: string, type: IdentityProviderType) {
const startFlowResponse = await startFlow(id, idpTypeToSlug(type));
if (
startFlowResponse &&
startFlowResponse.nextStep.case === "authUrl" &&
startFlowResponse?.nextStep.value
) {
router.push(startFlowResponse.nextStep.value);
if (response && "redirect" in response && response?.redirect) {
return router.push(response.redirect);
}
}
@@ -86,7 +78,7 @@ export function SignInWithIdp({
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.APPLE)
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.APPLE))
}
></SignInWithApple>
);
@@ -96,7 +88,7 @@ export function SignInWithIdp({
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.OAUTH)
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.OAUTH))
}
></SignInWithGeneric>
);
@@ -106,7 +98,7 @@ export function SignInWithIdp({
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.OIDC)
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.OIDC))
}
></SignInWithGeneric>
);
@@ -116,7 +108,10 @@ export function SignInWithIdp({
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.GITHUB)
startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.GITHUB),
)
}
></SignInWithGithub>
);
@@ -126,7 +121,10 @@ export function SignInWithIdp({
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.GITHUB_ES)
startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.GITHUB_ES),
)
}
></SignInWithGithub>
);
@@ -136,7 +134,10 @@ export function SignInWithIdp({
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.AZURE_AD)
startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.AZURE_AD),
)
}
></SignInWithAzureAd>
);
@@ -147,7 +148,10 @@ export function SignInWithIdp({
e2e="google"
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.GOOGLE)
startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.GOOGLE),
)
}
></SignInWithGoogle>
);
@@ -157,7 +161,10 @@ export function SignInWithIdp({
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.GITLAB)
startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.GITLAB),
)
}
></SignInWithGitlab>
);
@@ -167,9 +174,9 @@ export function SignInWithIdp({
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(
startFlow(
idp.id,
IdentityProviderType.GITLAB_SELF_HOSTED,
idpTypeToSlug(IdentityProviderType.GITLAB_SELF_HOSTED),
)
}
></SignInWithGitlab>

View File

@@ -0,0 +1,9 @@
import { ReactNode } from "react";
export function Skeleton({ children }: { children?: ReactNode }) {
return (
<div className="skeleton py-12 px-8 rounded-lg bg-background-light-600 dark:bg-background-dark-600 flex flex-row items-center justify-center">
{children}
</div>
);
}

View File

@@ -1,5 +1,7 @@
"use client";
import { getNextUrl } from "@/lib/client";
import { verifyTOTP } from "@/lib/server-actions";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -24,6 +26,7 @@ type Props = {
authRequestId?: string;
organization?: string;
checkAfter?: boolean;
loginSettings?: LoginSettings;
};
export function TotpRegister({
uri,
@@ -33,6 +36,7 @@ export function TotpRegister({
authRequestId,
organization,
checkAfter,
loginSettings,
}: Props) {
const t = useTranslations("otp");
@@ -50,7 +54,7 @@ export function TotpRegister({
async function continueWithCode(values: Inputs) {
setLoading(true);
return verifyTOTP(values.code, loginName, organization)
.then((response) => {
.then(async () => {
// if attribute is set, validate MFA after it is setup, otherwise proceed as usual (when mfa is enforced to login)
if (checkAfter) {
const params = new URLSearchParams({});
@@ -67,35 +71,34 @@ export function TotpRegister({
return router.push(`/otp/time-based?` + params);
} else {
if (authRequestId && sessionId) {
const params = new URLSearchParams({
const url =
authRequestId && sessionId
? await getNextUrl(
{
sessionId: sessionId,
authRequest: authRequestId,
});
authRequestId: authRequestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: loginName
? await getNextUrl(
{
loginName: loginName,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: null;
if (organization) {
params.append("organization", organization);
}
return router.push(`/login?` + params);
} else if (loginName) {
const params = new URLSearchParams({
loginName,
});
if (authRequestId) {
params.append("authRequestId", authRequestId);
}
if (organization) {
params.append("organization", organization);
}
return router.push(`/signedin?` + params);
if (url) {
return router.push(url);
}
}
})
.catch((e) => {
setError(e.message);
return;
})
.finally(() => {
setLoading(false);

View File

@@ -55,13 +55,19 @@ export function UsernameForm({
})
.catch(() => {
setError("An internal error occurred");
return;
})
.finally(() => {
setLoading(false);
});
if (res?.redirect) {
return router.push(res.redirect);
}
if (res?.error) {
setError(res.error);
return;
}
return res;

View File

@@ -3,6 +3,7 @@
import { Alert } from "@/components/alert";
import { resendVerification, sendVerification } from "@/lib/server/email";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { Button, ButtonVariants } from "./button";
@@ -23,6 +24,8 @@ type Props = {
export function VerifyForm({ userId, code, isInvite, params }: Props) {
const t = useTranslations("verify");
const router = useRouter();
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
defaultValues: {
@@ -59,7 +62,7 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
): Promise<boolean | void> {
setLoading(true);
await sendVerification({
const response = await sendVerification({
code: value.code,
userId,
isInvite: isInvite,
@@ -71,6 +74,15 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
.finally(() => {
setLoading(false);
});
if (response?.error) {
setError(response.error);
return;
}
if (response?.redirect) {
return router.push(response?.redirect);
}
},
[isInvite, userId],
);

View File

@@ -5,7 +5,7 @@ import { cookies } from "next/headers";
export default getRequestConfig(async () => {
const fallback = "en";
const cookiesList = cookies();
const cookiesList = await cookies();
const locale: string = cookiesList.get(LANGUAGE_COOKIE_NAME)?.value ?? "en";
const userMessages = (await import(`../../locales/${locale}.json`)).default;

View File

@@ -0,0 +1,38 @@
type FinishFlowCommand =
| {
sessionId: string;
authRequestId: string;
}
| { loginName: string };
/**
* for client: redirects user back to OIDC application or to a success page when using authRequestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName
* @param command
* @returns
*/
export async function getNextUrl(
command: FinishFlowCommand & { organization?: string },
defaultRedirectUri?: string,
): Promise<string> {
if ("sessionId" in command && "authRequestId" in command) {
const url =
`/login?` +
new URLSearchParams({
sessionId: command.sessionId,
authRequest: command.authRequestId,
});
return url;
}
if (defaultRedirectUri) {
return defaultRedirectUri;
}
const signedInUrl =
`/signedin?` +
new URLSearchParams({
loginName: command.loginName,
});
return signedInUrl;
}

View File

@@ -20,8 +20,8 @@ export type Cookie = {
type SessionCookie<T> = Cookie & T;
function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) {
const cookiesList = cookies();
async function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) {
const cookiesList = await cookies();
return cookiesList.set({
name: "sessions",
@@ -32,7 +32,7 @@ function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) {
}
export async function setLanguageCookie(language: string) {
const cookiesList = cookies();
const cookiesList = await cookies();
await cookiesList.set({
name: LANGUAGE_COOKIE_NAME,
@@ -46,7 +46,7 @@ export async function addSessionToCookie<T>(
session: SessionCookie<T>,
cleanup: boolean = false,
): Promise<any> {
const cookiesList = cookies();
const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions");
let currentSessions: SessionCookie<T>[] = stringifiedCookie?.value
@@ -90,7 +90,7 @@ export async function updateSessionCookie<T>(
session: SessionCookie<T>,
cleanup: boolean = false,
): Promise<any> {
const cookiesList = cookies();
const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions");
const sessions: SessionCookie<T>[] = stringifiedCookie?.value
@@ -121,7 +121,7 @@ export async function removeSessionFromCookie<T>(
session: SessionCookie<T>,
cleanup: boolean = false,
): Promise<any> {
const cookiesList = cookies();
const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions");
const sessions: SessionCookie<T>[] = stringifiedCookie?.value
@@ -143,7 +143,7 @@ export async function removeSessionFromCookie<T>(
}
export async function getMostRecentSessionCookie<T>(): Promise<any> {
const cookiesList = cookies();
const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
@@ -166,7 +166,7 @@ export async function getSessionCookieById<T>({
sessionId: string;
organization?: string;
}): Promise<SessionCookie<T>> {
const cookiesList = cookies();
const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
@@ -194,7 +194,7 @@ export async function getSessionCookieByLoginName<T>({
loginName?: string;
organization?: string;
}): Promise<SessionCookie<T>> {
const cookiesList = cookies();
const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
@@ -222,7 +222,7 @@ export async function getSessionCookieByLoginName<T>({
export async function getAllSessionCookieIds<T>(
cleanup: boolean = false,
): Promise<any> {
const cookiesList = cookies();
const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
@@ -253,7 +253,7 @@ export async function getAllSessionCookieIds<T>(
export async function getAllSessions<T>(
cleanup: boolean = false,
): Promise<SessionCookie<T>[]> {
const cookiesList = cookies();
const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
@@ -287,7 +287,7 @@ export async function getMostRecentCookieWithLoginname<T>({
loginName?: string;
organization?: string;
}): Promise<any> {
const cookiesList = cookies();
const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {

View File

@@ -7,7 +7,7 @@ import {
getSession,
setSession,
} from "@/lib/zitadel";
import { timestampMs } from "@zitadel/client";
import { Duration, timestampMs } from "@zitadel/client";
import {
Challenges,
RequestChallenges,
@@ -30,6 +30,7 @@ export async function createSessionAndUpdateCookie(
checks: Checks,
challenges: RequestChallenges | undefined,
authRequestId: string | undefined,
lifetime?: Duration,
): Promise<Session> {
const createdSession = await createSessionFromChecks(checks, challenges);
@@ -82,10 +83,12 @@ export async function createSessionForIdpAndUpdateCookie(
idpIntentToken?: string | undefined;
},
authRequestId: string | undefined,
lifetime?: Duration,
): Promise<Session> {
const createdSession = await createSessionForUserIdAndIdpIntent(
userId,
idpIntent,
lifetime,
);
if (createdSession) {
@@ -140,12 +143,14 @@ export async function setSessionAndUpdateCookie(
checks?: Checks,
challenges?: RequestChallenges,
authRequestId?: string,
lifetime?: Duration,
) {
return setSession(
recentCookie.id,
recentCookie.token,
challenges,
checks,
lifetime,
).then((updatedSession) => {
if (updatedSession) {
const sessionCookie: CustomCookieData = {

View File

@@ -10,7 +10,6 @@ import {
} from "@/lib/zitadel";
import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { redirect } from "next/navigation";
import { createSessionAndUpdateCookie } from "./cookie";
type VerifyUserByEmailCommand = {
@@ -74,7 +73,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
if (session.factors?.user?.loginName) {
params.set("loginName", session.factors?.user?.loginName);
}
return redirect(`/authenticator/set?${params}`);
return { redirect: `/authenticator/set?${params}` };
}
}
@@ -134,6 +133,6 @@ export async function sendVerificationRedirectWithoutCheck(command: {
if (session.factors?.user?.loginName) {
params.set("loginName", session.factors?.user?.loginName);
}
return redirect(`/authenticator/set?${params}`);
return { redirect: `/authenticator/set?${params}` };
}
}

View File

@@ -15,5 +15,13 @@ export async function startIDPFlow(command: StartIDPFlowCommand) {
successUrl: command.successUrl,
failureUrl: command.failureUrl,
},
}).then((response) => {
if (
response &&
response.nextStep.case === "authUrl" &&
response?.nextStep.value
) {
return { redirect: response.nextStep.value };
}
});
}

View File

@@ -20,7 +20,7 @@ export type RegisterUserResponse = {
};
export async function inviteUser(command: InviteUserCommand) {
const host = headers().get("host");
const host = (await headers()).get("host");
const human = await addHumanUser({
email: command.email,

View File

@@ -5,7 +5,6 @@ import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
import {
@@ -44,7 +43,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
});
if (identityProviders.length === 1) {
const host = headers().get("host");
const host = (await headers()).get("host");
const identityProviderType = identityProviders[0].type;
const provider = idpTypeToSlug(identityProviderType);
@@ -70,7 +69,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
});
if (resp?.nextStep.case === "authUrl") {
return redirect(resp.nextStep.value);
return { redirect: resp.nextStep.value };
}
}
};
@@ -81,7 +80,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
});
if (identityProviders.length === 1) {
const host = headers().get("host");
const host = (await headers()).get("host");
const identityProviderId = identityProviders[0].idpId;
const idp = await getIDPByID(identityProviderId);
@@ -115,7 +114,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
});
if (resp?.nextStep.case === "authUrl") {
return redirect(resp.nextStep.value);
return { redirect: resp.nextStep.value };
}
}
};
@@ -154,7 +153,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
params.append("authRequestid", command.authRequestId);
}
return redirect("/password/set?" + params);
return { redirect: "/password/set?" + params };
}
const methods = await listAuthenticationMethodTypes(
@@ -170,6 +169,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const paramsVerify = new URLSearchParams({
loginName: session.factors?.user?.loginName,
userId: session.factors?.user?.id, // verify needs user id
invite: "true", // TODO: check - set this to true as we dont expect old email verification method here
});
if (command.organization || session.factors?.user?.organizationId) {
@@ -183,7 +183,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
paramsVerify.append("authRequestId", command.authRequestId);
}
redirect("/verify?" + paramsVerify);
return { redirect: "/verify?" + paramsVerify };
}
const paramsAuthenticatorSetup = new URLSearchParams({
@@ -202,7 +202,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
paramsAuthenticatorSetup.append("authRequestId", command.authRequestId);
}
redirect("/authenticator/set?" + paramsAuthenticatorSetup);
return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup };
}
if (methods.authMethodTypes.length == 1) {
@@ -224,7 +224,10 @@ export async function sendLoginname(command: SendLoginnameCommand) {
paramsPassword.authRequestId = command.authRequestId;
}
return redirect("/password?" + new URLSearchParams(paramsPassword));
return {
redirect: "/password?" + new URLSearchParams(paramsPassword),
};
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
const paramsPasskey: any = { loginName: command.loginName };
if (command.authRequestId) {
@@ -236,7 +239,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
command.organization ?? session.factors?.user?.organizationId;
}
return redirect("/passkey?" + new URLSearchParams(paramsPasskey));
return { redirect: "/passkey?" + new URLSearchParams(paramsPasskey) };
}
} else {
// prefer passkey in favor of other methods
@@ -255,7 +258,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
command.organization ?? session.factors?.user?.organizationId;
}
return redirect("/passkey?" + new URLSearchParams(passkeyParams));
return { redirect: "/passkey?" + new URLSearchParams(passkeyParams) };
} else if (
methods.authMethodTypes.includes(AuthenticationMethodType.IDP)
) {
@@ -275,9 +278,9 @@ export async function sendLoginname(command: SendLoginnameCommand) {
command.organization ?? session.factors?.user?.organizationId;
}
return redirect(
"/password?" + new URLSearchParams(paramsPasswordDefault),
);
return {
redirect: "/password?" + new URLSearchParams(paramsPasswordDefault),
};
}
}
}
@@ -325,7 +328,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
params.set("loginName", command.loginName);
}
return redirect("/register?" + params);
return { redirect: "/register?" + params };
}
}
@@ -342,7 +345,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
paramsPasswordDefault.append("organization", command.organization);
}
return redirect("/password?" + paramsPasswordDefault);
return { redirect: "/password?" + paramsPasswordDefault };
}
// fallbackToPassword

View File

@@ -12,6 +12,7 @@ import {
getSessionCookieById,
getSessionCookieByLoginName,
} from "../cookies";
import { getLoginSettings } from "../zitadel";
export type SetOTPCommand = {
loginName?: string;
@@ -23,22 +24,23 @@ export type SetOTPCommand = {
};
export async function setOTP(command: SetOTPCommand) {
const recentPromise = command.sessionId
? getSessionCookieById({ sessionId: command.sessionId }).catch((error) => {
const recentSession = command.sessionId
? await getSessionCookieById({ sessionId: command.sessionId }).catch(
(error) => {
return Promise.reject(error);
})
},
)
: command.loginName
? getSessionCookieByLoginName({
? await getSessionCookieByLoginName({
loginName: command.loginName,
organization: command.organization,
}).catch((error) => {
return Promise.reject(error);
})
: getMostRecentSessionCookie().catch((error) => {
: await getMostRecentSessionCookie().catch((error) => {
return Promise.reject(error);
});
return recentPromise.then((recent) => {
const checks = create(ChecksSchema, {});
if (command.method === "time-based") {
@@ -55,11 +57,14 @@ export async function setOTP(command: SetOTPCommand) {
});
}
const loginSettings = await getLoginSettings(command.organization);
return setSessionAndUpdateCookie(
recent,
recentSession,
checks,
undefined,
command.authRequestId,
loginSettings?.secondFactorCheckLifetime,
).then((session) => {
return {
sessionId: session.id,
@@ -67,5 +72,4 @@ export async function setOTP(command: SetOTPCommand) {
challenges: session.challenges,
};
});
});
}

View File

@@ -37,7 +37,7 @@ export async function registerPasskeyLink(
sessionToken: sessionCookie.token,
});
const host = headers().get("host");
const host = (await headers()).get("host");
if (!host) {
throw new Error("Could not get domain");
@@ -73,7 +73,7 @@ export async function verifyPasskey(command: VerifyPasskeyCommand) {
// if no name is provided, try to generate one from the user agent
let passkeyName = command.passkeyName;
if (!!!passkeyName) {
const headersList = headers();
const headersList = await headers();
const userAgentStructure = { headers: headersList };
const { browser, device, os } = userAgent(userAgentStructure);

View File

@@ -5,6 +5,7 @@ import {
setSessionAndUpdateCookie,
} from "@/lib/server/cookie";
import {
getLoginSettings,
getUserByID,
listAuthenticationMethodTypes,
listUsers,
@@ -16,10 +17,11 @@ import {
Checks,
ChecksSchema,
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { getNextUrl } from "../client";
import { getSessionCookieByLoginName } from "../cookies";
type ResetPasswordCommand = {
@@ -28,7 +30,7 @@ type ResetPasswordCommand = {
};
export async function resetPassword(command: ResetPasswordCommand) {
const host = headers().get("host");
const host = (await headers()).get("host");
const users = await listUsers({
loginName: command.loginName,
@@ -65,6 +67,8 @@ export async function sendPassword(command: UpdateSessionCommand) {
let session;
let user: User;
let loginSettings: LoginSettings | undefined;
if (!sessionCookie) {
const users = await listUsers({
loginName: command.loginName,
@@ -79,10 +83,13 @@ export async function sendPassword(command: UpdateSessionCommand) {
password: { password: command.checks.password?.password },
});
loginSettings = await getLoginSettings(command.organization);
session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
loginSettings?.passwordCheckLifetime,
);
}
@@ -94,6 +101,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
command.checks,
undefined,
command.authRequestId,
loginSettings?.passwordCheckLifetime,
);
if (!session?.factors?.user?.id) {
@@ -109,6 +117,12 @@ export async function sendPassword(command: UpdateSessionCommand) {
user = userResponse.user;
}
if (!loginSettings) {
loginSettings = await getLoginSettings(
command.organization ?? session.factors?.user?.organizationId,
);
}
if (!session?.factors?.user?.id || !sessionCookie) {
return { error: "Could not create session for user" };
}
@@ -153,13 +167,13 @@ export async function sendPassword(command: UpdateSessionCommand) {
const factor = availableSecondFactors[0];
// if passwordless is other method, but user selected password as alternative, perform a login
if (factor === AuthenticationMethodType.TOTP) {
return redirect(`/otp/time-based?` + params);
return { redirect: `/otp/time-based?` + params };
} else if (factor === AuthenticationMethodType.OTP_SMS) {
return redirect(`/otp/sms?` + params);
return { redirect: `/otp/sms?` + params };
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
return redirect(`/otp/email?` + params);
return { redirect: `/otp/email?` + params };
} else if (factor === AuthenticationMethodType.U2F) {
return redirect(`/u2f?` + params);
return { redirect: `/u2f?` + params };
}
} else if (availableSecondFactors?.length >= 1) {
const params = new URLSearchParams({
@@ -177,7 +191,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
);
}
return redirect(`/mfa?` + params);
return { redirect: `/mfa?` + params };
} else if (user.state === UserState.INITIAL) {
const params = new URLSearchParams({
loginName: session.factors.user.loginName,
@@ -194,7 +208,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
);
}
return redirect(`/password/change?` + params);
return { redirect: `/password/change?` + params };
} else if (command.forceMfa && !availableSecondFactors.length) {
const params = new URLSearchParams({
loginName: session.factors.user.loginName,
@@ -214,7 +228,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
}
// TODO: provide a way to setup passkeys on mfa page?
return redirect(`/mfa/set?` + params);
return { redirect: `/mfa/set?` + params };
}
// TODO: implement passkey setup
@@ -240,41 +254,28 @@ export async function sendPassword(command: UpdateSessionCommand) {
// return router.push(`/passkey/set?` + params);
// }
else if (command.authRequestId && session.id) {
const params = new URLSearchParams({
const nextUrl = await getNextUrl(
{
sessionId: session.id,
authRequest: command.authRequestId,
});
if (command.organization || session.factors?.user?.organizationId) {
params.append(
"organization",
command.organization ?? session.factors?.user?.organizationId,
);
}
return { nextStep: `/login?${params}` };
}
// without OIDC flow
const params = new URLSearchParams(
command.authRequestId
? {
loginName: session.factors.user.loginName,
authRequestId: command.authRequestId,
}
: {
loginName: session.factors.user.loginName,
},
);
if (command.organization || session.factors?.user?.organizationId) {
params.append(
"organization",
organization:
command.organization ?? session.factors?.user?.organizationId,
},
loginSettings?.defaultRedirectUri,
);
return { redirect: nextUrl };
}
return redirect(`/signedin?` + params);
const url = await getNextUrl(
{
loginName: session.factors.user.loginName,
organization: session.factors?.user?.organizationId,
},
loginSettings?.defaultRedirectUri,
);
return { redirect: url };
}
export async function changePassword(command: {

View File

@@ -1,14 +1,14 @@
"use server";
import { createSessionAndUpdateCookie } from "@/lib/server/cookie";
import { addHumanUser } from "@/lib/zitadel";
import { addHumanUser, getLoginSettings } from "@/lib/zitadel";
import { create } from "@zitadel/client";
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
import {
ChecksJson,
ChecksSchema,
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { redirect } from "next/navigation";
import { getNextUrl } from "../client";
type RegisterUserCommand = {
email: string;
@@ -37,6 +37,8 @@ export async function registerUser(command: RegisterUserCommand) {
return { error: "Could not create user" };
}
const loginSettings = await getLoginSettings(command.organization);
let checkPayload: any = {
user: { search: { case: "userId", value: human.userId } },
};
@@ -54,6 +56,7 @@ export async function registerUser(command: RegisterUserCommand) {
checks,
undefined,
command.authRequestId,
command.password ? loginSettings?.passwordCheckLifetime : undefined,
);
if (!session || !session.factors?.user) {
@@ -70,20 +73,22 @@ export async function registerUser(command: RegisterUserCommand) {
params.append("authRequestId", command.authRequestId);
}
return redirect("/passkey/set?" + params);
return { redirect: "/passkey/set?" + params };
} else {
const params = new URLSearchParams({
const url = await getNextUrl(
command.authRequestId && session.id
? {
sessionId: session.id,
authRequestId: command.authRequestId,
organization: session.factors.user.organizationId,
}
: {
loginName: session.factors.user.loginName,
organization: session.factors.user.organizationId,
});
},
loginSettings?.defaultRedirectUri,
);
if (command.authRequestId && session.factors.user.id) {
params.append("authRequest", command.authRequestId);
params.append("sessionId", session.id);
return redirect("/login?" + params);
} else {
return redirect("/signedin?" + params);
}
return { redirect: url };
}
}

View File

@@ -4,9 +4,19 @@ import {
createSessionForIdpAndUpdateCookie,
setSessionAndUpdateCookie,
} from "@/lib/server/cookie";
import { deleteSession, listAuthenticationMethodTypes } from "@/lib/zitadel";
import {
deleteSession,
getLoginSettings,
getUserByID,
listAuthenticationMethodTypes,
} from "@/lib/zitadel";
import { Duration } from "@zitadel/client";
import { RequestChallenges } 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 { headers } from "next/headers";
import { redirect } from "next/navigation";
import { getNextUrl } from "../client";
import {
getMostRecentSessionCookie,
getSessionCookieById,
@@ -31,7 +41,75 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) {
if (!userId || !idpIntent) {
throw new Error("No userId or loginName provided");
}
return createSessionForIdpAndUpdateCookie(userId, idpIntent, authRequestId);
const user = await getUserByID(userId);
if (!user) {
return { error: "Could not find user" };
}
const loginSettings = await getLoginSettings(user.details?.resourceOwner);
const session = await createSessionForIdpAndUpdateCookie(
userId,
idpIntent,
authRequestId,
loginSettings?.externalLoginCheckLifetime,
);
if (!session || !session.factors?.user) {
return { error: "Could not create session" };
}
const url = await getNextUrl(
authRequestId && session.id
? {
sessionId: session.id,
authRequestId: authRequestId,
organization: session.factors.user.organizationId,
}
: {
loginName: session.factors.user.loginName,
organization: session.factors.user.organizationId,
},
loginSettings?.defaultRedirectUri,
);
if (url) {
return { redirect: url };
}
}
export async function continueWithSession({
authRequestId,
...session
}: Session & { authRequestId?: string }) {
const loginSettings = await getLoginSettings(
session.factors?.user?.organizationId,
);
const url =
authRequestId && session.id && session.factors?.user
? await getNextUrl(
{
sessionId: session.id,
authRequestId: authRequestId,
organization: session.factors.user.organizationId,
},
loginSettings?.defaultRedirectUri,
)
: session.factors?.user
? await getNextUrl(
{
loginName: session.factors.user.loginName,
organization: session.factors.user.organizationId,
},
loginSettings?.defaultRedirectUri,
)
: null;
if (url) {
return redirect(url);
}
}
export type UpdateSessionCommand = {
@@ -41,6 +119,7 @@ export type UpdateSessionCommand = {
checks?: Checks;
authRequestId?: string;
challenges?: RequestChallenges;
lifetime?: Duration;
};
export async function updateSession(options: UpdateSessionCommand) {
@@ -52,22 +131,21 @@ export async function updateSession(options: UpdateSessionCommand) {
authRequestId,
challenges,
} = options;
const sessionPromise = sessionId
? getSessionCookieById({ sessionId }).catch((error) => {
const recentSession = sessionId
? await getSessionCookieById({ sessionId }).catch((error) => {
return Promise.reject(error);
})
: loginName
? getSessionCookieByLoginName({ loginName, organization }).catch(
? await getSessionCookieByLoginName({ loginName, organization }).catch(
(error) => {
return Promise.reject(error);
},
)
: getMostRecentSessionCookie().catch((error) => {
: await getMostRecentSessionCookie().catch((error) => {
return Promise.reject(error);
});
// TODO remove ports from host header for URL with port
const host = "localhost";
const host = (await headers()).get("host");
if (
host &&
@@ -76,16 +154,24 @@ export async function updateSession(options: UpdateSessionCommand) {
!challenges.webAuthN.domain
) {
const [hostname, port] = host.split(":");
challenges.webAuthN.domain = hostname;
}
const recent = await sessionPromise;
const loginSettings = await getLoginSettings(organization);
const lifetime = checks?.webAuthN
? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey
: checks?.otpEmail || checks?.otpSms
? loginSettings?.secondFactorCheckLifetime
: undefined;
const session = await setSessionAndUpdateCookie(
recent,
recentSession,
checks,
challenges,
authRequestId,
lifetime,
);
// if password, check if user has MFA methods

View File

@@ -32,7 +32,7 @@ export async function addU2F(command: RegisterU2FCommand) {
sessionToken: sessionCookie.token,
});
const domain = headers().get("host");
const domain = (await headers()).get("host");
if (!domain) {
return { error: "Could not get domain" };
@@ -54,7 +54,7 @@ export async function addU2F(command: RegisterU2FCommand) {
export async function verifyU2F(command: VerifyU2FCommand) {
let passkeyName = command.passkeyName;
if (!!!passkeyName) {
const headersList = headers();
const headersList = await headers();
const userAgentStructure = { headers: headersList };
const { browser, device, os } = userAgent(userAgentStructure);

View File

@@ -18,17 +18,11 @@ import {
VerifyU2FRegistrationRequest,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { create, fromJson, toJson } from "@zitadel/client";
import { create, Duration } from "@zitadel/client";
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { BrandingSettingsSchema } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { LegalAndSupportSettingsSchema } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
import {
IdentityProviderType,
LoginSettingsSchema,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { PasswordComplexitySettingsSchema } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import {
NotificationType,
@@ -43,15 +37,9 @@ import {
User,
UserState,
} from "@zitadel/proto/zitadel/user/v2/user_pb";
import { unstable_cache } from "next/cache";
import { unstable_cacheLife as cacheLife } from "next/cache";
import { PROVIDER_MAPPING } from "./idp";
const SESSION_LIFETIME_S = 3600; // TODO load from oidc settings
const CACHE_REVALIDATION_INTERVAL_IN_SECONDS = process.env
.CACHE_REVALIDATION_INTERVAL_IN_SECONDS
? Number(process.env.CACHE_REVALIDATION_INTERVAL_IN_SECONDS)
: 3600;
const transport = createServerTransport(
process.env.ZITADEL_SERVICE_USER_TOKEN!,
{ baseUrl: process.env.ZITADEL_API_URL! },
@@ -62,47 +50,31 @@ export const userService = createUserServiceClient(transport);
export const oidcService = createOIDCServiceClient(transport);
export const idpService = createIdpServiceClient(transport);
export const orgService = createOrganizationServiceClient(transport);
export const settingsService = createSettingsServiceClient(transport);
const useCache = process.env.DEBUG !== "true";
async function cacheWrapper<T>(callback: Promise<T>) {
"use cache";
cacheLife("hours");
return callback;
}
export async function getBrandingSettings(organization?: string) {
return unstable_cache(
async () => {
return await settingsService
const callback = settingsService
.getBrandingSettings({ ctx: makeReqCtx(organization) }, {})
.then((resp) =>
resp.settings
? toJson(BrandingSettingsSchema, resp.settings)
: undefined,
);
},
["brandingSettings", organization ?? "default"],
{
revalidate: CACHE_REVALIDATION_INTERVAL_IN_SECONDS,
tags: ["brandingSettings"],
},
)().then((resp) =>
resp ? fromJson(BrandingSettingsSchema, resp) : undefined,
);
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function getLoginSettings(orgId?: string) {
return unstable_cache(
async () => {
return await settingsService
const callback = settingsService
.getLoginSettings({ ctx: makeReqCtx(orgId) }, {})
.then((resp) =>
resp.settings
? toJson(LoginSettingsSchema, resp.settings)
: undefined,
);
},
["loginSettings", orgId ?? "default"],
{
revalidate: CACHE_REVALIDATION_INTERVAL_IN_SECONDS,
tags: ["loginSettings"],
},
)().then((resp) => (resp ? fromJson(LoginSettingsSchema, resp) : undefined));
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function listIDPLinks(userId: string) {
@@ -132,65 +104,39 @@ export async function registerTOTP(userId: string) {
}
export async function getGeneralSettings() {
return settingsService
const callback = settingsService
.getGeneralSettings({}, {})
.then((resp) => resp.supportedLanguages);
return useCache ? cacheWrapper(callback) : callback;
}
export async function getLegalAndSupportSettings(organization?: string) {
return unstable_cache(
async () => {
return await settingsService
const callback = settingsService
.getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {})
.then((resp) =>
resp.settings
? toJson(LegalAndSupportSettingsSchema, resp.settings)
: undefined,
);
},
["legalAndSupportSettings", organization ?? "default"],
{
revalidate: CACHE_REVALIDATION_INTERVAL_IN_SECONDS,
tags: ["legalAndSupportSettings"],
},
)().then((resp) =>
resp ? fromJson(LegalAndSupportSettingsSchema, resp) : undefined,
);
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function getPasswordComplexitySettings(organization?: string) {
return unstable_cache(
async () => {
return await settingsService
const callback = settingsService
.getPasswordComplexitySettings({ ctx: makeReqCtx(organization) })
.then((resp) =>
resp.settings
? toJson(PasswordComplexitySettingsSchema, resp.settings)
: undefined,
);
},
["complexitySettings", organization ?? "default"],
{
revalidate: CACHE_REVALIDATION_INTERVAL_IN_SECONDS,
tags: ["complexitySettings"],
},
)().then((resp) =>
resp ? fromJson(PasswordComplexitySettingsSchema, resp) : undefined,
);
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function createSessionFromChecks(
checks: Checks,
challenges: RequestChallenges | undefined,
lifetime?: Duration,
) {
return sessionService.createSession(
{
checks: checks,
challenges,
lifetime: {
seconds: BigInt(SESSION_LIFETIME_S),
nanos: 0,
},
lifetime,
},
{},
);
@@ -202,6 +148,7 @@ export async function createSessionForUserIdAndIdpIntent(
idpIntentId?: string | undefined;
idpIntentToken?: string | undefined;
},
lifetime?: Duration,
) {
return sessionService.createSession({
checks: {
@@ -213,10 +160,7 @@ export async function createSessionForUserIdAndIdpIntent(
},
idpIntent,
},
// lifetime: {
// seconds: 300,
// nanos: 0,
// },
lifetime,
});
}
@@ -225,6 +169,7 @@ export async function setSession(
sessionToken: string,
challenges: RequestChallenges | undefined,
checks?: Checks,
lifetime?: Duration,
) {
return sessionService.setSession(
{
@@ -233,6 +178,7 @@ export async function setSession(
challenges,
checks: checks ? checks : {},
metadata: {},
lifetime,
},
{},
);

View File

@@ -1,5 +1,5 @@
// include styles from the ui package
@import "./vars.scss";
@use "./vars.scss";
@tailwind base;
@tailwind components;
@@ -24,3 +24,42 @@ html {
.form-checkbox:checked {
background-image: url("/checkbox.svg");
}
.skeleton {
--accents-2: var(--theme-light-background-400);
--accents-1: var(--theme-light-background-500);
background-image: linear-gradient(
270deg,
var(--accents-1),
var(--accents-2),
var(--accents-2),
var(--accents-1)
);
background-size: 400% 100%;
animation: skeleton_loading 8s ease-in-out infinite;
}
.dark .skeleton {
--accents-2: var(--theme-dark-background-400);
--accents-1: var(--theme-dark-background-500);
background-image: linear-gradient(
270deg,
var(--accents-1),
var(--accents-2),
var(--accents-2),
var(--accents-1)
);
background-size: 400% 100%;
animation: skeleton_loading 8s ease-in-out infinite;
}
@keyframes skeleton_loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}

View File

@@ -1,4 +1,4 @@
const sharedConfig = require("zitadel-tailwind-config/tailwind.config.mjs");
import sharedConfig from "zitadel-tailwind-config/tailwind.config.mjs";
let colors = {
background: { light: { contrast: {} }, dark: { contrast: {} } },
@@ -35,7 +35,7 @@ types.forEach((type) => {
});
/** @type {import('tailwindcss').Config} */
module.exports = {
export default {
presets: [sharedConfig],
darkMode: "class",
content: ["./src/**/*.{js,ts,jsx,tsx}"],

View File

@@ -27,7 +27,9 @@
},
"pnpm": {
"overrides": {
"@typescript-eslint/parser": "^7.9.0"
"@typescript-eslint/parser": "^7.9.0",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
}
},
"devDependencies": {

View File

@@ -1,6 +1,13 @@
module.exports = {
parser: "@babel/eslint-parser",
extends: ["next", "turbo", "prettier"],
rules: {
"@next/next/no-html-link-for-pages": "off",
},
parserOptions: {
requireConfigFile: false,
babelOptions: {
presets: ["next/babel"],
},
},
};

View File

@@ -7,10 +7,11 @@
"access": "public"
},
"dependencies": {
"eslint-config-next": "^14.2.3",
"@typescript-eslint/parser": "^7.9.0",
"eslint-config-next": "^14.2.18",
"eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "^2.0.9",
"eslint-plugin-react": "^7.34.1",
"eslint-config-turbo": "^2.0.9"
"@babel/eslint-parser": "^7.25.9"
}
}

View File

@@ -4,4 +4,4 @@ export { NewAuthorizationBearerInterceptor } from "./interceptors";
// TODO: Move this to `./protobuf.ts` and export it from there
export { create, fromJson, toJson } from "@bufbuild/protobuf";
export { TimestampSchema, timestampDate, timestampFromDate, timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt";
export type { Timestamp } from "@bufbuild/protobuf/wkt";
export type { Duration, Timestamp } from "@bufbuild/protobuf/wkt";

View File

@@ -1,12 +1,8 @@
const colors = require("tailwindcss/colors");
import colors from "tailwindcss/colors";
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./page/**/*.{js,ts,jsx,tsx}",
"./ui/**/*.{js,ts,jsx,tsx}",
],
export default {
content: ["./app/**/*.{js,ts,jsx,tsx}", "./page/**/*.{js,ts,jsx,tsx}", "./ui/**/*.{js,ts,jsx,tsx}"],
future: {
hoverOnlyWhenSupported: true,
},
@@ -48,10 +44,10 @@ module.exports = {
},
backgroundImage: ({ theme }) => ({
"dark-vc-border-gradient": `radial-gradient(at left top, ${theme(
"colors.gray.800"
"colors.gray.800",
)}, 50px, ${theme("colors.gray.800")} 50%)`,
"vc-border-gradient": `radial-gradient(at left top, ${theme(
"colors.gray.200"
"colors.gray.200",
)}, 50px, ${theme("colors.gray.300")} 50%)`,
}),
keyframes: ({ theme }) => ({

1163
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff