mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-11 21:32:12 +00:00
Merge branch 'main' into main
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
{
|
||||
"service": "zitadel.settings.v2.SettingsService",
|
||||
"method": "GetBrandingSettings",
|
||||
"out": {}
|
||||
"out": {
|
||||
"data": {
|
||||
"settings": {
|
||||
"darkTheme": {
|
||||
"backgroundColor": "#ff0000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"service": "zitadel.settings.v2.SettingsService",
|
||||
|
||||
2
apps/login/next-env.d.ts
vendored
2
apps/login/next-env.d.ts
vendored
@@ -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.
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -1,54 +1,61 @@
|
||||
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}>
|
||||
<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 ">
|
||||
{children}
|
||||
<div className="flex flex-row justify-end py-4 items-center space-x-4">
|
||||
<LanguageSwitcher />
|
||||
<Theme />
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<Analytics />
|
||||
</NextIntlClientProvider>
|
||||
}
|
||||
>
|
||||
<LanguageProvider>
|
||||
<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 ">
|
||||
{children}
|
||||
<div className="flex flex-row justify-end py-4 items-center space-x-4">
|
||||
<LanguageSwitcher />
|
||||
<Theme />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LanguageProvider>
|
||||
</Suspense>
|
||||
</ThemeProvider>
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>{" "}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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"}
|
||||
/>
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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 ? (
|
||||
<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>
|
||||
const loginSettings = await getLoginSettings(organization);
|
||||
|
||||
{legal && passwordComplexitySettings && (
|
||||
<SetRegisterPasswordForm
|
||||
passwordComplexitySettings={passwordComplexitySettings}
|
||||
email={email}
|
||||
firstname={firstname}
|
||||
lastname={lastname}
|
||||
organization={organization}
|
||||
authRequestId={authRequestId}
|
||||
></SetRegisterPasswordForm>
|
||||
)}
|
||||
</div>
|
||||
</DynamicTheme>
|
||||
) : (
|
||||
if (!loginSettings?.allowRegister) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
73
apps/login/src/app/(login)/register/password/page.tsx
Normal file
73
apps/login/src/app/(login)/register/password/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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">
|
||||
<RadioGroup.Label
|
||||
as="p"
|
||||
className={`font-medium ${checked ? "" : ""}`}
|
||||
>
|
||||
{method.name}
|
||||
</RadioGroup.Label>
|
||||
<RadioGroup.Description
|
||||
as="span"
|
||||
className={`text-xs text-opacity-80 dark:text-opacity-80 inline ${
|
||||
checked ? "" : ""
|
||||
}`}
|
||||
>
|
||||
{method.description}
|
||||
<span aria-hidden="true">·</span>{" "}
|
||||
</RadioGroup.Description>
|
||||
</div>
|
||||
</div>
|
||||
{checked && (
|
||||
<div className="shrink-0 text-white">
|
||||
<CheckIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<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 ? "" : ""}`}
|
||||
>
|
||||
{t(`methods.${method}`)}
|
||||
</RadioGroup.Label>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
13
apps/login/src/components/language-provider.tsx
Normal file
13
apps/login/src/components/language-provider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
sessionId: response.sessionId,
|
||||
authRequest: authRequestId,
|
||||
});
|
||||
const url =
|
||||
authRequestId && response.sessionId
|
||||
? await getNextUrl(
|
||||
{
|
||||
sessionId: response.sessionId,
|
||||
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);
|
||||
|
||||
@@ -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({
|
||||
sessionId: resp.sessionId,
|
||||
authRequest: authRequestId,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const params = new URLSearchParams({});
|
||||
return submitLogin(data).then(async (resp) => {
|
||||
const url =
|
||||
authRequestId && resp?.sessionId
|
||||
? await getNextUrl(
|
||||
{
|
||||
sessionId: resp.sessionId,
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
<div className="pb-4">
|
||||
<AuthenticationMethodRadio
|
||||
selected={selected}
|
||||
selectionChanged={setSelected}
|
||||
/>
|
||||
</div>
|
||||
{/* 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" />}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const paramsToContinue = new URLSearchParams({});
|
||||
|
||||
router.push(urlToContinue);
|
||||
if (sessionId) {
|
||||
paramsToContinue.append("sessionId", sessionId);
|
||||
}
|
||||
if (loginName) {
|
||||
paramsToContinue.append("loginName", loginName);
|
||||
}
|
||||
if (organization) {
|
||||
paramsToContinue.append("organization", organization);
|
||||
}
|
||||
if (authRequestId) {
|
||||
paramsToContinue.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("An internal error occurred");
|
||||
return;
|
||||
})
|
||||
: !valid
|
||||
? `/loginname?` +
|
||||
new URLSearchParams(
|
||||
authRequestId
|
||||
? {
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
submit: "true",
|
||||
authRequestId,
|
||||
}
|
||||
: {
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
submit: "true",
|
||||
},
|
||||
)
|
||||
: "/signedin?" +
|
||||
new URLSearchParams(
|
||||
authRequestId
|
||||
? {
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
authRequestId,
|
||||
}
|
||||
: {
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
},
|
||||
)
|
||||
}
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
if (res?.redirect) {
|
||||
return router.push(res.redirect);
|
||||
}
|
||||
|
||||
if (res?.error) {
|
||||
setError(res.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
9
apps/login/src/components/skeleton.tsx
Normal file
9
apps/login/src/components/skeleton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
sessionId: sessionId,
|
||||
authRequest: authRequestId,
|
||||
});
|
||||
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 (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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
38
apps/login/src/lib/client.ts
Normal file
38
apps/login/src/lib/client.ts
Normal 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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getSessionCookieById,
|
||||
getSessionCookieByLoginName,
|
||||
} from "../cookies";
|
||||
import { getLoginSettings } from "../zitadel";
|
||||
|
||||
export type SetOTPCommand = {
|
||||
loginName?: string;
|
||||
@@ -23,49 +24,52 @@ export type SetOTPCommand = {
|
||||
};
|
||||
|
||||
export async function setOTP(command: SetOTPCommand) {
|
||||
const recentPromise = command.sessionId
|
||||
? getSessionCookieById({ sessionId: command.sessionId }).catch((error) => {
|
||||
return Promise.reject(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, {});
|
||||
const checks = create(ChecksSchema, {});
|
||||
|
||||
if (command.method === "time-based") {
|
||||
checks.totp = create(CheckTOTPSchema, {
|
||||
code: command.code,
|
||||
});
|
||||
} else if (command.method === "sms") {
|
||||
checks.otpSms = create(CheckOTPSchema, {
|
||||
code: command.code,
|
||||
});
|
||||
} else if (command.method === "email") {
|
||||
checks.otpEmail = create(CheckOTPSchema, {
|
||||
code: command.code,
|
||||
});
|
||||
}
|
||||
|
||||
return setSessionAndUpdateCookie(
|
||||
recent,
|
||||
checks,
|
||||
undefined,
|
||||
command.authRequestId,
|
||||
).then((session) => {
|
||||
return {
|
||||
sessionId: session.id,
|
||||
factors: session.factors,
|
||||
challenges: session.challenges,
|
||||
};
|
||||
if (command.method === "time-based") {
|
||||
checks.totp = create(CheckTOTPSchema, {
|
||||
code: command.code,
|
||||
});
|
||||
} else if (command.method === "sms") {
|
||||
checks.otpSms = create(CheckOTPSchema, {
|
||||
code: command.code,
|
||||
});
|
||||
} else if (command.method === "email") {
|
||||
checks.otpEmail = create(CheckOTPSchema, {
|
||||
code: command.code,
|
||||
});
|
||||
}
|
||||
|
||||
const loginSettings = await getLoginSettings(command.organization);
|
||||
|
||||
return setSessionAndUpdateCookie(
|
||||
recentSession,
|
||||
checks,
|
||||
undefined,
|
||||
command.authRequestId,
|
||||
loginSettings?.secondFactorCheckLifetime,
|
||||
).then((session) => {
|
||||
return {
|
||||
sessionId: session.id,
|
||||
factors: session.factors,
|
||||
challenges: session.challenges,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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({
|
||||
sessionId: session.id,
|
||||
authRequest: command.authRequestId,
|
||||
});
|
||||
const nextUrl = await getNextUrl(
|
||||
{
|
||||
sessionId: session.id,
|
||||
authRequestId: command.authRequestId,
|
||||
organization:
|
||||
command.organization ?? session.factors?.user?.organizationId,
|
||||
},
|
||||
loginSettings?.defaultRedirectUri,
|
||||
);
|
||||
|
||||
if (command.organization || session.factors?.user?.organizationId) {
|
||||
params.append(
|
||||
"organization",
|
||||
command.organization ?? session.factors?.user?.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
return { nextStep: `/login?${params}` };
|
||||
return { redirect: nextUrl };
|
||||
}
|
||||
|
||||
// without OIDC flow
|
||||
const params = new URLSearchParams(
|
||||
command.authRequestId
|
||||
? {
|
||||
loginName: session.factors.user.loginName,
|
||||
authRequestId: command.authRequestId,
|
||||
}
|
||||
: {
|
||||
loginName: session.factors.user.loginName,
|
||||
},
|
||||
const url = await getNextUrl(
|
||||
{
|
||||
loginName: session.factors.user.loginName,
|
||||
organization: session.factors?.user?.organizationId,
|
||||
},
|
||||
loginSettings?.defaultRedirectUri,
|
||||
);
|
||||
|
||||
if (command.organization || session.factors?.user?.organizationId) {
|
||||
params.append(
|
||||
"organization",
|
||||
command.organization ?? session.factors?.user?.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
return redirect(`/signedin?` + params);
|
||||
return { redirect: url };
|
||||
}
|
||||
|
||||
export async function changePassword(command: {
|
||||
|
||||
@@ -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({
|
||||
loginName: session.factors.user.loginName,
|
||||
organization: session.factors.user.organizationId,
|
||||
});
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
.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,
|
||||
);
|
||||
const callback = settingsService
|
||||
.getBrandingSettings({ ctx: makeReqCtx(organization) }, {})
|
||||
.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
|
||||
.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));
|
||||
const callback = settingsService
|
||||
.getLoginSettings({ ctx: makeReqCtx(orgId) }, {})
|
||||
.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
|
||||
.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,
|
||||
);
|
||||
const callback = settingsService
|
||||
.getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {})
|
||||
.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
|
||||
.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,
|
||||
);
|
||||
const callback = settingsService
|
||||
.getPasswordComplexitySettings({ ctx: makeReqCtx(organization) })
|
||||
.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,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"],
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
1163
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user