mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-11 22:42:18 +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) {
|
export async function registerUserScreenPassword(page: Page, firstname: string, lastname: string, email: string) {
|
||||||
await registerUserScreen(page, firstname, lastname, email);
|
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) {
|
export async function registerUserScreenPasskey(page: Page, firstname: string, lastname: string, email: string) {
|
||||||
await registerUserScreen(page, firstname, lastname, email);
|
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) {
|
export async function registerPasswordScreen(page: Page, password1: string, password2: string) {
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ describe("register", () => {
|
|||||||
result: [{ id: "256088834543534543" }],
|
result: [{ id: "256088834543534543" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", {
|
||||||
|
data: {
|
||||||
|
settings: {
|
||||||
|
passkeysType: 1,
|
||||||
|
allowRegister: true,
|
||||||
|
allowUsernamePassword: true,
|
||||||
|
defaultRedirectUri: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
stub("zitadel.user.v2.UserService", "AddHumanUser", {
|
stub("zitadel.user.v2.UserService", "AddHumanUser", {
|
||||||
data: {
|
data: {
|
||||||
userId: "221394658884845598",
|
userId: "221394658884845598",
|
||||||
@@ -53,9 +63,11 @@ describe("register", () => {
|
|||||||
|
|
||||||
it("should redirect a user who selects passwordless on register to /passkey/set", () => {
|
it("should redirect a user who selects passwordless on register to /passkey/set", () => {
|
||||||
cy.visit("/register");
|
cy.visit("/register");
|
||||||
cy.get('input[autocomplete="firstname"]').focus().type("John");
|
cy.get('input[data-testid="firstname-text-input"]').focus().type("John");
|
||||||
cy.get('input[autocomplete="lastname"]').focus().type("Doe");
|
cy.get('input[data-testid="lastname-text-input"]').focus().type("Doe");
|
||||||
cy.get('input[autocomplete="email"]').focus().type("john@zitadel.com");
|
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="privacypolicy"]').check();
|
||||||
cy.get('input[type="checkbox"][value="tos"]').check();
|
cy.get('input[type="checkbox"][value="tos"]').check();
|
||||||
cy.get('button[type="submit"]').click();
|
cy.get('button[type="submit"]').click();
|
||||||
|
|||||||
@@ -122,6 +122,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"register": {
|
"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",
|
"title": "Registrieren",
|
||||||
"description": "Erstellen Sie Ihr ZITADEL-Konto.",
|
"description": "Erstellen Sie Ihr ZITADEL-Konto.",
|
||||||
"selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten",
|
"selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten",
|
||||||
@@ -151,7 +163,8 @@
|
|||||||
},
|
},
|
||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Willkommen {user}!",
|
"title": "Willkommen {user}!",
|
||||||
"description": "Sie sind angemeldet."
|
"description": "Sie sind angemeldet.",
|
||||||
|
"continue": "Weiter"
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "Keine Benutzer-ID angegeben!",
|
"userIdMissing": "Keine Benutzer-ID angegeben!",
|
||||||
|
|||||||
@@ -122,6 +122,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"register": {
|
"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",
|
"title": "Register",
|
||||||
"description": "Create your ZITADEL account.",
|
"description": "Create your ZITADEL account.",
|
||||||
"selectMethod": "Select the method you would like to authenticate",
|
"selectMethod": "Select the method you would like to authenticate",
|
||||||
@@ -151,7 +163,8 @@
|
|||||||
},
|
},
|
||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Welcome {user}!",
|
"title": "Welcome {user}!",
|
||||||
"description": "You are signed in."
|
"description": "You are signed in.",
|
||||||
|
"continue": "Continue"
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "No userId provided!",
|
"userIdMissing": "No userId provided!",
|
||||||
|
|||||||
@@ -122,6 +122,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"register": {
|
"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",
|
"title": "Registrarse",
|
||||||
"description": "Crea tu cuenta ZITADEL.",
|
"description": "Crea tu cuenta ZITADEL.",
|
||||||
"selectMethod": "Selecciona el método con el que deseas autenticarte",
|
"selectMethod": "Selecciona el método con el que deseas autenticarte",
|
||||||
@@ -151,7 +163,8 @@
|
|||||||
},
|
},
|
||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "¡Bienvenido {user}!",
|
"title": "¡Bienvenido {user}!",
|
||||||
"description": "Has iniciado sesión."
|
"description": "Has iniciado sesión.",
|
||||||
|
"continue": "Continuar"
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "¡No se proporcionó userId!",
|
"userIdMissing": "¡No se proporcionó userId!",
|
||||||
|
|||||||
@@ -122,6 +122,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"register": {
|
"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",
|
"title": "Registrati",
|
||||||
"description": "Crea il tuo account ZITADEL.",
|
"description": "Crea il tuo account ZITADEL.",
|
||||||
"selectMethod": "Seleziona il metodo con cui desideri autenticarti",
|
"selectMethod": "Seleziona il metodo con cui desideri autenticarti",
|
||||||
@@ -151,7 +163,8 @@
|
|||||||
},
|
},
|
||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Benvenuto {user}!",
|
"title": "Benvenuto {user}!",
|
||||||
"description": "Sei connesso."
|
"description": "Sei connesso.",
|
||||||
|
"continue": "Continua"
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "Nessun userId fornito!",
|
"userIdMissing": "Nessun userId fornito!",
|
||||||
|
|||||||
@@ -2,7 +2,15 @@
|
|||||||
{
|
{
|
||||||
"service": "zitadel.settings.v2.SettingsService",
|
"service": "zitadel.settings.v2.SettingsService",
|
||||||
"method": "GetBrandingSettings",
|
"method": "GetBrandingSettings",
|
||||||
"out": {}
|
"out": {
|
||||||
|
"data": {
|
||||||
|
"settings": {
|
||||||
|
"darkTheme": {
|
||||||
|
"backgroundColor": "#ff0000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"service": "zitadel.settings.v2.SettingsService",
|
"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" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// 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 = {
|
const nextConfig = {
|
||||||
reactStrictMode: true, // Recommended for the `pages` directory, default in `app`.
|
reactStrictMode: true, // Recommended for the `pages` directory, default in `app`.
|
||||||
swcMinify: true,
|
experimental: {
|
||||||
|
dynamicIO: true,
|
||||||
|
},
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev --turbopack",
|
||||||
"test": "concurrently --timings --kill-others-on-fail 'npm:test:unit' 'npm:test:integration'",
|
"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:watch": "concurrently --kill-others 'npm:test:unit:watch' 'npm:test:integration:watch'",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
@@ -45,13 +45,13 @@
|
|||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"next": "14.2.14",
|
"next": "15.0.4-canary.23",
|
||||||
"next-intl": "^3.20.0",
|
"next-intl": "^3.25.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"nice-grpc": "2.0.1",
|
"nice-grpc": "2.0.1",
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "19.0.0-rc-66855b96-20241106",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||||
"react-hook-form": "7.39.5",
|
"react-hook-form": "7.39.5",
|
||||||
"swr": "^2.2.0",
|
"swr": "^2.2.0",
|
||||||
"tinycolor2": "1.4.2"
|
"tinycolor2": "1.4.2"
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
"@testing-library/react": "^16.0.1",
|
"@testing-library/react": "^16.0.1",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "22.9.0",
|
"@types/node": "22.9.0",
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||||
"@types/tinycolor2": "1.4.3",
|
"@types/tinycolor2": "1.4.3",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vercel/git-hooks": "1.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.
|
**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.
|
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
|
- logout
|
||||||
|
|
||||||
> NOTE: This page has to be explicitly enabled or act as a fallback if no default redirect is set.
|
> 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({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "accounts" });
|
const t = await getTranslations({ locale, namespace: "accounts" });
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ import {
|
|||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "authenticator" });
|
const t = await getTranslations({ locale, namespace: "authenticator" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|||||||
@@ -12,13 +12,11 @@ const PROVIDER_NAME_MAPPING: {
|
|||||||
[IdentityProviderType.AZURE_AD]: "Microsoft",
|
[IdentityProviderType.AZURE_AD]: "Microsoft",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
params,
|
params: Promise<{ provider: string }>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
params: { provider: string };
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "idp" });
|
const t = await getTranslations({ locale, namespace: "idp" });
|
||||||
|
|
||||||
|
|||||||
@@ -29,13 +29,12 @@ async function loginFailed(branding?: BrandingSettings) {
|
|||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
params,
|
params: Promise<{ provider: string }>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
params: { provider: string };
|
|
||||||
}) {
|
}) {
|
||||||
|
const params = await props.params;
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "idp" });
|
const t = await getTranslations({ locale, namespace: "idp" });
|
||||||
const { id, token, authRequestId, organization } = searchParams;
|
const { id, token, authRequestId, organization } = searchParams;
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ function getIdentityProviders(orgId?: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "idp" });
|
const t = await getTranslations({ locale, namespace: "idp" });
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import {
|
|||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "invite" });
|
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 { getLocale, getTranslations } from "next-intl/server";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "invite" });
|
const t = await getTranslations({ locale, namespace: "invite" });
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,46 @@
|
|||||||
import "@/styles/globals.scss";
|
import "@/styles/globals.scss";
|
||||||
|
|
||||||
|
import { LanguageProvider } from "@/components/language-provider";
|
||||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||||
|
import { Skeleton } from "@/components/skeleton";
|
||||||
import { Theme } from "@/components/theme";
|
import { Theme } from "@/components/theme";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { Analytics } from "@vercel/analytics/react";
|
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 { Lato } from "next/font/google";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode, Suspense } from "react";
|
||||||
|
|
||||||
const lato = Lato({
|
const lato = Lato({
|
||||||
weight: ["400", "700", "900"],
|
weight: ["400", "700", "900"],
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const revalidate = 60; // revalidate every minute
|
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const locale = await getLocale();
|
|
||||||
const messages = await getMessages();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html className={`${lato.className}`} suppressHydrationWarning>
|
||||||
lang={locale}
|
|
||||||
className={`${lato.className}`}
|
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
|
||||||
<head />
|
<head />
|
||||||
<body>
|
<body>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<NextIntlClientProvider messages={messages}>
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div
|
||||||
|
className={`relative min-h-screen bg-background-light-600 dark:bg-background-dark-600 flex flex-col justify-center`}
|
||||||
|
>
|
||||||
|
<div className="relative mx-auto max-w-[440px] py-8 w-full">
|
||||||
|
<Skeleton>
|
||||||
|
<div className="h-40"></div>
|
||||||
|
</Skeleton>
|
||||||
|
<div className="flex flex-row justify-end py-4 items-center space-x-4">
|
||||||
|
<Theme />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LanguageProvider>
|
||||||
<div
|
<div
|
||||||
className={`relative min-h-screen bg-background-light-600 dark:bg-background-dark-600 flex flex-col justify-center`}
|
className={`relative min-h-screen bg-background-light-600 dark:bg-background-dark-600 flex flex-col justify-center`}
|
||||||
>
|
>
|
||||||
@@ -45,10 +52,10 @@ export default async function RootLayout({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</LanguageProvider>
|
||||||
<Analytics />
|
</Suspense>
|
||||||
</NextIntlClientProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ function getIdentityProviders(orgId?: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "loginname" });
|
const t = await getTranslations({ locale, namespace: "loginname" });
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ import {
|
|||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "mfa" });
|
const t = await getTranslations({ locale, namespace: "mfa" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|||||||
@@ -32,11 +32,10 @@ function isSessionValid(session: Partial<Session>): {
|
|||||||
return { valid, verifiedAt };
|
return { valid, verifiedAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "mfa" });
|
const t = await getTranslations({ locale, namespace: "mfa" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|||||||
@@ -3,16 +3,15 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
|||||||
import { LoginOTP } from "@/components/login-otp";
|
import { LoginOTP } from "@/components/login-otp";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import { getBrandingSettings } from "@/lib/zitadel";
|
import { getBrandingSettings, getLoginSettings } from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
params,
|
params: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
params: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const params = await props.params;
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "otp" });
|
const t = await getTranslations({ locale, namespace: "otp" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
@@ -29,6 +28,8 @@ export default async function Page({
|
|||||||
|
|
||||||
const branding = await getBrandingSettings(organization);
|
const branding = await getBrandingSettings(organization);
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(organization);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicTheme branding={branding}>
|
<DynamicTheme branding={branding}>
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
@@ -65,6 +66,7 @@ export default async function Page({
|
|||||||
authRequestId={authRequestId}
|
authRequestId={authRequestId}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
method={method}
|
method={method}
|
||||||
|
loginSettings={loginSettings}
|
||||||
></LoginOTP>
|
></LoginOTP>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
addOTPEmail,
|
addOTPEmail,
|
||||||
addOTPSMS,
|
addOTPSMS,
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
|
getLoginSettings,
|
||||||
registerTOTP,
|
registerTOTP,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
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 Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
params,
|
params: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
params: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const params = await props.params;
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "otp" });
|
const t = await getTranslations({ locale, namespace: "otp" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
@@ -32,6 +32,8 @@ export default async function Page({
|
|||||||
const { method } = params;
|
const { method } = params;
|
||||||
|
|
||||||
const branding = await getBrandingSettings(organization);
|
const branding = await getBrandingSettings(organization);
|
||||||
|
const loginSettings = await getLoginSettings(organization);
|
||||||
|
|
||||||
const session = await loadMostRecentSession({
|
const session = await loadMostRecentSession({
|
||||||
loginName,
|
loginName,
|
||||||
organization,
|
organization,
|
||||||
@@ -137,6 +139,7 @@ export default async function Page({
|
|||||||
authRequestId={authRequestId}
|
authRequestId={authRequestId}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
checkAfter={checkAfter === "true"}
|
checkAfter={checkAfter === "true"}
|
||||||
|
loginSettings={loginSettings}
|
||||||
></TotpRegister>
|
></TotpRegister>
|
||||||
</div>{" "}
|
</div>{" "}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ import { LoginPasskey } from "@/components/login-passkey";
|
|||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getSessionCookieById } from "@/lib/cookies";
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
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";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "passkey" });
|
const t = await getTranslations({ locale, namespace: "passkey" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
@@ -37,6 +40,8 @@ export default async function Page({
|
|||||||
|
|
||||||
const branding = await getBrandingSettings(organization);
|
const branding = await getBrandingSettings(organization);
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(organization);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicTheme branding={branding}>
|
<DynamicTheme branding={branding}>
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
@@ -61,6 +66,7 @@ export default async function Page({
|
|||||||
authRequestId={authRequestId}
|
authRequestId={authRequestId}
|
||||||
altPassword={altPassword === "true"}
|
altPassword={altPassword === "true"}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
|
loginSettings={loginSettings}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import { loadMostRecentSession } from "@/lib/session";
|
|||||||
import { getBrandingSettings } from "@/lib/zitadel";
|
import { getBrandingSettings } from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "passkey" });
|
const t = await getTranslations({ locale, namespace: "passkey" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ import {
|
|||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "password" });
|
const t = await getTranslations({ locale, namespace: "password" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
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 { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "password" });
|
const t = await getTranslations({ locale, namespace: "password" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
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
|
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}
|
loginSettings={loginSettings}
|
||||||
promptPasswordless={
|
promptPasswordless={
|
||||||
loginSettings?.passkeysType === PasskeysType.ALLOWED
|
loginSettings?.passkeysType == PasskeysType.ALLOWED
|
||||||
}
|
}
|
||||||
isAlternative={alt === "true"}
|
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 { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "password" });
|
const t = await getTranslations({ locale, namespace: "password" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { RegisterFormWithoutPassword } from "@/components/register-form-without-password";
|
import { RegisterForm } from "@/components/register-form";
|
||||||
import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
|
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
getDefaultOrg,
|
getDefaultOrg,
|
||||||
getLegalAndSupportSettings,
|
getLegalAndSupportSettings,
|
||||||
|
getLoginSettings,
|
||||||
getPasswordComplexitySettings,
|
getPasswordComplexitySettings,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "register" });
|
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 legal = await getLegalAndSupportSettings(organization);
|
||||||
const passwordComplexitySettings =
|
const passwordComplexitySettings =
|
||||||
await getPasswordComplexitySettings(organization);
|
await getPasswordComplexitySettings(organization);
|
||||||
|
|
||||||
const branding = await getBrandingSettings(organization);
|
const branding = await getBrandingSettings(organization);
|
||||||
|
|
||||||
return setPassword ? (
|
const loginSettings = await getLoginSettings(organization);
|
||||||
|
|
||||||
|
if (!loginSettings?.allowRegister) {
|
||||||
|
return (
|
||||||
<DynamicTheme branding={branding}>
|
<DynamicTheme branding={branding}>
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<h1>{t("password.title")}</h1>
|
<h1>{t("disabled.title")}</h1>
|
||||||
<p className="ztdl-p">{t("description")}</p>
|
<p className="ztdl-p">{t("disabled.description")}</p>
|
||||||
|
|
||||||
{legal && passwordComplexitySettings && (
|
|
||||||
<SetRegisterPasswordForm
|
|
||||||
passwordComplexitySettings={passwordComplexitySettings}
|
|
||||||
email={email}
|
|
||||||
firstname={firstname}
|
|
||||||
lastname={lastname}
|
|
||||||
organization={organization}
|
|
||||||
authRequestId={authRequestId}
|
|
||||||
></SetRegisterPasswordForm>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
) : (
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<DynamicTheme branding={branding}>
|
<DynamicTheme branding={branding}>
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<h1>{t("title")}</h1>
|
<h1>{t("title")}</h1>
|
||||||
<p className="ztdl-p">{t("description")}</p>
|
<p className="ztdl-p">{t("description")}</p>
|
||||||
|
|
||||||
{legal && passwordComplexitySettings && (
|
{legal && passwordComplexitySettings && (
|
||||||
<RegisterFormWithoutPassword
|
<RegisterForm
|
||||||
legal={legal}
|
legal={legal}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
firstname={firstname}
|
firstname={firstname}
|
||||||
lastname={lastname}
|
lastname={lastname}
|
||||||
email={email}
|
email={email}
|
||||||
authRequestId={authRequestId}
|
authRequestId={authRequestId}
|
||||||
></RegisterFormWithoutPassword>
|
loginSettings={loginSettings}
|
||||||
|
></RegisterForm>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</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 { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { SelfServiceMenu } from "@/components/self-service-menu";
|
import { SelfServiceMenu } from "@/components/self-service-menu";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getMostRecentCookieWithLoginname } from "@/lib/cookies";
|
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 { create } from "@zitadel/client";
|
||||||
import {
|
import {
|
||||||
CreateCallbackRequestSchema,
|
CreateCallbackRequestSchema,
|
||||||
SessionSchema,
|
SessionSchema,
|
||||||
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
async function loadSession(loginName: string, authRequestId?: string) {
|
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 locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "signedin" });
|
const t = await getTranslations({ locale, namespace: "signedin" });
|
||||||
|
|
||||||
@@ -48,6 +56,11 @@ export default async function Page({ searchParams }: { searchParams: any }) {
|
|||||||
|
|
||||||
const branding = await getBrandingSettings(organization);
|
const branding = await getBrandingSettings(organization);
|
||||||
|
|
||||||
|
let loginSettings;
|
||||||
|
if (!authRequestId) {
|
||||||
|
loginSettings = await getLoginSettings(organization);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicTheme branding={branding}>
|
<DynamicTheme branding={branding}>
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
@@ -66,6 +79,22 @@ export default async function Page({ searchParams }: { searchParams: any }) {
|
|||||||
{sessionFactors?.id && (
|
{sessionFactors?.id && (
|
||||||
<SelfServiceMenu sessionId={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>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import { loadMostRecentSession } from "@/lib/session";
|
|||||||
import { getBrandingSettings, getSession } from "@/lib/zitadel";
|
import { getBrandingSettings, getSession } from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "u2f" });
|
const t = await getTranslations({ locale, namespace: "u2f" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import { loadMostRecentSession } from "@/lib/session";
|
|||||||
import { getBrandingSettings } from "@/lib/zitadel";
|
import { getBrandingSettings } from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page(props: {
|
||||||
searchParams,
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "u2f" });
|
const t = await getTranslations({ locale, namespace: "u2f" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
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 { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
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 locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "verify" });
|
const t = await getTranslations({ locale, namespace: "verify" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RadioGroup } from "@headlessui/react";
|
import { RadioGroup } from "@headlessui/react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export enum AuthenticationMethod {
|
||||||
|
Passkey = "passkey",
|
||||||
|
Password = "password",
|
||||||
|
}
|
||||||
|
|
||||||
export const methods = [
|
export const methods = [
|
||||||
{
|
AuthenticationMethod.Passkey,
|
||||||
name: "Passkeys",
|
AuthenticationMethod.Password,
|
||||||
description: "Authenticate with your device.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Password",
|
|
||||||
description: "Authenticate with a password",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AuthenticationMethodRadio({
|
export function AuthenticationMethodRadio({
|
||||||
@@ -20,58 +20,68 @@ export function AuthenticationMethodRadio({
|
|||||||
selected: any;
|
selected: any;
|
||||||
selectionChanged: (value: any) => void;
|
selectionChanged: (value: any) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("register");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="mx-auto w-full max-w-md">
|
||||||
<RadioGroup value={selected} onChange={selectionChanged}>
|
<RadioGroup value={selected} onChange={selectionChanged}>
|
||||||
<RadioGroup.Label className="sr-only">Server size</RadioGroup.Label>
|
<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) => (
|
{methods.map((method) => (
|
||||||
<RadioGroup.Option
|
<RadioGroup.Option
|
||||||
key={method.name}
|
key={method}
|
||||||
value={method}
|
value={method}
|
||||||
data-testid={method.name + "-radio"}
|
data-testid={method + "-radio"}
|
||||||
className={({ active, checked }) =>
|
className={({ active, checked }) =>
|
||||||
`${
|
`${
|
||||||
active
|
active
|
||||||
? "h-full ring-2 ring-opacity-60 ring-primary-light-500 dark:ring-white/20"
|
? "ring-2 ring-opacity-60 ring-primary-light-500 dark:ring-white/20"
|
||||||
: "h-full "
|
: ""
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
checked
|
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"
|
: "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 }) => (
|
{({ active, checked }) => (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex flex-col items-center w-full text-sm">
|
||||||
<div className="flex items-center">
|
{method === "passkey" && (
|
||||||
<div className="text-sm">
|
<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
|
<RadioGroup.Label
|
||||||
as="p"
|
as="p"
|
||||||
className={`font-medium ${checked ? "" : ""}`}
|
className={`font-medium ${checked ? "" : ""}`}
|
||||||
>
|
>
|
||||||
{method.name}
|
{t(`methods.${method}`)}
|
||||||
</RadioGroup.Label>
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -83,24 +93,3 @@ export function AuthenticationMethodRadio({
|
|||||||
</div>
|
</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 { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FieldValues, useForm } from "react-hook-form";
|
import { FieldValues, useForm } from "react-hook-form";
|
||||||
import { Alert } from "./alert";
|
import { Alert } from "./alert";
|
||||||
@@ -65,6 +64,7 @@ export function ChangePasswordForm({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not change password");
|
setError("Could not change password");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -107,10 +107,6 @@ export function ChangePasswordForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordResponse && passwordResponse.nextStep) {
|
|
||||||
return redirect(passwordResponse.nextStep);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function ChooseAuthenticatorToSetup({
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{loginSettings.passkeysType === PasskeysType.NOT_ALLOWED &&
|
{loginSettings.passkeysType == PasskeysType.NOT_ALLOWED &&
|
||||||
!loginSettings.allowUsernamePassword && (
|
!loginSettings.allowUsernamePassword && (
|
||||||
<Alert type={AlertType.ALERT}>{t("noMethodsAvailable")}</Alert>
|
<Alert type={AlertType.ALERT}>{t("noMethodsAvailable")}</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -35,7 +35,7 @@ export function ChooseAuthenticatorToSetup({
|
|||||||
loginSettings.allowUsernamePassword &&
|
loginSettings.allowUsernamePassword &&
|
||||||
PASSWORD(false, "/password/set?" + params)}
|
PASSWORD(false, "/password/set?" + params)}
|
||||||
{!authMethods.includes(AuthenticationMethodType.PASSKEY) &&
|
{!authMethods.includes(AuthenticationMethodType.PASSKEY) &&
|
||||||
loginSettings.passkeysType === PasskeysType.ALLOWED &&
|
loginSettings.passkeysType == PasskeysType.ALLOWED &&
|
||||||
PASSKEYS(false, "/passkey/set?" + params)}
|
PASSKEYS(false, "/passkey/set?" + params)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -35,30 +35,18 @@ export function IdpSignin({
|
|||||||
},
|
},
|
||||||
authRequestId,
|
authRequestId,
|
||||||
})
|
})
|
||||||
.then((session) => {
|
.then((response) => {
|
||||||
if (authRequestId && session && session.id) {
|
if (response && "error" in response && response?.error) {
|
||||||
return router.push(
|
setError(response?.error);
|
||||||
`/login?` +
|
return;
|
||||||
new URLSearchParams({
|
|
||||||
sessionId: session.id,
|
|
||||||
authRequest: authRequestId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const params = new URLSearchParams({});
|
|
||||||
if (session.factors?.user?.loginName) {
|
|
||||||
params.set("loginName", session.factors?.user?.loginName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authRequestId) {
|
if (response && "redirect" in response && response?.redirect) {
|
||||||
params.set("authRequestId", authRequestId);
|
return router.push(response.redirect);
|
||||||
}
|
|
||||||
|
|
||||||
return router.push(`/signedin?` + params);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
setError(error.message);
|
setError("An internal error occurred");
|
||||||
return;
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import {
|
import {
|
||||||
@@ -64,6 +65,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
|||||||
{label} {required && "*"}
|
{label} {required && "*"}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
|
suppressHydrationWarning
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={styles(!!error, !!disabled)}
|
className={styles(!!error, !!disabled)}
|
||||||
defaultValue={defaultValue}
|
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";
|
"use client";
|
||||||
|
|
||||||
|
import { getNextUrl } from "@/lib/client";
|
||||||
import { updateSession } from "@/lib/server/session";
|
import { updateSession } from "@/lib/server/session";
|
||||||
import { create } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
|
import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
|
||||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_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 { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
@@ -22,6 +24,7 @@ type Props = {
|
|||||||
organization?: string;
|
organization?: string;
|
||||||
method: string;
|
method: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
|
loginSettings?: LoginSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Inputs = {
|
type Inputs = {
|
||||||
@@ -35,6 +38,7 @@ export function LoginOTP({
|
|||||||
organization,
|
organization,
|
||||||
method,
|
method,
|
||||||
code,
|
code,
|
||||||
|
loginSettings,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTranslations("otp");
|
const t = useTranslations("otp");
|
||||||
|
|
||||||
@@ -59,6 +63,7 @@ export function LoginOTP({
|
|||||||
updateSessionForOTPChallenge()
|
updateSessionForOTPChallenge()
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -91,6 +96,7 @@ export function LoginOTP({
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error.message ?? "Could not request OTP challenge");
|
setError(error.message ?? "Could not request OTP challenge");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -152,41 +158,30 @@ export function LoginOTP({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setCodeAndContinue(values: Inputs, organization?: string) {
|
function setCodeAndContinue(values: Inputs, organization?: string) {
|
||||||
return submitCode(values, organization).then((response) => {
|
return submitCode(values, organization).then(async (response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
if (authRequestId && response && response.sessionId) {
|
const url =
|
||||||
const params = new URLSearchParams({
|
authRequestId && response.sessionId
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
sessionId: response.sessionId,
|
sessionId: response.sessionId,
|
||||||
authRequest: authRequestId,
|
authRequestId: authRequestId,
|
||||||
});
|
organization: response.factors?.user?.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: response.factors?.user
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
|
loginName: response.factors.user.loginName,
|
||||||
|
organization: response.factors?.user?.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
if (organization) {
|
if (url) {
|
||||||
params.append("organization", organization);
|
router.push(url);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -209,6 +204,7 @@ export function LoginOTP({
|
|||||||
updateSessionForOTPChallenge()
|
updateSessionForOTPChallenge()
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
|
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
|
||||||
|
import { getNextUrl } from "@/lib/client";
|
||||||
import { updateSession } from "@/lib/server/session";
|
import { updateSession } from "@/lib/server/session";
|
||||||
import { create } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import {
|
import {
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
UserVerificationRequirement,
|
UserVerificationRequirement,
|
||||||
} from "@zitadel/proto/zitadel/session/v2/challenge_pb";
|
} from "@zitadel/proto/zitadel/session/v2/challenge_pb";
|
||||||
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_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 { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
@@ -24,6 +26,7 @@ type Props = {
|
|||||||
altPassword: boolean;
|
altPassword: boolean;
|
||||||
login?: boolean;
|
login?: boolean;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
|
loginSettings?: LoginSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LoginPasskey({
|
export function LoginPasskey({
|
||||||
@@ -33,6 +36,7 @@ export function LoginPasskey({
|
|||||||
altPassword,
|
altPassword,
|
||||||
organization,
|
organization,
|
||||||
login = true,
|
login = true,
|
||||||
|
loginSettings,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTranslations("passkey");
|
const t = useTranslations("passkey");
|
||||||
|
|
||||||
@@ -63,6 +67,7 @@ export function LoginPasskey({
|
|||||||
return submitLoginAndContinue(pK)
|
return submitLoginAndContinue(pK)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -70,6 +75,7 @@ export function LoginPasskey({
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -98,6 +104,7 @@ export function LoginPasskey({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not request passkey challenge");
|
setError("Could not request passkey challenge");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -119,6 +126,7 @@ export function LoginPasskey({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not verify passkey");
|
setError("Could not verify passkey");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -147,7 +155,6 @@ export function LoginPasskey({
|
|||||||
})
|
})
|
||||||
.then((assertedCredential: any) => {
|
.then((assertedCredential: any) => {
|
||||||
if (!assertedCredential) {
|
if (!assertedCredential) {
|
||||||
setLoading(false);
|
|
||||||
setError("An error on retrieving passkey");
|
setError("An error on retrieving passkey");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -175,28 +182,34 @@ export function LoginPasskey({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return submitLogin(data).then((resp) => {
|
return submitLogin(data).then(async (resp) => {
|
||||||
if (authRequestId && resp && resp.sessionId) {
|
const url =
|
||||||
return router.push(
|
authRequestId && resp?.sessionId
|
||||||
`/login?` +
|
? await getNextUrl(
|
||||||
new URLSearchParams({
|
{
|
||||||
sessionId: resp.sessionId,
|
sessionId: resp.sessionId,
|
||||||
authRequest: authRequestId,
|
authRequestId: authRequestId,
|
||||||
}),
|
organization: organization,
|
||||||
);
|
},
|
||||||
} else {
|
loginSettings?.defaultRedirectUri,
|
||||||
const params = new URLSearchParams({});
|
)
|
||||||
|
: resp?.factors?.user?.loginName
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
|
loginName: resp.factors.user.loginName,
|
||||||
|
organization: organization,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
if (authRequestId) {
|
if (url) {
|
||||||
params.set("authRequestId", authRequestId);
|
router.push(url);
|
||||||
}
|
|
||||||
if (resp?.factors?.user?.loginName) {
|
|
||||||
params.set("loginName", resp.factors.user.loginName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return router.push(`/signedin?` + params);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +281,7 @@ export function LoginPasskey({
|
|||||||
return submitLoginAndContinue(pK)
|
return submitLoginAndContinue(pK)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ export function PasswordForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && response.nextStep) {
|
if (response && "redirect" in response && response.redirect) {
|
||||||
return router.push(response.nextStep);
|
return router.push(response.redirect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
|
|
||||||
import { registerUser } from "@/lib/server/register";
|
import { registerUser } from "@/lib/server/register";
|
||||||
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
|
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 { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FieldValues, useForm } from "react-hook-form";
|
import { FieldValues, useForm } from "react-hook-form";
|
||||||
import { Alert } from "./alert";
|
import { Alert } from "./alert";
|
||||||
import {
|
import {
|
||||||
|
AuthenticationMethod,
|
||||||
AuthenticationMethodRadio,
|
AuthenticationMethodRadio,
|
||||||
methods,
|
methods,
|
||||||
} from "./authentication-method-radio";
|
} from "./authentication-method-radio";
|
||||||
@@ -32,15 +37,17 @@ type Props = {
|
|||||||
email?: string;
|
email?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
|
loginSettings?: LoginSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RegisterFormWithoutPassword({
|
export function RegisterForm({
|
||||||
legal,
|
legal,
|
||||||
email,
|
email,
|
||||||
firstname,
|
firstname,
|
||||||
lastname,
|
lastname,
|
||||||
organization,
|
organization,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
|
loginSettings,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTranslations("register");
|
const t = useTranslations("register");
|
||||||
|
|
||||||
@@ -54,7 +61,7 @@ export function RegisterFormWithoutPassword({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
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 [error, setError] = useState<string>("");
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -76,11 +83,15 @@ export function RegisterFormWithoutPassword({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && "error" in response) {
|
if (response && "error" in response && response.error) {
|
||||||
setError(response.error);
|
setError(response.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response && "redirect" in response && response.redirect) {
|
||||||
|
return router.push(response.redirect);
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +109,11 @@ export function RegisterFormWithoutPassword({
|
|||||||
registerParams.authRequestId = authRequestId;
|
registerParams.authRequestId = authRequestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redirect user to /register/password if password is chosen
|
||||||
if (withPassword) {
|
if (withPassword) {
|
||||||
return router.push(`/register?` + new URLSearchParams(registerParams));
|
return router.push(
|
||||||
|
`/register/password?` + new URLSearchParams(registerParams),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return submitAndRegister(value);
|
return submitAndRegister(value);
|
||||||
}
|
}
|
||||||
@@ -108,7 +122,6 @@ export function RegisterFormWithoutPassword({
|
|||||||
const { errors } = formState;
|
const { errors } = formState;
|
||||||
|
|
||||||
const [tosAndPolicyAccepted, setTosAndPolicyAccepted] = useState(false);
|
const [tosAndPolicyAccepted, setTosAndPolicyAccepted] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="w-full">
|
<form className="w-full">
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
@@ -146,38 +159,45 @@ export function RegisterFormWithoutPassword({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{legal && (
|
{legal && (
|
||||||
<PrivacyPolicyCheckboxes
|
<PrivacyPolicyCheckboxes
|
||||||
legal={legal}
|
legal={legal}
|
||||||
onChange={setTosAndPolicyAccepted}
|
onChange={setTosAndPolicyAccepted}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="mt-4 ztdl-p mb-6 block text-left">{t("selectMethod")}</p>
|
<p className="mt-4 ztdl-p mb-6 block text-left">{t("selectMethod")}</p>
|
||||||
|
{/* show chooser if both methods are allowed */}
|
||||||
|
{loginSettings &&
|
||||||
|
loginSettings.allowUsernamePassword &&
|
||||||
|
loginSettings.passkeysType == PasskeysType.ALLOWED && (
|
||||||
<div className="pb-4">
|
<div className="pb-4">
|
||||||
<AuthenticationMethodRadio
|
<AuthenticationMethodRadio
|
||||||
selected={selected}
|
selected={selected}
|
||||||
selectionChanged={setSelected}
|
selectionChanged={setSelected}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<Alert>{error}</Alert>
|
<Alert>{error}</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
||||||
<BackButton data-testid="back-button" />
|
<BackButton data-testid="back-button" />
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant={ButtonVariants.Primary}
|
variant={ButtonVariants.Primary}
|
||||||
disabled={loading || !formState.isValid || !tosAndPolicyAccepted}
|
disabled={loading || !formState.isValid || !tosAndPolicyAccepted}
|
||||||
onClick={handleSubmit((values) =>
|
onClick={handleSubmit((values) => {
|
||||||
submitAndContinue(values, !(selected.name === methods[0].name)),
|
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"
|
data-testid="submit-button"
|
||||||
>
|
>
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
@@ -53,6 +53,7 @@ export function RegisterPasskey({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not verify Passkey");
|
setError("Could not verify Passkey");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -68,6 +69,7 @@ export function RegisterPasskey({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not register passkey");
|
setError("Could not register passkey");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
|
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
|
||||||
|
import { getNextUrl } from "@/lib/client";
|
||||||
import { addU2F, verifyU2F } from "@/lib/server/u2f";
|
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 { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -17,6 +19,7 @@ type Props = {
|
|||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
checkAfter: boolean;
|
checkAfter: boolean;
|
||||||
|
loginSettings?: LoginSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RegisterU2f({
|
export function RegisterU2f({
|
||||||
@@ -25,6 +28,7 @@ export function RegisterU2f({
|
|||||||
organization,
|
organization,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
checkAfter,
|
checkAfter,
|
||||||
|
loginSettings,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTranslations("u2f");
|
const t = useTranslations("u2f");
|
||||||
|
|
||||||
@@ -50,6 +54,7 @@ export function RegisterU2f({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("An error on verifying passkey occurred");
|
setError("An error on verifying passkey occurred");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -57,12 +62,13 @@ export function RegisterU2f({
|
|||||||
|
|
||||||
if (response && "error" in response && response?.error) {
|
if (response && "error" in response && response?.error) {
|
||||||
setError(response?.error);
|
setError(response?.error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitRegisterAndContinue(): Promise<boolean | void> {
|
async function submitRegisterAndContinue(): Promise<boolean | void | null> {
|
||||||
setError("");
|
setError("");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await addU2F({
|
const response = await addU2F({
|
||||||
@@ -70,6 +76,7 @@ export function RegisterU2f({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("An error on registering passkey");
|
setError("An error on registering passkey");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -77,6 +84,7 @@ export function RegisterU2f({
|
|||||||
|
|
||||||
if (response && "error" in response && response?.error) {
|
if (response && "error" in response && response?.error) {
|
||||||
setError(response?.error);
|
setError(response?.error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response || !("u2fId" in response)) {
|
if (!response || !("u2fId" in response)) {
|
||||||
@@ -146,38 +154,47 @@ export function RegisterU2f({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (checkAfter) {
|
||||||
const paramsToContinue = new URLSearchParams({});
|
const paramsToContinue = new URLSearchParams({});
|
||||||
let urlToContinue = "/accounts";
|
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
paramsToContinue.append("sessionId", sessionId);
|
paramsToContinue.append("sessionId", sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginName) {
|
if (loginName) {
|
||||||
paramsToContinue.append("loginName", loginName);
|
paramsToContinue.append("loginName", loginName);
|
||||||
}
|
}
|
||||||
if (organization) {
|
if (organization) {
|
||||||
paramsToContinue.append("organization", organization);
|
paramsToContinue.append("organization", organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkAfter) {
|
|
||||||
if (authRequestId) {
|
if (authRequestId) {
|
||||||
paramsToContinue.append("authRequestId", authRequestId);
|
paramsToContinue.append("authRequestId", authRequestId);
|
||||||
}
|
}
|
||||||
urlToContinue = `/u2f?` + paramsToContinue;
|
|
||||||
} else if (authRequestId && sessionId) {
|
|
||||||
if (authRequestId) {
|
|
||||||
paramsToContinue.append("authRequest", authRequestId);
|
|
||||||
}
|
|
||||||
urlToContinue = `/login?` + paramsToContinue;
|
|
||||||
} else if (loginName) {
|
|
||||||
if (authRequestId) {
|
|
||||||
paramsToContinue.append("authRequestId", authRequestId);
|
|
||||||
}
|
|
||||||
urlToContinue = `/signedin?` + paramsToContinue;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push(urlToContinue);
|
return router.push(`/u2f?` + paramsToContinue);
|
||||||
|
} else {
|
||||||
|
const url =
|
||||||
|
authRequestId && sessionId
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
|
sessionId: sessionId,
|
||||||
|
authRequestId: authRequestId,
|
||||||
|
organization: organization,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: loginName
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
|
loginName: loginName,
|
||||||
|
organization: organization,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
if (url) {
|
||||||
|
return router.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
"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 { XCircleIcon } from "@heroicons/react/24/outline";
|
||||||
import { Timestamp, timestampDate } from "@zitadel/client";
|
import { Timestamp, timestampDate } from "@zitadel/client";
|
||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Link from "next/link";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Avatar } from "./avatar";
|
import { Avatar } from "./avatar";
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ export function SessionItem({
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error.message);
|
setError(error.message);
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -55,43 +57,41 @@ export function SessionItem({
|
|||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<button
|
||||||
prefetch={false}
|
onClick={async () => {
|
||||||
href={
|
if (valid && session?.factors?.user) {
|
||||||
valid && authRequestId
|
return continueWithSession({
|
||||||
? `/login?` +
|
...session,
|
||||||
new URLSearchParams({
|
authRequestId: authRequestId,
|
||||||
// loginName: session.factors?.user?.loginName as string,
|
});
|
||||||
sessionId: session.id,
|
} else if (session.factors?.user) {
|
||||||
authRequest: authRequestId,
|
setLoading(true);
|
||||||
|
const res = await sendLoginname({
|
||||||
|
loginName: session.factors?.user?.loginName,
|
||||||
|
organization: session.factors.user.organizationId,
|
||||||
|
authRequestId: authRequestId,
|
||||||
})
|
})
|
||||||
: !valid
|
.catch(() => {
|
||||||
? `/loginname?` +
|
setError("An internal error occurred");
|
||||||
new URLSearchParams(
|
return;
|
||||||
authRequestId
|
})
|
||||||
? {
|
.finally(() => {
|
||||||
loginName: session.factors?.user?.loginName as string,
|
setLoading(false);
|
||||||
submit: "true",
|
});
|
||||||
authRequestId,
|
|
||||||
|
if (res?.redirect) {
|
||||||
|
return router.push(res.redirect);
|
||||||
}
|
}
|
||||||
: {
|
|
||||||
loginName: session.factors?.user?.loginName as string,
|
if (res?.error) {
|
||||||
submit: "true",
|
setError(res.error);
|
||||||
},
|
return;
|
||||||
)
|
|
||||||
: "/signedin?" +
|
|
||||||
new URLSearchParams(
|
|
||||||
authRequestId
|
|
||||||
? {
|
|
||||||
loginName: session.factors?.user?.loginName as string,
|
|
||||||
authRequestId,
|
|
||||||
}
|
}
|
||||||
: {
|
|
||||||
loginName: session.factors?.user?.loginName as string,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
className="group flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light hover:shadow-lg dark:hover:bg-white/10 py-2 px-4 rounded-md transition-all"
|
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">
|
<div className="pr-4">
|
||||||
@@ -132,6 +132,6 @@ export function SessionItem({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { timestampMs } from "@zitadel/client";
|
||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -11,6 +12,14 @@ type Props = {
|
|||||||
authRequestId?: string;
|
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) {
|
export function SessionsList({ sessions, authRequestId }: Props) {
|
||||||
const t = useTranslations("accounts");
|
const t = useTranslations("accounts");
|
||||||
const [list, setList] = useState<Session[]>(sessions);
|
const [list, setList] = useState<Session[]>(sessions);
|
||||||
@@ -18,6 +27,7 @@ export function SessionsList({ sessions, authRequestId }: Props) {
|
|||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
{list
|
{list
|
||||||
.filter((session) => session?.factors?.user?.loginName)
|
.filter((session) => session?.factors?.user?.loginName)
|
||||||
|
.sort(sortFc)
|
||||||
.map((session, index) => {
|
.map((session, index) => {
|
||||||
return (
|
return (
|
||||||
<SessionItem
|
<SessionItem
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { create } from "@zitadel/client";
|
|||||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { redirect } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FieldValues, useForm } from "react-hook-form";
|
import { FieldValues, useForm } from "react-hook-form";
|
||||||
import { Alert } from "./alert";
|
import { Alert } from "./alert";
|
||||||
@@ -60,6 +60,8 @@ export function SetPasswordForm({
|
|||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
async function submitPassword(values: Inputs) {
|
async function submitPassword(values: Inputs) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let payload: { userId: string; password: string; code?: string } = {
|
let payload: { userId: string; password: string; code?: string } = {
|
||||||
@@ -127,8 +129,12 @@ export function SetPasswordForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordResponse && passwordResponse.nextStep) {
|
if (
|
||||||
return redirect(passwordResponse.nextStep);
|
passwordResponse &&
|
||||||
|
"redirect" in passwordResponse &&
|
||||||
|
passwordResponse.redirect
|
||||||
|
) {
|
||||||
|
return router.push(passwordResponse.redirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { registerUser } from "@/lib/server/register";
|
import { registerUser } from "@/lib/server/register";
|
||||||
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FieldValues, useForm } from "react-hook-form";
|
import { FieldValues, useForm } from "react-hook-form";
|
||||||
import { Alert } from "./alert";
|
import { Alert } from "./alert";
|
||||||
@@ -56,6 +57,8 @@ export function SetRegisterPasswordForm({
|
|||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
async function submitRegister(values: Inputs) {
|
async function submitRegister(values: Inputs) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await registerUser({
|
const response = await registerUser({
|
||||||
@@ -68,15 +71,20 @@ export function SetRegisterPasswordForm({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not register user");
|
setError("Could not register user");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && "error" in response) {
|
if (response && "error" in response && response.error) {
|
||||||
setError(response.error);
|
setError(response.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response && "redirect" in response && response.redirect) {
|
||||||
|
return router.push(response.redirect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { errors } = formState;
|
const { errors } = formState;
|
||||||
|
|||||||
@@ -56,22 +56,14 @@ export function SignInWithIdp({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not start IDP flow");
|
setError("Could not start IDP flow");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
if (response && "redirect" in response && response?.redirect) {
|
||||||
}
|
return router.push(response.redirect);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +78,7 @@ export function SignInWithIdp({
|
|||||||
key={`idp-${i}`}
|
key={`idp-${i}`}
|
||||||
name={idp.name}
|
name={idp.name}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigateToAuthUrl(idp.id, IdentityProviderType.APPLE)
|
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.APPLE))
|
||||||
}
|
}
|
||||||
></SignInWithApple>
|
></SignInWithApple>
|
||||||
);
|
);
|
||||||
@@ -96,7 +88,7 @@ export function SignInWithIdp({
|
|||||||
key={`idp-${i}`}
|
key={`idp-${i}`}
|
||||||
name={idp.name}
|
name={idp.name}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigateToAuthUrl(idp.id, IdentityProviderType.OAUTH)
|
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.OAUTH))
|
||||||
}
|
}
|
||||||
></SignInWithGeneric>
|
></SignInWithGeneric>
|
||||||
);
|
);
|
||||||
@@ -106,7 +98,7 @@ export function SignInWithIdp({
|
|||||||
key={`idp-${i}`}
|
key={`idp-${i}`}
|
||||||
name={idp.name}
|
name={idp.name}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigateToAuthUrl(idp.id, IdentityProviderType.OIDC)
|
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.OIDC))
|
||||||
}
|
}
|
||||||
></SignInWithGeneric>
|
></SignInWithGeneric>
|
||||||
);
|
);
|
||||||
@@ -116,7 +108,10 @@ export function SignInWithIdp({
|
|||||||
key={`idp-${i}`}
|
key={`idp-${i}`}
|
||||||
name={idp.name}
|
name={idp.name}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigateToAuthUrl(idp.id, IdentityProviderType.GITHUB)
|
startFlow(
|
||||||
|
idp.id,
|
||||||
|
idpTypeToSlug(IdentityProviderType.GITHUB),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
></SignInWithGithub>
|
></SignInWithGithub>
|
||||||
);
|
);
|
||||||
@@ -126,7 +121,10 @@ export function SignInWithIdp({
|
|||||||
key={`idp-${i}`}
|
key={`idp-${i}`}
|
||||||
name={idp.name}
|
name={idp.name}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigateToAuthUrl(idp.id, IdentityProviderType.GITHUB_ES)
|
startFlow(
|
||||||
|
idp.id,
|
||||||
|
idpTypeToSlug(IdentityProviderType.GITHUB_ES),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
></SignInWithGithub>
|
></SignInWithGithub>
|
||||||
);
|
);
|
||||||
@@ -136,7 +134,10 @@ export function SignInWithIdp({
|
|||||||
key={`idp-${i}`}
|
key={`idp-${i}`}
|
||||||
name={idp.name}
|
name={idp.name}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigateToAuthUrl(idp.id, IdentityProviderType.AZURE_AD)
|
startFlow(
|
||||||
|
idp.id,
|
||||||
|
idpTypeToSlug(IdentityProviderType.AZURE_AD),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
></SignInWithAzureAd>
|
></SignInWithAzureAd>
|
||||||
);
|
);
|
||||||
@@ -147,7 +148,10 @@ export function SignInWithIdp({
|
|||||||
e2e="google"
|
e2e="google"
|
||||||
name={idp.name}
|
name={idp.name}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigateToAuthUrl(idp.id, IdentityProviderType.GOOGLE)
|
startFlow(
|
||||||
|
idp.id,
|
||||||
|
idpTypeToSlug(IdentityProviderType.GOOGLE),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
></SignInWithGoogle>
|
></SignInWithGoogle>
|
||||||
);
|
);
|
||||||
@@ -157,7 +161,10 @@ export function SignInWithIdp({
|
|||||||
key={`idp-${i}`}
|
key={`idp-${i}`}
|
||||||
name={idp.name}
|
name={idp.name}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigateToAuthUrl(idp.id, IdentityProviderType.GITLAB)
|
startFlow(
|
||||||
|
idp.id,
|
||||||
|
idpTypeToSlug(IdentityProviderType.GITLAB),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
></SignInWithGitlab>
|
></SignInWithGitlab>
|
||||||
);
|
);
|
||||||
@@ -167,9 +174,9 @@ export function SignInWithIdp({
|
|||||||
key={`idp-${i}`}
|
key={`idp-${i}`}
|
||||||
name={idp.name}
|
name={idp.name}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigateToAuthUrl(
|
startFlow(
|
||||||
idp.id,
|
idp.id,
|
||||||
IdentityProviderType.GITLAB_SELF_HOSTED,
|
idpTypeToSlug(IdentityProviderType.GITLAB_SELF_HOSTED),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></SignInWithGitlab>
|
></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";
|
"use client";
|
||||||
|
import { getNextUrl } from "@/lib/client";
|
||||||
import { verifyTOTP } from "@/lib/server-actions";
|
import { verifyTOTP } from "@/lib/server-actions";
|
||||||
|
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -24,6 +26,7 @@ type Props = {
|
|||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
checkAfter?: boolean;
|
checkAfter?: boolean;
|
||||||
|
loginSettings?: LoginSettings;
|
||||||
};
|
};
|
||||||
export function TotpRegister({
|
export function TotpRegister({
|
||||||
uri,
|
uri,
|
||||||
@@ -33,6 +36,7 @@ export function TotpRegister({
|
|||||||
authRequestId,
|
authRequestId,
|
||||||
organization,
|
organization,
|
||||||
checkAfter,
|
checkAfter,
|
||||||
|
loginSettings,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTranslations("otp");
|
const t = useTranslations("otp");
|
||||||
|
|
||||||
@@ -50,7 +54,7 @@ export function TotpRegister({
|
|||||||
async function continueWithCode(values: Inputs) {
|
async function continueWithCode(values: Inputs) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
return verifyTOTP(values.code, loginName, organization)
|
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 attribute is set, validate MFA after it is setup, otherwise proceed as usual (when mfa is enforced to login)
|
||||||
if (checkAfter) {
|
if (checkAfter) {
|
||||||
const params = new URLSearchParams({});
|
const params = new URLSearchParams({});
|
||||||
@@ -67,35 +71,34 @@ export function TotpRegister({
|
|||||||
|
|
||||||
return router.push(`/otp/time-based?` + params);
|
return router.push(`/otp/time-based?` + params);
|
||||||
} else {
|
} else {
|
||||||
if (authRequestId && sessionId) {
|
const url =
|
||||||
const params = new URLSearchParams({
|
authRequestId && sessionId
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
authRequest: authRequestId,
|
authRequestId: authRequestId,
|
||||||
});
|
organization: organization,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: loginName
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
|
loginName: loginName,
|
||||||
|
organization: organization,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
if (organization) {
|
if (url) {
|
||||||
params.append("organization", organization);
|
return router.push(url);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
setError(e.message);
|
setError(e.message);
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -55,13 +55,19 @@ export function UsernameForm({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("An internal error occurred");
|
setError("An internal error occurred");
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res?.redirect) {
|
||||||
|
return router.push(res.redirect);
|
||||||
|
}
|
||||||
|
|
||||||
if (res?.error) {
|
if (res?.error) {
|
||||||
setError(res.error);
|
setError(res.error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Alert } from "@/components/alert";
|
import { Alert } from "@/components/alert";
|
||||||
import { resendVerification, sendVerification } from "@/lib/server/email";
|
import { resendVerification, sendVerification } from "@/lib/server/email";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Button, ButtonVariants } from "./button";
|
import { Button, ButtonVariants } from "./button";
|
||||||
@@ -23,6 +24,8 @@ type Props = {
|
|||||||
export function VerifyForm({ userId, code, isInvite, params }: Props) {
|
export function VerifyForm({ userId, code, isInvite, params }: Props) {
|
||||||
const t = useTranslations("verify");
|
const t = useTranslations("verify");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { register, handleSubmit, formState } = useForm<Inputs>({
|
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -59,7 +62,7 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
|
|||||||
): Promise<boolean | void> {
|
): Promise<boolean | void> {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
await sendVerification({
|
const response = await sendVerification({
|
||||||
code: value.code,
|
code: value.code,
|
||||||
userId,
|
userId,
|
||||||
isInvite: isInvite,
|
isInvite: isInvite,
|
||||||
@@ -71,6 +74,15 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
setError(response.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response?.redirect) {
|
||||||
|
return router.push(response?.redirect);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[isInvite, userId],
|
[isInvite, userId],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cookies } from "next/headers";
|
|||||||
|
|
||||||
export default getRequestConfig(async () => {
|
export default getRequestConfig(async () => {
|
||||||
const fallback = "en";
|
const fallback = "en";
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
const locale: string = cookiesList.get(LANGUAGE_COOKIE_NAME)?.value ?? "en";
|
const locale: string = cookiesList.get(LANGUAGE_COOKIE_NAME)?.value ?? "en";
|
||||||
|
|
||||||
const userMessages = (await import(`../../locales/${locale}.json`)).default;
|
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;
|
type SessionCookie<T> = Cookie & T;
|
||||||
|
|
||||||
function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) {
|
async function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
|
|
||||||
return cookiesList.set({
|
return cookiesList.set({
|
||||||
name: "sessions",
|
name: "sessions",
|
||||||
@@ -32,7 +32,7 @@ function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function setLanguageCookie(language: string) {
|
export async function setLanguageCookie(language: string) {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
|
|
||||||
await cookiesList.set({
|
await cookiesList.set({
|
||||||
name: LANGUAGE_COOKIE_NAME,
|
name: LANGUAGE_COOKIE_NAME,
|
||||||
@@ -46,7 +46,7 @@ export async function addSessionToCookie<T>(
|
|||||||
session: SessionCookie<T>,
|
session: SessionCookie<T>,
|
||||||
cleanup: boolean = false,
|
cleanup: boolean = false,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
let currentSessions: SessionCookie<T>[] = stringifiedCookie?.value
|
let currentSessions: SessionCookie<T>[] = stringifiedCookie?.value
|
||||||
@@ -90,7 +90,7 @@ export async function updateSessionCookie<T>(
|
|||||||
session: SessionCookie<T>,
|
session: SessionCookie<T>,
|
||||||
cleanup: boolean = false,
|
cleanup: boolean = false,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
const sessions: SessionCookie<T>[] = stringifiedCookie?.value
|
const sessions: SessionCookie<T>[] = stringifiedCookie?.value
|
||||||
@@ -121,7 +121,7 @@ export async function removeSessionFromCookie<T>(
|
|||||||
session: SessionCookie<T>,
|
session: SessionCookie<T>,
|
||||||
cleanup: boolean = false,
|
cleanup: boolean = false,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
const sessions: SessionCookie<T>[] = stringifiedCookie?.value
|
const sessions: SessionCookie<T>[] = stringifiedCookie?.value
|
||||||
@@ -143,7 +143,7 @@ export async function removeSessionFromCookie<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getMostRecentSessionCookie<T>(): Promise<any> {
|
export async function getMostRecentSessionCookie<T>(): Promise<any> {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
if (stringifiedCookie?.value) {
|
if (stringifiedCookie?.value) {
|
||||||
@@ -166,7 +166,7 @@ export async function getSessionCookieById<T>({
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
}): Promise<SessionCookie<T>> {
|
}): Promise<SessionCookie<T>> {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
if (stringifiedCookie?.value) {
|
if (stringifiedCookie?.value) {
|
||||||
@@ -194,7 +194,7 @@ export async function getSessionCookieByLoginName<T>({
|
|||||||
loginName?: string;
|
loginName?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
}): Promise<SessionCookie<T>> {
|
}): Promise<SessionCookie<T>> {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
if (stringifiedCookie?.value) {
|
if (stringifiedCookie?.value) {
|
||||||
@@ -222,7 +222,7 @@ export async function getSessionCookieByLoginName<T>({
|
|||||||
export async function getAllSessionCookieIds<T>(
|
export async function getAllSessionCookieIds<T>(
|
||||||
cleanup: boolean = false,
|
cleanup: boolean = false,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
if (stringifiedCookie?.value) {
|
if (stringifiedCookie?.value) {
|
||||||
@@ -253,7 +253,7 @@ export async function getAllSessionCookieIds<T>(
|
|||||||
export async function getAllSessions<T>(
|
export async function getAllSessions<T>(
|
||||||
cleanup: boolean = false,
|
cleanup: boolean = false,
|
||||||
): Promise<SessionCookie<T>[]> {
|
): Promise<SessionCookie<T>[]> {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
if (stringifiedCookie?.value) {
|
if (stringifiedCookie?.value) {
|
||||||
@@ -287,7 +287,7 @@ export async function getMostRecentCookieWithLoginname<T>({
|
|||||||
loginName?: string;
|
loginName?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
const cookiesList = cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
if (stringifiedCookie?.value) {
|
if (stringifiedCookie?.value) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
getSession,
|
getSession,
|
||||||
setSession,
|
setSession,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { timestampMs } from "@zitadel/client";
|
import { Duration, timestampMs } from "@zitadel/client";
|
||||||
import {
|
import {
|
||||||
Challenges,
|
Challenges,
|
||||||
RequestChallenges,
|
RequestChallenges,
|
||||||
@@ -30,6 +30,7 @@ export async function createSessionAndUpdateCookie(
|
|||||||
checks: Checks,
|
checks: Checks,
|
||||||
challenges: RequestChallenges | undefined,
|
challenges: RequestChallenges | undefined,
|
||||||
authRequestId: string | undefined,
|
authRequestId: string | undefined,
|
||||||
|
lifetime?: Duration,
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
const createdSession = await createSessionFromChecks(checks, challenges);
|
const createdSession = await createSessionFromChecks(checks, challenges);
|
||||||
|
|
||||||
@@ -82,10 +83,12 @@ export async function createSessionForIdpAndUpdateCookie(
|
|||||||
idpIntentToken?: string | undefined;
|
idpIntentToken?: string | undefined;
|
||||||
},
|
},
|
||||||
authRequestId: string | undefined,
|
authRequestId: string | undefined,
|
||||||
|
lifetime?: Duration,
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
const createdSession = await createSessionForUserIdAndIdpIntent(
|
const createdSession = await createSessionForUserIdAndIdpIntent(
|
||||||
userId,
|
userId,
|
||||||
idpIntent,
|
idpIntent,
|
||||||
|
lifetime,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (createdSession) {
|
if (createdSession) {
|
||||||
@@ -140,12 +143,14 @@ export async function setSessionAndUpdateCookie(
|
|||||||
checks?: Checks,
|
checks?: Checks,
|
||||||
challenges?: RequestChallenges,
|
challenges?: RequestChallenges,
|
||||||
authRequestId?: string,
|
authRequestId?: string,
|
||||||
|
lifetime?: Duration,
|
||||||
) {
|
) {
|
||||||
return setSession(
|
return setSession(
|
||||||
recentCookie.id,
|
recentCookie.id,
|
||||||
recentCookie.token,
|
recentCookie.token,
|
||||||
challenges,
|
challenges,
|
||||||
checks,
|
checks,
|
||||||
|
lifetime,
|
||||||
).then((updatedSession) => {
|
).then((updatedSession) => {
|
||||||
if (updatedSession) {
|
if (updatedSession) {
|
||||||
const sessionCookie: CustomCookieData = {
|
const sessionCookie: CustomCookieData = {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { create } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import { createSessionAndUpdateCookie } from "./cookie";
|
import { createSessionAndUpdateCookie } from "./cookie";
|
||||||
|
|
||||||
type VerifyUserByEmailCommand = {
|
type VerifyUserByEmailCommand = {
|
||||||
@@ -74,7 +73,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
if (session.factors?.user?.loginName) {
|
if (session.factors?.user?.loginName) {
|
||||||
params.set("loginName", 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) {
|
if (session.factors?.user?.loginName) {
|
||||||
params.set("loginName", 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,
|
successUrl: command.successUrl,
|
||||||
failureUrl: command.failureUrl,
|
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) {
|
export async function inviteUser(command: InviteUserCommand) {
|
||||||
const host = headers().get("host");
|
const host = (await headers()).get("host");
|
||||||
|
|
||||||
const human = await addHumanUser({
|
const human = await addHumanUser({
|
||||||
email: command.email,
|
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 { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
|
import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -44,7 +43,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (identityProviders.length === 1) {
|
if (identityProviders.length === 1) {
|
||||||
const host = headers().get("host");
|
const host = (await headers()).get("host");
|
||||||
const identityProviderType = identityProviders[0].type;
|
const identityProviderType = identityProviders[0].type;
|
||||||
|
|
||||||
const provider = idpTypeToSlug(identityProviderType);
|
const provider = idpTypeToSlug(identityProviderType);
|
||||||
@@ -70,7 +69,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (resp?.nextStep.case === "authUrl") {
|
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) {
|
if (identityProviders.length === 1) {
|
||||||
const host = headers().get("host");
|
const host = (await headers()).get("host");
|
||||||
const identityProviderId = identityProviders[0].idpId;
|
const identityProviderId = identityProviders[0].idpId;
|
||||||
|
|
||||||
const idp = await getIDPByID(identityProviderId);
|
const idp = await getIDPByID(identityProviderId);
|
||||||
@@ -115,7 +114,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (resp?.nextStep.case === "authUrl") {
|
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);
|
params.append("authRequestid", command.authRequestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect("/password/set?" + params);
|
return { redirect: "/password/set?" + params };
|
||||||
}
|
}
|
||||||
|
|
||||||
const methods = await listAuthenticationMethodTypes(
|
const methods = await listAuthenticationMethodTypes(
|
||||||
@@ -170,6 +169,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
const paramsVerify = new URLSearchParams({
|
const paramsVerify = new URLSearchParams({
|
||||||
loginName: session.factors?.user?.loginName,
|
loginName: session.factors?.user?.loginName,
|
||||||
userId: session.factors?.user?.id, // verify needs user id
|
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) {
|
if (command.organization || session.factors?.user?.organizationId) {
|
||||||
@@ -183,7 +183,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
paramsVerify.append("authRequestId", command.authRequestId);
|
paramsVerify.append("authRequestId", command.authRequestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect("/verify?" + paramsVerify);
|
return { redirect: "/verify?" + paramsVerify };
|
||||||
}
|
}
|
||||||
|
|
||||||
const paramsAuthenticatorSetup = new URLSearchParams({
|
const paramsAuthenticatorSetup = new URLSearchParams({
|
||||||
@@ -202,7 +202,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
paramsAuthenticatorSetup.append("authRequestId", command.authRequestId);
|
paramsAuthenticatorSetup.append("authRequestId", command.authRequestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect("/authenticator/set?" + paramsAuthenticatorSetup);
|
return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (methods.authMethodTypes.length == 1) {
|
if (methods.authMethodTypes.length == 1) {
|
||||||
@@ -224,7 +224,10 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
paramsPassword.authRequestId = command.authRequestId;
|
paramsPassword.authRequestId = command.authRequestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect("/password?" + new URLSearchParams(paramsPassword));
|
return {
|
||||||
|
redirect: "/password?" + new URLSearchParams(paramsPassword),
|
||||||
|
};
|
||||||
|
|
||||||
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
|
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
|
||||||
const paramsPasskey: any = { loginName: command.loginName };
|
const paramsPasskey: any = { loginName: command.loginName };
|
||||||
if (command.authRequestId) {
|
if (command.authRequestId) {
|
||||||
@@ -236,7 +239,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
command.organization ?? session.factors?.user?.organizationId;
|
command.organization ?? session.factors?.user?.organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect("/passkey?" + new URLSearchParams(paramsPasskey));
|
return { redirect: "/passkey?" + new URLSearchParams(paramsPasskey) };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// prefer passkey in favor of other methods
|
// prefer passkey in favor of other methods
|
||||||
@@ -255,7 +258,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
command.organization ?? session.factors?.user?.organizationId;
|
command.organization ?? session.factors?.user?.organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect("/passkey?" + new URLSearchParams(passkeyParams));
|
return { redirect: "/passkey?" + new URLSearchParams(passkeyParams) };
|
||||||
} else if (
|
} else if (
|
||||||
methods.authMethodTypes.includes(AuthenticationMethodType.IDP)
|
methods.authMethodTypes.includes(AuthenticationMethodType.IDP)
|
||||||
) {
|
) {
|
||||||
@@ -275,9 +278,9 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
command.organization ?? session.factors?.user?.organizationId;
|
command.organization ?? session.factors?.user?.organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect(
|
return {
|
||||||
"/password?" + new URLSearchParams(paramsPasswordDefault),
|
redirect: "/password?" + new URLSearchParams(paramsPasswordDefault),
|
||||||
);
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,7 +328,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
params.set("loginName", command.loginName);
|
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);
|
paramsPasswordDefault.append("organization", command.organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect("/password?" + paramsPasswordDefault);
|
return { redirect: "/password?" + paramsPasswordDefault };
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallbackToPassword
|
// fallbackToPassword
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
getSessionCookieById,
|
getSessionCookieById,
|
||||||
getSessionCookieByLoginName,
|
getSessionCookieByLoginName,
|
||||||
} from "../cookies";
|
} from "../cookies";
|
||||||
|
import { getLoginSettings } from "../zitadel";
|
||||||
|
|
||||||
export type SetOTPCommand = {
|
export type SetOTPCommand = {
|
||||||
loginName?: string;
|
loginName?: string;
|
||||||
@@ -23,22 +24,23 @@ export type SetOTPCommand = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function setOTP(command: SetOTPCommand) {
|
export async function setOTP(command: SetOTPCommand) {
|
||||||
const recentPromise = command.sessionId
|
const recentSession = command.sessionId
|
||||||
? getSessionCookieById({ sessionId: command.sessionId }).catch((error) => {
|
? await getSessionCookieById({ sessionId: command.sessionId }).catch(
|
||||||
|
(error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
})
|
},
|
||||||
|
)
|
||||||
: command.loginName
|
: command.loginName
|
||||||
? getSessionCookieByLoginName({
|
? await getSessionCookieByLoginName({
|
||||||
loginName: command.loginName,
|
loginName: command.loginName,
|
||||||
organization: command.organization,
|
organization: command.organization,
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
})
|
})
|
||||||
: getMostRecentSessionCookie().catch((error) => {
|
: await getMostRecentSessionCookie().catch((error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
return recentPromise.then((recent) => {
|
|
||||||
const checks = create(ChecksSchema, {});
|
const checks = create(ChecksSchema, {});
|
||||||
|
|
||||||
if (command.method === "time-based") {
|
if (command.method === "time-based") {
|
||||||
@@ -55,11 +57,14 @@ export async function setOTP(command: SetOTPCommand) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(command.organization);
|
||||||
|
|
||||||
return setSessionAndUpdateCookie(
|
return setSessionAndUpdateCookie(
|
||||||
recent,
|
recentSession,
|
||||||
checks,
|
checks,
|
||||||
undefined,
|
undefined,
|
||||||
command.authRequestId,
|
command.authRequestId,
|
||||||
|
loginSettings?.secondFactorCheckLifetime,
|
||||||
).then((session) => {
|
).then((session) => {
|
||||||
return {
|
return {
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
@@ -67,5 +72,4 @@ export async function setOTP(command: SetOTPCommand) {
|
|||||||
challenges: session.challenges,
|
challenges: session.challenges,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function registerPasskeyLink(
|
|||||||
sessionToken: sessionCookie.token,
|
sessionToken: sessionCookie.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
const host = headers().get("host");
|
const host = (await headers()).get("host");
|
||||||
|
|
||||||
if (!host) {
|
if (!host) {
|
||||||
throw new Error("Could not get domain");
|
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
|
// if no name is provided, try to generate one from the user agent
|
||||||
let passkeyName = command.passkeyName;
|
let passkeyName = command.passkeyName;
|
||||||
if (!!!passkeyName) {
|
if (!!!passkeyName) {
|
||||||
const headersList = headers();
|
const headersList = await headers();
|
||||||
const userAgentStructure = { headers: headersList };
|
const userAgentStructure = { headers: headersList };
|
||||||
const { browser, device, os } = userAgent(userAgentStructure);
|
const { browser, device, os } = userAgent(userAgentStructure);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
setSessionAndUpdateCookie,
|
setSessionAndUpdateCookie,
|
||||||
} from "@/lib/server/cookie";
|
} from "@/lib/server/cookie";
|
||||||
import {
|
import {
|
||||||
|
getLoginSettings,
|
||||||
getUserByID,
|
getUserByID,
|
||||||
listAuthenticationMethodTypes,
|
listAuthenticationMethodTypes,
|
||||||
listUsers,
|
listUsers,
|
||||||
@@ -16,10 +17,11 @@ import {
|
|||||||
Checks,
|
Checks,
|
||||||
ChecksSchema,
|
ChecksSchema,
|
||||||
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
} 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 { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { getNextUrl } from "../client";
|
||||||
import { getSessionCookieByLoginName } from "../cookies";
|
import { getSessionCookieByLoginName } from "../cookies";
|
||||||
|
|
||||||
type ResetPasswordCommand = {
|
type ResetPasswordCommand = {
|
||||||
@@ -28,7 +30,7 @@ type ResetPasswordCommand = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function resetPassword(command: ResetPasswordCommand) {
|
export async function resetPassword(command: ResetPasswordCommand) {
|
||||||
const host = headers().get("host");
|
const host = (await headers()).get("host");
|
||||||
|
|
||||||
const users = await listUsers({
|
const users = await listUsers({
|
||||||
loginName: command.loginName,
|
loginName: command.loginName,
|
||||||
@@ -65,6 +67,8 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
|
|
||||||
let session;
|
let session;
|
||||||
let user: User;
|
let user: User;
|
||||||
|
let loginSettings: LoginSettings | undefined;
|
||||||
|
|
||||||
if (!sessionCookie) {
|
if (!sessionCookie) {
|
||||||
const users = await listUsers({
|
const users = await listUsers({
|
||||||
loginName: command.loginName,
|
loginName: command.loginName,
|
||||||
@@ -79,10 +83,13 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
password: { password: command.checks.password?.password },
|
password: { password: command.checks.password?.password },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
loginSettings = await getLoginSettings(command.organization);
|
||||||
|
|
||||||
session = await createSessionAndUpdateCookie(
|
session = await createSessionAndUpdateCookie(
|
||||||
checks,
|
checks,
|
||||||
undefined,
|
undefined,
|
||||||
command.authRequestId,
|
command.authRequestId,
|
||||||
|
loginSettings?.passwordCheckLifetime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +101,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
command.checks,
|
command.checks,
|
||||||
undefined,
|
undefined,
|
||||||
command.authRequestId,
|
command.authRequestId,
|
||||||
|
loginSettings?.passwordCheckLifetime,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
if (!session?.factors?.user?.id) {
|
||||||
@@ -109,6 +117,12 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
user = userResponse.user;
|
user = userResponse.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!loginSettings) {
|
||||||
|
loginSettings = await getLoginSettings(
|
||||||
|
command.organization ?? session.factors?.user?.organizationId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!session?.factors?.user?.id || !sessionCookie) {
|
if (!session?.factors?.user?.id || !sessionCookie) {
|
||||||
return { error: "Could not create session for user" };
|
return { error: "Could not create session for user" };
|
||||||
}
|
}
|
||||||
@@ -153,13 +167,13 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
const factor = availableSecondFactors[0];
|
const factor = availableSecondFactors[0];
|
||||||
// if passwordless is other method, but user selected password as alternative, perform a login
|
// if passwordless is other method, but user selected password as alternative, perform a login
|
||||||
if (factor === AuthenticationMethodType.TOTP) {
|
if (factor === AuthenticationMethodType.TOTP) {
|
||||||
return redirect(`/otp/time-based?` + params);
|
return { redirect: `/otp/time-based?` + params };
|
||||||
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
||||||
return redirect(`/otp/sms?` + params);
|
return { redirect: `/otp/sms?` + params };
|
||||||
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
||||||
return redirect(`/otp/email?` + params);
|
return { redirect: `/otp/email?` + params };
|
||||||
} else if (factor === AuthenticationMethodType.U2F) {
|
} else if (factor === AuthenticationMethodType.U2F) {
|
||||||
return redirect(`/u2f?` + params);
|
return { redirect: `/u2f?` + params };
|
||||||
}
|
}
|
||||||
} else if (availableSecondFactors?.length >= 1) {
|
} else if (availableSecondFactors?.length >= 1) {
|
||||||
const params = new URLSearchParams({
|
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) {
|
} else if (user.state === UserState.INITIAL) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
loginName: session.factors.user.loginName,
|
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) {
|
} else if (command.forceMfa && !availableSecondFactors.length) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
loginName: session.factors.user.loginName,
|
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?
|
// TODO: provide a way to setup passkeys on mfa page?
|
||||||
return redirect(`/mfa/set?` + params);
|
return { redirect: `/mfa/set?` + params };
|
||||||
}
|
}
|
||||||
// TODO: implement passkey setup
|
// TODO: implement passkey setup
|
||||||
|
|
||||||
@@ -240,41 +254,28 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
// return router.push(`/passkey/set?` + params);
|
// return router.push(`/passkey/set?` + params);
|
||||||
// }
|
// }
|
||||||
else if (command.authRequestId && session.id) {
|
else if (command.authRequestId && session.id) {
|
||||||
const params = new URLSearchParams({
|
const nextUrl = await getNextUrl(
|
||||||
|
{
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
authRequest: command.authRequestId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (command.organization || session.factors?.user?.organizationId) {
|
|
||||||
params.append(
|
|
||||||
"organization",
|
|
||||||
command.organization ?? session.factors?.user?.organizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { nextStep: `/login?${params}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
// without OIDC flow
|
|
||||||
const params = new URLSearchParams(
|
|
||||||
command.authRequestId
|
|
||||||
? {
|
|
||||||
loginName: session.factors.user.loginName,
|
|
||||||
authRequestId: command.authRequestId,
|
authRequestId: command.authRequestId,
|
||||||
}
|
organization:
|
||||||
: {
|
|
||||||
loginName: session.factors.user.loginName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (command.organization || session.factors?.user?.organizationId) {
|
|
||||||
params.append(
|
|
||||||
"organization",
|
|
||||||
command.organization ?? session.factors?.user?.organizationId,
|
command.organization ?? session.factors?.user?.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return { redirect: nextUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect(`/signedin?` + params);
|
const url = await getNextUrl(
|
||||||
|
{
|
||||||
|
loginName: session.factors.user.loginName,
|
||||||
|
organization: session.factors?.user?.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { redirect: url };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function changePassword(command: {
|
export async function changePassword(command: {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { createSessionAndUpdateCookie } from "@/lib/server/cookie";
|
import { createSessionAndUpdateCookie } from "@/lib/server/cookie";
|
||||||
import { addHumanUser } from "@/lib/zitadel";
|
import { addHumanUser, getLoginSettings } from "@/lib/zitadel";
|
||||||
import { create } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import {
|
import {
|
||||||
ChecksJson,
|
ChecksJson,
|
||||||
ChecksSchema,
|
ChecksSchema,
|
||||||
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { redirect } from "next/navigation";
|
import { getNextUrl } from "../client";
|
||||||
|
|
||||||
type RegisterUserCommand = {
|
type RegisterUserCommand = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -37,6 +37,8 @@ export async function registerUser(command: RegisterUserCommand) {
|
|||||||
return { error: "Could not create user" };
|
return { error: "Could not create user" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(command.organization);
|
||||||
|
|
||||||
let checkPayload: any = {
|
let checkPayload: any = {
|
||||||
user: { search: { case: "userId", value: human.userId } },
|
user: { search: { case: "userId", value: human.userId } },
|
||||||
};
|
};
|
||||||
@@ -54,6 +56,7 @@ export async function registerUser(command: RegisterUserCommand) {
|
|||||||
checks,
|
checks,
|
||||||
undefined,
|
undefined,
|
||||||
command.authRequestId,
|
command.authRequestId,
|
||||||
|
command.password ? loginSettings?.passwordCheckLifetime : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!session || !session.factors?.user) {
|
if (!session || !session.factors?.user) {
|
||||||
@@ -70,20 +73,22 @@ export async function registerUser(command: RegisterUserCommand) {
|
|||||||
params.append("authRequestId", command.authRequestId);
|
params.append("authRequestId", command.authRequestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect("/passkey/set?" + params);
|
return { redirect: "/passkey/set?" + params };
|
||||||
} else {
|
} else {
|
||||||
const params = new URLSearchParams({
|
const url = await getNextUrl(
|
||||||
|
command.authRequestId && session.id
|
||||||
|
? {
|
||||||
|
sessionId: session.id,
|
||||||
|
authRequestId: command.authRequestId,
|
||||||
|
organization: session.factors.user.organizationId,
|
||||||
|
}
|
||||||
|
: {
|
||||||
loginName: session.factors.user.loginName,
|
loginName: session.factors.user.loginName,
|
||||||
organization: session.factors.user.organizationId,
|
organization: session.factors.user.organizationId,
|
||||||
});
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
if (command.authRequestId && session.factors.user.id) {
|
return { redirect: url };
|
||||||
params.append("authRequest", command.authRequestId);
|
|
||||||
params.append("sessionId", session.id);
|
|
||||||
|
|
||||||
return redirect("/login?" + params);
|
|
||||||
} else {
|
|
||||||
return redirect("/signedin?" + params);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,19 @@ import {
|
|||||||
createSessionForIdpAndUpdateCookie,
|
createSessionForIdpAndUpdateCookie,
|
||||||
setSessionAndUpdateCookie,
|
setSessionAndUpdateCookie,
|
||||||
} from "@/lib/server/cookie";
|
} 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 { 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 { 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 {
|
import {
|
||||||
getMostRecentSessionCookie,
|
getMostRecentSessionCookie,
|
||||||
getSessionCookieById,
|
getSessionCookieById,
|
||||||
@@ -31,7 +41,75 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) {
|
|||||||
if (!userId || !idpIntent) {
|
if (!userId || !idpIntent) {
|
||||||
throw new Error("No userId or loginName provided");
|
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 = {
|
export type UpdateSessionCommand = {
|
||||||
@@ -41,6 +119,7 @@ export type UpdateSessionCommand = {
|
|||||||
checks?: Checks;
|
checks?: Checks;
|
||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
challenges?: RequestChallenges;
|
challenges?: RequestChallenges;
|
||||||
|
lifetime?: Duration;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function updateSession(options: UpdateSessionCommand) {
|
export async function updateSession(options: UpdateSessionCommand) {
|
||||||
@@ -52,22 +131,21 @@ export async function updateSession(options: UpdateSessionCommand) {
|
|||||||
authRequestId,
|
authRequestId,
|
||||||
challenges,
|
challenges,
|
||||||
} = options;
|
} = options;
|
||||||
const sessionPromise = sessionId
|
const recentSession = sessionId
|
||||||
? getSessionCookieById({ sessionId }).catch((error) => {
|
? await getSessionCookieById({ sessionId }).catch((error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
})
|
})
|
||||||
: loginName
|
: loginName
|
||||||
? getSessionCookieByLoginName({ loginName, organization }).catch(
|
? await getSessionCookieByLoginName({ loginName, organization }).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: getMostRecentSessionCookie().catch((error) => {
|
: await getMostRecentSessionCookie().catch((error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO remove ports from host header for URL with port
|
const host = (await headers()).get("host");
|
||||||
const host = "localhost";
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
host &&
|
host &&
|
||||||
@@ -76,16 +154,24 @@ export async function updateSession(options: UpdateSessionCommand) {
|
|||||||
!challenges.webAuthN.domain
|
!challenges.webAuthN.domain
|
||||||
) {
|
) {
|
||||||
const [hostname, port] = host.split(":");
|
const [hostname, port] = host.split(":");
|
||||||
|
|
||||||
challenges.webAuthN.domain = hostname;
|
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(
|
const session = await setSessionAndUpdateCookie(
|
||||||
recent,
|
recentSession,
|
||||||
checks,
|
checks,
|
||||||
challenges,
|
challenges,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
|
lifetime,
|
||||||
);
|
);
|
||||||
|
|
||||||
// if password, check if user has MFA methods
|
// if password, check if user has MFA methods
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export async function addU2F(command: RegisterU2FCommand) {
|
|||||||
sessionToken: sessionCookie.token,
|
sessionToken: sessionCookie.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
const domain = headers().get("host");
|
const domain = (await headers()).get("host");
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
return { error: "Could not get domain" };
|
return { error: "Could not get domain" };
|
||||||
@@ -54,7 +54,7 @@ export async function addU2F(command: RegisterU2FCommand) {
|
|||||||
export async function verifyU2F(command: VerifyU2FCommand) {
|
export async function verifyU2F(command: VerifyU2FCommand) {
|
||||||
let passkeyName = command.passkeyName;
|
let passkeyName = command.passkeyName;
|
||||||
if (!!!passkeyName) {
|
if (!!!passkeyName) {
|
||||||
const headersList = headers();
|
const headersList = await headers();
|
||||||
const userAgentStructure = { headers: headersList };
|
const userAgentStructure = { headers: headersList };
|
||||||
const { browser, device, os } = userAgent(userAgentStructure);
|
const { browser, device, os } = userAgent(userAgentStructure);
|
||||||
|
|
||||||
|
|||||||
@@ -18,17 +18,11 @@ import {
|
|||||||
VerifyU2FRegistrationRequest,
|
VerifyU2FRegistrationRequest,
|
||||||
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
} 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 { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
||||||
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||||
import { BrandingSettingsSchema } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
|
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_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 type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||||
import {
|
import {
|
||||||
NotificationType,
|
NotificationType,
|
||||||
@@ -43,15 +37,9 @@ import {
|
|||||||
User,
|
User,
|
||||||
UserState,
|
UserState,
|
||||||
} from "@zitadel/proto/zitadel/user/v2/user_pb";
|
} 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";
|
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(
|
const transport = createServerTransport(
|
||||||
process.env.ZITADEL_SERVICE_USER_TOKEN!,
|
process.env.ZITADEL_SERVICE_USER_TOKEN!,
|
||||||
{ baseUrl: process.env.ZITADEL_API_URL! },
|
{ baseUrl: process.env.ZITADEL_API_URL! },
|
||||||
@@ -62,47 +50,31 @@ export const userService = createUserServiceClient(transport);
|
|||||||
export const oidcService = createOIDCServiceClient(transport);
|
export const oidcService = createOIDCServiceClient(transport);
|
||||||
export const idpService = createIdpServiceClient(transport);
|
export const idpService = createIdpServiceClient(transport);
|
||||||
export const orgService = createOrganizationServiceClient(transport);
|
export const orgService = createOrganizationServiceClient(transport);
|
||||||
|
|
||||||
export const settingsService = createSettingsServiceClient(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) {
|
export async function getBrandingSettings(organization?: string) {
|
||||||
return unstable_cache(
|
const callback = settingsService
|
||||||
async () => {
|
|
||||||
return await settingsService
|
|
||||||
.getBrandingSettings({ ctx: makeReqCtx(organization) }, {})
|
.getBrandingSettings({ ctx: makeReqCtx(organization) }, {})
|
||||||
.then((resp) =>
|
.then((resp) => (resp.settings ? resp.settings : undefined));
|
||||||
resp.settings
|
|
||||||
? toJson(BrandingSettingsSchema, resp.settings)
|
return useCache ? cacheWrapper(callback) : callback;
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
["brandingSettings", organization ?? "default"],
|
|
||||||
{
|
|
||||||
revalidate: CACHE_REVALIDATION_INTERVAL_IN_SECONDS,
|
|
||||||
tags: ["brandingSettings"],
|
|
||||||
},
|
|
||||||
)().then((resp) =>
|
|
||||||
resp ? fromJson(BrandingSettingsSchema, resp) : undefined,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLoginSettings(orgId?: string) {
|
export async function getLoginSettings(orgId?: string) {
|
||||||
return unstable_cache(
|
const callback = settingsService
|
||||||
async () => {
|
|
||||||
return await settingsService
|
|
||||||
.getLoginSettings({ ctx: makeReqCtx(orgId) }, {})
|
.getLoginSettings({ ctx: makeReqCtx(orgId) }, {})
|
||||||
.then((resp) =>
|
.then((resp) => (resp.settings ? resp.settings : undefined));
|
||||||
resp.settings
|
|
||||||
? toJson(LoginSettingsSchema, resp.settings)
|
return useCache ? cacheWrapper(callback) : callback;
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
["loginSettings", orgId ?? "default"],
|
|
||||||
{
|
|
||||||
revalidate: CACHE_REVALIDATION_INTERVAL_IN_SECONDS,
|
|
||||||
tags: ["loginSettings"],
|
|
||||||
},
|
|
||||||
)().then((resp) => (resp ? fromJson(LoginSettingsSchema, resp) : undefined));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listIDPLinks(userId: string) {
|
export async function listIDPLinks(userId: string) {
|
||||||
@@ -132,65 +104,39 @@ export async function registerTOTP(userId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getGeneralSettings() {
|
export async function getGeneralSettings() {
|
||||||
return settingsService
|
const callback = settingsService
|
||||||
.getGeneralSettings({}, {})
|
.getGeneralSettings({}, {})
|
||||||
.then((resp) => resp.supportedLanguages);
|
.then((resp) => resp.supportedLanguages);
|
||||||
|
|
||||||
|
return useCache ? cacheWrapper(callback) : callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLegalAndSupportSettings(organization?: string) {
|
export async function getLegalAndSupportSettings(organization?: string) {
|
||||||
return unstable_cache(
|
const callback = settingsService
|
||||||
async () => {
|
|
||||||
return await settingsService
|
|
||||||
.getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {})
|
.getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {})
|
||||||
.then((resp) =>
|
.then((resp) => (resp.settings ? resp.settings : undefined));
|
||||||
resp.settings
|
|
||||||
? toJson(LegalAndSupportSettingsSchema, resp.settings)
|
return useCache ? cacheWrapper(callback) : callback;
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
["legalAndSupportSettings", organization ?? "default"],
|
|
||||||
{
|
|
||||||
revalidate: CACHE_REVALIDATION_INTERVAL_IN_SECONDS,
|
|
||||||
tags: ["legalAndSupportSettings"],
|
|
||||||
},
|
|
||||||
)().then((resp) =>
|
|
||||||
resp ? fromJson(LegalAndSupportSettingsSchema, resp) : undefined,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPasswordComplexitySettings(organization?: string) {
|
export async function getPasswordComplexitySettings(organization?: string) {
|
||||||
return unstable_cache(
|
const callback = settingsService
|
||||||
async () => {
|
|
||||||
return await settingsService
|
|
||||||
.getPasswordComplexitySettings({ ctx: makeReqCtx(organization) })
|
.getPasswordComplexitySettings({ ctx: makeReqCtx(organization) })
|
||||||
.then((resp) =>
|
.then((resp) => (resp.settings ? resp.settings : undefined));
|
||||||
resp.settings
|
|
||||||
? toJson(PasswordComplexitySettingsSchema, resp.settings)
|
return useCache ? cacheWrapper(callback) : callback;
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
["complexitySettings", organization ?? "default"],
|
|
||||||
{
|
|
||||||
revalidate: CACHE_REVALIDATION_INTERVAL_IN_SECONDS,
|
|
||||||
tags: ["complexitySettings"],
|
|
||||||
},
|
|
||||||
)().then((resp) =>
|
|
||||||
resp ? fromJson(PasswordComplexitySettingsSchema, resp) : undefined,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSessionFromChecks(
|
export async function createSessionFromChecks(
|
||||||
checks: Checks,
|
checks: Checks,
|
||||||
challenges: RequestChallenges | undefined,
|
challenges: RequestChallenges | undefined,
|
||||||
|
lifetime?: Duration,
|
||||||
) {
|
) {
|
||||||
return sessionService.createSession(
|
return sessionService.createSession(
|
||||||
{
|
{
|
||||||
checks: checks,
|
checks: checks,
|
||||||
challenges,
|
challenges,
|
||||||
lifetime: {
|
lifetime,
|
||||||
seconds: BigInt(SESSION_LIFETIME_S),
|
|
||||||
nanos: 0,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@@ -202,6 +148,7 @@ export async function createSessionForUserIdAndIdpIntent(
|
|||||||
idpIntentId?: string | undefined;
|
idpIntentId?: string | undefined;
|
||||||
idpIntentToken?: string | undefined;
|
idpIntentToken?: string | undefined;
|
||||||
},
|
},
|
||||||
|
lifetime?: Duration,
|
||||||
) {
|
) {
|
||||||
return sessionService.createSession({
|
return sessionService.createSession({
|
||||||
checks: {
|
checks: {
|
||||||
@@ -213,10 +160,7 @@ export async function createSessionForUserIdAndIdpIntent(
|
|||||||
},
|
},
|
||||||
idpIntent,
|
idpIntent,
|
||||||
},
|
},
|
||||||
// lifetime: {
|
lifetime,
|
||||||
// seconds: 300,
|
|
||||||
// nanos: 0,
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +169,7 @@ export async function setSession(
|
|||||||
sessionToken: string,
|
sessionToken: string,
|
||||||
challenges: RequestChallenges | undefined,
|
challenges: RequestChallenges | undefined,
|
||||||
checks?: Checks,
|
checks?: Checks,
|
||||||
|
lifetime?: Duration,
|
||||||
) {
|
) {
|
||||||
return sessionService.setSession(
|
return sessionService.setSession(
|
||||||
{
|
{
|
||||||
@@ -233,6 +178,7 @@ export async function setSession(
|
|||||||
challenges,
|
challenges,
|
||||||
checks: checks ? checks : {},
|
checks: checks ? checks : {},
|
||||||
metadata: {},
|
metadata: {},
|
||||||
|
lifetime,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// include styles from the ui package
|
// include styles from the ui package
|
||||||
@import "./vars.scss";
|
@use "./vars.scss";
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@@ -24,3 +24,42 @@ html {
|
|||||||
.form-checkbox:checked {
|
.form-checkbox:checked {
|
||||||
background-image: url("/checkbox.svg");
|
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 = {
|
let colors = {
|
||||||
background: { light: { contrast: {} }, dark: { contrast: {} } },
|
background: { light: { contrast: {} }, dark: { contrast: {} } },
|
||||||
@@ -35,7 +35,7 @@ types.forEach((type) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
export default {
|
||||||
presets: [sharedConfig],
|
presets: [sharedConfig],
|
||||||
darkMode: "class",
|
darkMode: "class",
|
||||||
content: ["./src/**/*.{js,ts,jsx,tsx}"],
|
content: ["./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"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": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
parser: "@babel/eslint-parser",
|
||||||
extends: ["next", "turbo", "prettier"],
|
extends: ["next", "turbo", "prettier"],
|
||||||
rules: {
|
rules: {
|
||||||
"@next/next/no-html-link-for-pages": "off",
|
"@next/next/no-html-link-for-pages": "off",
|
||||||
},
|
},
|
||||||
|
parserOptions: {
|
||||||
|
requireConfigFile: false,
|
||||||
|
babelOptions: {
|
||||||
|
presets: ["next/babel"],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-config-next": "^14.2.3",
|
|
||||||
"@typescript-eslint/parser": "^7.9.0",
|
"@typescript-eslint/parser": "^7.9.0",
|
||||||
|
"eslint-config-next": "^14.2.18",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-config-turbo": "^2.0.9",
|
||||||
"eslint-plugin-react": "^7.34.1",
|
"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
|
// TODO: Move this to `./protobuf.ts` and export it from there
|
||||||
export { create, fromJson, toJson } from "@bufbuild/protobuf";
|
export { create, fromJson, toJson } from "@bufbuild/protobuf";
|
||||||
export { TimestampSchema, timestampDate, timestampFromDate, timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt";
|
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} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
export default {
|
||||||
content: [
|
content: ["./app/**/*.{js,ts,jsx,tsx}", "./page/**/*.{js,ts,jsx,tsx}", "./ui/**/*.{js,ts,jsx,tsx}"],
|
||||||
"./app/**/*.{js,ts,jsx,tsx}",
|
|
||||||
"./page/**/*.{js,ts,jsx,tsx}",
|
|
||||||
"./ui/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
future: {
|
future: {
|
||||||
hoverOnlyWhenSupported: true,
|
hoverOnlyWhenSupported: true,
|
||||||
},
|
},
|
||||||
@@ -48,10 +44,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
backgroundImage: ({ theme }) => ({
|
backgroundImage: ({ theme }) => ({
|
||||||
"dark-vc-border-gradient": `radial-gradient(at left top, ${theme(
|
"dark-vc-border-gradient": `radial-gradient(at left top, ${theme(
|
||||||
"colors.gray.800"
|
"colors.gray.800",
|
||||||
)}, 50px, ${theme("colors.gray.800")} 50%)`,
|
)}, 50px, ${theme("colors.gray.800")} 50%)`,
|
||||||
"vc-border-gradient": `radial-gradient(at left top, ${theme(
|
"vc-border-gradient": `radial-gradient(at left top, ${theme(
|
||||||
"colors.gray.200"
|
"colors.gray.200",
|
||||||
)}, 50px, ${theme("colors.gray.300")} 50%)`,
|
)}, 50px, ${theme("colors.gray.300")} 50%)`,
|
||||||
}),
|
}),
|
||||||
keyframes: ({ theme }) => ({
|
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