fix(login): prettier setup (#11798)

# Which Problems Are Solved
Invalid formatting in the login codebase due to invalid prettier
configuration

# How the Problems Are Solved
Fxed .prettierignore wildcards for login source files

# Additional Changes
Removed .prettierrc file because there is already a prettier.config.mjs
file

# Additional Context
This commit is contained in:
Ramon
2026-03-10 13:30:20 +00:00
committed by GitHub
parent 33c635fbc3
commit 057fb8dff2
156 changed files with 761 additions and 1231 deletions
+5 -2
View File
@@ -1,6 +1,9 @@
*
!constants
!constants/**
!src
!src/**
!locales
!scripts/healthcheck.mjs
!scripts/server.mjs
!locales/**
!scripts
!scripts/**
-6
View File
@@ -1,6 +0,0 @@
{
"printWidth": 125,
"trailingComma": "all",
"plugins": ["prettier-plugin-organize-imports"],
"filepath": ""
}
+1 -1
View File
@@ -1,5 +1,5 @@
export default {
printWidth: 80,
printWidth: 125,
tabWidth: 2,
useTabs: false,
semi: true,
+1 -1
View File
@@ -1,5 +1,5 @@
import * as https from "node:https";
import * as http from "node:http";
import * as https from "node:https";
const scheme = process.env.ZITADEL_TLS_ENABLED === "true" ? "https" : "http";
const port = process.env.PORT || "3000";
+1 -1
View File
@@ -1,4 +1,4 @@
import { readFileSync, accessSync, constants } from "node:fs";
import { accessSync, constants, readFileSync } from "node:fs";
import { createServer } from "node:https";
import { createRequire } from "node:module";
+3 -5
View File
@@ -21,8 +21,7 @@ async function loadSessions({ serviceConfig }: { serviceConfig: ServiceConfig })
const cookieIds = await getAllSessionCookieIds();
if (cookieIds && cookieIds.length) {
const response = await listSessions({ serviceConfig, ids: cookieIds.filter((id) => !!id) as string[],
});
const response = await listSessions({ serviceConfig, ids: cookieIds.filter((id) => !!id) as string[] });
return response?.sessions ?? [];
} else {
console.info("No session cookie found.");
@@ -41,7 +40,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
let defaultOrganization;
if (!organization) {
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
const org: Organization | null = await getDefaultOrg({ serviceConfig });
if (org) {
defaultOrganization = org.id;
}
@@ -49,8 +48,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
let sessions = await loadSessions({ serviceConfig });
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
});
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization });
const params = new URLSearchParams();
@@ -24,8 +24,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
const _headers = await headers();
const { serviceConfig } = getServiceConfig(_headers);
const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({ serviceConfig, userCode,
});
const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({ serviceConfig, userCode });
if (!deviceAuthorizationRequest) {
return (
@@ -37,14 +36,13 @@ export default async function Page(props: { searchParams: Promise<Record<string
let defaultOrganization;
if (!organization) {
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
const org: Organization | null = await getDefaultOrg({ serviceConfig });
if (org) {
defaultOrganization = org.id;
}
}
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
});
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization });
const params = new URLSearchParams();
+2 -3
View File
@@ -24,14 +24,13 @@ export default async function Page(props: { searchParams: Promise<Record<string
let defaultOrganization;
if (!organization) {
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
const org: Organization | null = await getDefaultOrg({ serviceConfig });
if (org) {
defaultOrganization = org.id;
}
}
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
});
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization });
return (
<DynamicTheme branding={branding}>
@@ -24,14 +24,13 @@ export default async function Page(props: { searchParams: Promise<Record<string
let defaultOrganization;
if (!organization) {
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
const org: Organization | null = await getDefaultOrg({ serviceConfig });
if (org) {
defaultOrganization = org.id;
}
}
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
});
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization });
return (
<DynamicTheme branding={branding}>
@@ -51,7 +50,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
{postErrorRedirectUrl && (
<Link href={postErrorRedirectUrl}>
<Button className="bg-primary-light-500 hover:bg-primary-light-400 dark:bg-primary-dark-500 dark:hover:bg-primary-dark-400 w-full rounded-md px-4 py-3 text-center transition-all">
<Button className="w-full rounded-md bg-primary-light-500 px-4 py-3 text-center transition-all hover:bg-primary-light-400 dark:bg-primary-dark-500 dark:hover:bg-primary-dark-400">
<Translated i18nKey="accountNotFound.backToLogin" namespace="idp" />
</Button>
</Link>
@@ -20,11 +20,9 @@ export default async function Page(props: {
const _headers = await headers();
const { serviceConfig } = getServiceConfig(_headers);
const branding = await getBrandingSettings({ serviceConfig, organization,
});
const branding = await getBrandingSettings({ serviceConfig, organization });
const loginSettings = await getLoginSettings({ serviceConfig, organization,
});
const loginSettings = await getLoginSettings({ serviceConfig, organization });
let authMethods: AuthenticationMethodType[] = [];
let user: User | undefined = undefined;
@@ -39,8 +37,7 @@ export default async function Page(props: {
}
if (userId) {
const userResponse = await getUserByID({ serviceConfig, userId,
});
const userResponse = await getUserByID({ serviceConfig, userId });
if (userResponse) {
user = userResponse.user;
if (user?.type.case === "human") {
@@ -52,8 +49,7 @@ export default async function Page(props: {
}
}
const authMethodsResponse = await listAuthenticationMethodTypes({ serviceConfig, userId,
});
const authMethodsResponse = await listAuthenticationMethodTypes({ serviceConfig, userId });
if (authMethodsResponse.authMethodTypes) {
authMethods = authMethodsResponse.authMethodTypes;
}
@@ -24,14 +24,13 @@ export default async function Page(props: { searchParams: Promise<Record<string
let defaultOrganization;
if (!organization) {
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
const org: Organization | null = await getDefaultOrg({ serviceConfig });
if (org) {
defaultOrganization = org.id;
}
}
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
});
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization });
return (
<DynamicTheme branding={branding}>
@@ -51,7 +50,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
{postErrorRedirectUrl && (
<Link href={postErrorRedirectUrl}>
<Button className="bg-primary-light-500 hover:bg-primary-light-400 dark:bg-primary-dark-500 dark:hover:bg-primary-dark-400 w-full rounded-md px-4 py-3 text-center transition-all">
<Button className="w-full rounded-md bg-primary-light-500 px-4 py-3 text-center transition-all hover:bg-primary-light-400 dark:bg-primary-dark-500 dark:hover:bg-primary-dark-400">
<Translated i18nKey="registrationFailed.backToLogin" namespace="idp" />
</Button>
</Link>
+3 -12
View File
@@ -11,15 +11,7 @@ export default async function Page(props: {
params: Promise<{ provider: string }>;
}) {
const searchParams = await props.searchParams;
const {
idpId,
organization,
link,
requestId,
postErrorRedirectUrl,
linkToSessionId,
linkFingerprint,
} = searchParams;
const { idpId, organization, link, requestId, postErrorRedirectUrl, linkToSessionId, linkFingerprint } = searchParams;
if (!idpId) {
throw new Error("No idpId provided in searchParams");
@@ -30,14 +22,13 @@ export default async function Page(props: {
let defaultOrganization;
if (!organization) {
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
const org: Organization | null = await getDefaultOrg({ serviceConfig });
if (org) {
defaultOrganization = org.id;
}
}
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
});
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization });
// return login failed if no linking or creation is allowed and no user was found
return (
+2 -4
View File
@@ -21,13 +21,11 @@ export default async function Page(props: { searchParams: Promise<Record<string
const _headers = await headers();
const { serviceConfig } = getServiceConfig(_headers);
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization,
}).then((resp) => {
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization }).then((resp) => {
return resp.identityProviders;
});
const branding = await getBrandingSettings({ serviceConfig, organization,
});
const branding = await getBrandingSettings({ serviceConfig, organization });
return (
<DynamicTheme branding={branding}>
+8 -8
View File
@@ -5,16 +5,16 @@ import { LanguageProvider } from "@/components/language-provider";
import { LanguageSwitcher } from "@/components/language-switcher";
import { Skeleton } from "@/components/skeleton";
import { ThemeProvider } from "@/components/theme-provider";
import * as Tooltip from "@radix-ui/react-tooltip";
import { Lato } from "next/font/google";
import React, { Suspense } from "react";
import ThemeSwitch from "@/components/theme-switch";
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { headers } from "next/headers";
import { LANGS, getLanguage } from "@/lib/i18n";
import { getServiceConfig } from "@/lib/service-url";
import { getAllowedLanguages } from "@/lib/zitadel";
import { LANGS, getLanguage } from "@/lib/i18n";
import * as Tooltip from "@radix-ui/react-tooltip";
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Lato } from "next/font/google";
import { headers } from "next/headers";
import React, { Suspense } from "react";
const lato = Lato({
weight: ["400", "700", "900"],
@@ -70,7 +70,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
>
<div className="relative mx-auto w-full max-w-[1100px] py-8">
<div>{children}</div>
<div className="flex flex-row items-center justify-end space-x-4 py-4 px-4 md:px-8 max-w-[440px] mx-auto md:max-w-full">
<div className="mx-auto flex max-w-[440px] flex-row items-center justify-end space-x-4 px-4 py-4 md:max-w-full md:px-8">
<LanguageSwitcher languages={languages} />
<ThemeSwitch />
</div>
+12 -10
View File
@@ -57,16 +57,18 @@ export default async function Page(props: { searchParams: Promise<Record<string
</div>
<div className="w-full">
{loginSettings?.allowLocalAuthentication && <UsernameForm
loginName={loginName}
requestId={requestId}
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
defaultOrganization={defaultOrganization}
loginSettings={loginSettings}
suffix={suffix}
submit={submit}
allowRegister={!!loginSettings?.allowRegister}
></UsernameForm>}
{loginSettings?.allowLocalAuthentication && (
<UsernameForm
loginName={loginName}
requestId={requestId}
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
defaultOrganization={defaultOrganization}
loginSettings={loginSettings}
suffix={suffix}
submit={submit}
allowRegister={!!loginSettings?.allowRegister}
></UsernameForm>
)}
{loginSettings?.allowExternalIdp && !!identityProviders?.length && (
<div className="w-full pb-4 pt-6">
@@ -12,8 +12,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
const { organization } = searchParams;
const branding = await getBrandingSettings({ serviceConfig, organization,
});
const branding = await getBrandingSettings({ serviceConfig, organization });
return (
<DynamicTheme branding={branding}>
@@ -26,12 +26,12 @@ export default async function Page(props: {
const _headers = await headers();
const { serviceConfig } = getServiceConfig(_headers);
const branding = await getBrandingSettings({ serviceConfig, organization,
});
const loginSettings = await getLoginSettings({ serviceConfig, organization,
});
const branding = await getBrandingSettings({ serviceConfig, organization });
const loginSettings = await getLoginSettings({ serviceConfig, organization });
const session = await loadMostRecentSession({ serviceConfig, sessionParams: {
const session = await loadMostRecentSession({
serviceConfig,
sessionParams: {
loginName,
organization,
},
@@ -40,8 +40,7 @@ export default async function Page(props: {
let totpResponse: RegisterTOTPResponse | undefined, error: Error | undefined;
if (session && session.factors?.user?.id) {
if (method === "time-based") {
await registerTOTP({ serviceConfig, userId: session.factors.user.id,
})
await registerTOTP({ serviceConfig, userId: session.factors.user.id })
.then((resp) => {
if (resp) {
totpResponse = resp;
@@ -51,14 +50,12 @@ export default async function Page(props: {
error = err;
});
} else if (method === "sms") {
await addOTPSMS({ serviceConfig, userId: session.factors.user.id,
}).catch((_error) => {
await addOTPSMS({ serviceConfig, userId: session.factors.user.id }).catch((_error) => {
// TODO: Throw this error?
new Error("Could not add OTP via SMS");
});
} else if (method === "email") {
await addOTPEmail({ serviceConfig, userId: session.factors.user.id,
}).catch((_error) => {
await addOTPEmail({ serviceConfig, userId: session.factors.user.id }).catch((_error) => {
// TODO: Throw this error?
new Error("Could not add OTP via Email");
});
@@ -28,21 +28,21 @@ export default async function Page(props: { searchParams: Promise<Record<string
// also allow no session to be found for userId-based flows
let session: Session | undefined;
if (loginName) {
session = await loadMostRecentSession({ serviceConfig, sessionParams: {
session = await loadMostRecentSession({
serviceConfig,
sessionParams: {
loginName,
organization,
},
});
}
const branding = await getBrandingSettings({ serviceConfig, organization,
});
const branding = await getBrandingSettings({ serviceConfig, organization });
let user: User | undefined;
let displayName: string | undefined;
if (userId) {
const userResponse = await getUserByID({ serviceConfig, userId,
});
const userResponse = await getUserByID({ serviceConfig, userId });
user = userResponse.user;
if (user?.type.case === "human") {
@@ -3,8 +3,8 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { SetPasswordForm } from "@/components/set-password-form";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar";
import { getServiceConfig } from "@/lib/service-url";
import { UNKNOWN_USER_ID } from "@/lib/constants";
import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import {
getBrandingSettings,
@@ -14,12 +14,12 @@ import {
getUserByID,
searchUsers,
} from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { headers } from "next/headers";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations("password");
+18 -26
View File
@@ -31,25 +31,20 @@ export default async function Page(props: { searchParams: Promise<Record<string
const { serviceConfig } = getServiceConfig(_headers);
if (!organization) {
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
const org: Organization | null = await getDefaultOrg({ serviceConfig });
if (org) {
organization = org.id;
}
}
const legal = await getLegalAndSupportSettings({ serviceConfig, organization,
});
const passwordComplexitySettings = await getPasswordComplexitySettings({ serviceConfig, organization,
});
const legal = await getLegalAndSupportSettings({ serviceConfig, organization });
const passwordComplexitySettings = await getPasswordComplexitySettings({ serviceConfig, organization });
const branding = await getBrandingSettings({ serviceConfig, organization,
});
const branding = await getBrandingSettings({ serviceConfig, organization });
const loginSettings = await getLoginSettings({ serviceConfig, organization,
});
const loginSettings = await getLoginSettings({ serviceConfig, organization });
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization,
}).then((resp) => {
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization }).then((resp) => {
return resp.identityProviders.filter((idp) => {
return idp.options?.isAutoCreation || idp.options?.isCreationAllowed; // check if IDP allows to create account automatically or manual creation is allowed
});
@@ -105,21 +100,18 @@ export default async function Page(props: { searchParams: Promise<Record<string
</Alert>
)}
{legal &&
passwordComplexitySettings &&
organization &&
loginSettings.allowLocalAuthentication && (
<RegisterForm
idpCount={!loginSettings?.allowExternalIdp ? 0 : identityProviders.length}
legal={legal}
organization={organization}
firstname={firstname}
lastname={lastname}
email={email}
requestId={requestId}
loginSettings={loginSettings}
></RegisterForm>
)}
{legal && passwordComplexitySettings && organization && loginSettings.allowLocalAuthentication && (
<RegisterForm
idpCount={!loginSettings?.allowExternalIdp ? 0 : identityProviders.length}
legal={legal}
organization={organization}
firstname={firstname}
lastname={lastname}
email={email}
requestId={requestId}
loginSettings={loginSettings}
></RegisterForm>
)}
{loginSettings?.allowExternalIdp && !!identityProviders.length && (
<>
@@ -21,7 +21,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
const { serviceConfig } = getServiceConfig(_headers);
if (!organization) {
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
const org: Organization | null = await getDefaultOrg({ serviceConfig });
if (org) {
organization = org.id;
}
@@ -29,16 +29,12 @@ export default async function Page(props: { searchParams: Promise<Record<string
const missingData = !firstname || !lastname || !email || !organization;
const legal = await getLegalAndSupportSettings({ serviceConfig, organization,
});
const passwordComplexitySettings = await getPasswordComplexitySettings({ serviceConfig, organization,
});
const legal = await getLegalAndSupportSettings({ serviceConfig, organization });
const passwordComplexitySettings = await getPasswordComplexitySettings({ serviceConfig, organization });
const branding = await getBrandingSettings({ serviceConfig, organization,
});
const branding = await getBrandingSettings({ serviceConfig, organization });
const loginSettings = await getLoginSettings({ serviceConfig, organization,
});
const loginSettings = await getLoginSettings({ serviceConfig, organization });
return missingData ? (
<DynamicTheme branding={branding}>
+1 -3
View File
@@ -85,9 +85,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
}
const redirectUri = await resolveRedirectUri(
requestId && sessionId
? { sessionId, requestId }
: { loginName: loginName ?? sessionFactors?.factors?.user?.loginName },
requestId && sessionId ? { sessionId, requestId } : { loginName: loginName ?? sessionFactors?.factors?.user?.loginName },
loginSettings?.defaultRedirectUri,
);
+4 -3
View File
@@ -23,14 +23,15 @@ export default async function Page(props: { searchParams: Promise<Record<string
const _headers = await headers();
const { serviceConfig } = getServiceConfig(_headers);
const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: {
const sessionFactors = await loadMostRecentSession({
serviceConfig,
sessionParams: {
loginName,
organization,
},
});
const branding = await getBrandingSettings({ serviceConfig, organization,
});
const branding = await getBrandingSettings({ serviceConfig, organization });
return (
<DynamicTheme branding={branding}>
+1 -1
View File
@@ -9,8 +9,8 @@ import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify";
import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getLoginSettings, getUserByID, searchUsers } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { headers } from "next/headers";
@@ -15,13 +15,13 @@ export default async function Page(props: { searchParams: Promise<any> }) {
const { loginName, organization, userId } = searchParams;
const branding = await getBrandingSettings({ serviceConfig, organization,
});
const branding = await getBrandingSettings({ serviceConfig, organization });
const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
}).catch((error) => {
console.warn("Error loading session:", error);
});
const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization } }).catch(
(error) => {
console.warn("Error loading session:", error);
},
);
const id = userId ?? sessionFactors?.factors?.user?.id;
@@ -29,8 +29,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
throw Error("Failed to get user id");
}
const userResponse = await getUserByID({ serviceConfig, userId: id,
});
const userResponse = await getUserByID({ serviceConfig, userId: id });
let user: User | undefined;
let human: HumanUser | undefined;
+1 -7
View File
@@ -5,13 +5,7 @@ import { Button } from "@/components/button";
import { ThemeWrapper } from "@/components/theme-wrapper";
import { Translated } from "@/components/translated";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
return (
// global-error must include html and body tags
<html>
+2 -2
View File
@@ -1,7 +1,7 @@
import { isRSCRequest, validateAuthRequest } from "@/lib/auth-utils";
import { getAllSessions } from "@/lib/cookies";
import { FlowInitiationParams, handleOIDCFlowInitiation, handleSAMLFlowInitiation } from "@/lib/server/flow-initiation";
import { getServiceConfig } from "@/lib/service-url";
import { validateAuthRequest, isRSCRequest } from "@/lib/auth-utils";
import { handleOIDCFlowInitiation, handleSAMLFlowInitiation, FlowInitiationParams } from "@/lib/server/flow-initiation";
import { listSessions, ServiceConfig } from "@/lib/zitadel";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { headers } from "next/headers";
+4 -4
View File
@@ -1,14 +1,14 @@
import { NextResponse } from "next/server";
import { trace, metrics, SpanStatusCode } from "@opentelemetry/api";
import { createLogger } from "@/lib/logger";
import {
recordAuthAttempt,
recordAuthSuccess,
recordAuthFailure,
recordRequestStart,
recordAuthSuccess,
recordRequestEnd,
recordRequestStart,
recordSessionCreationDuration,
} from "@/lib/metrics";
import { metrics, SpanStatusCode, trace } from "@opentelemetry/api";
import { NextResponse } from "next/server";
export const runtime = "nodejs";
const logger = createLogger("otel-test");
+3 -9
View File
@@ -9,20 +9,14 @@ export async function GET() {
const _headers = await headers();
const { serviceConfig } = getServiceConfig(_headers);
const settingsService: Client<typeof SettingsService> =
await createServiceForHost(SettingsService, serviceConfig);
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
const settings = await settingsService
.getSecuritySettings({})
.then((resp) => (resp.settings ? resp.settings : undefined));
const settings = await settingsService.getSecuritySettings({}).then((resp) => (resp.settings ? resp.settings : undefined));
const response = NextResponse.json({ settings }, { status: 200 });
// Add Cache-Control header to cache the response for up to 1 hour
response.headers.set(
"Cache-Control",
"public, max-age=3600, stale-while-revalidate=86400",
);
response.headers.set("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400");
return response;
}
+1 -6
View File
@@ -13,12 +13,7 @@ export function AddressBar({ domain }: Props) {
return (
<div className="flex items-center space-x-2 overflow-hidden p-3.5 lg:px-5 lg:py-3">
<div className="text-gray-600">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
+9 -20
View File
@@ -1,7 +1,4 @@
import {
ExclamationTriangleIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import { ExclamationTriangleIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
import { clsx } from "clsx";
import { ReactNode } from "react";
@@ -18,27 +15,19 @@ export enum AlertType {
const yellow =
"border-yellow-600/40 dark:border-yellow-500/20 bg-yellow-200/30 text-yellow-600 dark:bg-yellow-700/20 dark:text-yellow-200";
// const red =
"border-red-600/40 dark:border-red-500/20 bg-red-200/30 text-red-600 dark:bg-red-700/20 dark:text-red-200";
const neutral =
"border-divider-light dark:border-divider-dark bg-black/5 text-gray-600 dark:bg-white/10 dark:text-gray-200";
// "border-red-600/40 dark:border-red-500/20 bg-red-200/30 text-red-600 dark:bg-red-700/20 dark:text-red-200";
const neutral = "border-divider-light dark:border-divider-dark bg-black/5 text-gray-600 dark:bg-white/10 dark:text-gray-200";
export function Alert({ children, type = AlertType.ALERT }: Props) {
return (
<div
className={clsx(
"flex scroll-px-40 flex-row items-center justify-center rounded-md border py-2 pr-2",
{
[yellow]: type === AlertType.ALERT,
[neutral]: type === AlertType.INFO,
},
)}
className={clsx("flex scroll-px-40 flex-row items-center justify-center rounded-md border py-2 pr-2", {
[yellow]: type === AlertType.ALERT,
[neutral]: type === AlertType.INFO,
})}
>
{type === AlertType.ALERT && (
<ExclamationTriangleIcon className="ml-2 mr-2 h-5 w-5 flex-shrink-0" />
)}
{type === AlertType.INFO && (
<InformationCircleIcon className="ml-2 mr-2 h-5 w-5 flex-shrink-0" />
)}
{type === AlertType.ALERT && <ExclamationTriangleIcon className="ml-2 mr-2 h-5 w-5 flex-shrink-0" />}
{type === AlertType.INFO && <InformationCircleIcon className="ml-2 mr-2 h-5 w-5 flex-shrink-0" />}
<span className="w-full text-sm">{children}</span>
</div>
);
+9 -53
View File
@@ -7,20 +7,10 @@ import { BadgeState, StateBadge } from "./state-badge";
const cardClasses = (alreadyAdded: boolean) =>
clsx(
"relative bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 border border-divider-light dark:border-divider-dark transition-all ",
alreadyAdded
? "opacity-50 cursor-default"
: "hover:shadow-lg hover:dark:bg-white/10",
alreadyAdded ? "opacity-50 cursor-default" : "hover:shadow-lg hover:dark:bg-white/10",
);
const LinkWrapper = ({
alreadyAdded,
children,
link,
}: {
alreadyAdded: boolean;
children: ReactNode;
link: string;
}) => {
const LinkWrapper = ({ alreadyAdded, children, link }: { alreadyAdded: boolean; children: ReactNode; link: string }) => {
return !alreadyAdded ? (
<Link href={link} className={cardClasses(alreadyAdded)}>
{children}
@@ -33,12 +23,7 @@ const LinkWrapper = ({
export const TOTP = (alreadyAdded: boolean, link: string) => {
return (
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div
className={clsx(
"flex items-center font-medium",
alreadyAdded ? "opacity-50" : "",
)}
>
<div className={clsx("flex items-center font-medium", alreadyAdded ? "opacity-50" : "")}>
<svg
className="mr-4 h-8 w-8 -translate-x-[2px] transform fill-current text-black dark:text-white"
xmlns="http://www.w3.org/2000/svg"
@@ -61,12 +46,7 @@ export const TOTP = (alreadyAdded: boolean, link: string) => {
export const U2F = (alreadyAdded: boolean, link: string) => {
return (
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div
className={clsx(
"flex items-center font-medium",
alreadyAdded ? "" : "",
)}
>
<div className={clsx("flex items-center font-medium", alreadyAdded ? "" : "")}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -95,12 +75,7 @@ export const U2F = (alreadyAdded: boolean, link: string) => {
export const EMAIL = (alreadyAdded: boolean, link: string) => {
return (
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div
className={clsx(
"flex items-center font-medium",
alreadyAdded ? "" : "",
)}
>
<div className={clsx("flex items-center font-medium", alreadyAdded ? "" : "")}>
<svg
className="mr-4 h-8 w-8"
xmlns="http://www.w3.org/2000/svg"
@@ -130,12 +105,7 @@ export const EMAIL = (alreadyAdded: boolean, link: string) => {
export const SMS = (alreadyAdded: boolean, link: string) => {
return (
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div
className={clsx(
"flex items-center font-medium",
alreadyAdded ? "" : "",
)}
>
<div className={clsx("flex items-center font-medium", alreadyAdded ? "" : "")}>
<svg
className="mr-4 h-8 w-8"
xmlns="http://www.w3.org/2000/svg"
@@ -164,12 +134,7 @@ export const SMS = (alreadyAdded: boolean, link: string) => {
export const PASSKEYS = (alreadyAdded: boolean, link: string) => {
return (
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div
className={clsx(
"flex items-center font-medium",
alreadyAdded ? "" : "",
)}
>
<div className={clsx("flex items-center font-medium", alreadyAdded ? "" : "")}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -198,17 +163,8 @@ export const PASSKEYS = (alreadyAdded: boolean, link: string) => {
export const PASSWORD = (alreadyAdded: boolean, link: string) => {
return (
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div
className={clsx(
"flex items-center font-medium",
alreadyAdded ? "" : "",
)}
>
<svg
className="mr-4 h-7 w-8 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<div className={clsx("flex items-center font-medium", alreadyAdded ? "" : "")}>
<svg className="mr-4 h-7 w-8 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>
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { render } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Avatar, getInitials } from "./avatar";
// Mock next-themes
+1 -1
View File
@@ -1,8 +1,8 @@
"use client";
import { ColorShade, getColorHash } from "@/helpers/colors";
import { useTheme } from "next-themes";
import { getComponentRoundness } from "@/lib/theme";
import { useTheme } from "next-themes";
interface AvatarProps {
name: string | null | undefined;
+5 -12
View File
@@ -56,21 +56,14 @@ export const Boundary = ({
})}
>
<div
className={clsx(
"absolute -top-2 flex space-x-1 text-[9px] uppercase leading-4 tracking-widest",
{
"left-3 lg:left-5": size === "small",
"left-4 lg:left-9": size === "default",
},
)}
className={clsx("absolute -top-2 flex space-x-1 text-[9px] uppercase leading-4 tracking-widest", {
"left-3 lg:left-5": size === "small",
"left-4 lg:left-9": size === "default",
})}
>
{labels.map((label) => {
return (
<Label
key={label}
color={color}
animateRerendering={animateRerendering}
>
<Label key={label} color={color} animateRerendering={animateRerendering}>
{label}
</Label>
);
+2 -2
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { render } from "@testing-library/react";
import { Button, ButtonSizes, ButtonVariants, ButtonColors, getButtonClasses } from "./button";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { Button, ButtonColors, ButtonSizes, ButtonVariants, getButtonClasses } from "./button";
describe("Button Component", () => {
const originalEnv = process.env;
+2 -2
View File
@@ -1,7 +1,7 @@
import { APPEARANCE_STYLES, getComponentRoundness, getThemeConfig } from "@/lib/theme";
import { ThemeableProps } from "@/lib/themeUtils";
import { clsx } from "clsx";
import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react";
import { ThemeableProps } from "@/lib/themeUtils";
import { getThemeConfig, getComponentRoundness, APPEARANCE_STYLES } from "@/lib/theme";
export enum ButtonSizes {
Small = "Small",
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { render } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { Card } from "./card";
describe("Card Component", () => {
+2 -2
View File
@@ -1,6 +1,6 @@
import { APPEARANCE_STYLES, SPACING_STYLES, getComponentRoundness, getThemeConfig } from "@/lib/theme";
import { clsx } from "clsx";
import { HTMLAttributes, forwardRef, ReactNode } from "react";
import { getThemeConfig, APPEARANCE_STYLES, SPACING_STYLES, getComponentRoundness } from "@/lib/theme";
import { HTMLAttributes, ReactNode, forwardRef } from "react";
export interface CardProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
@@ -1,8 +1,8 @@
import { cleanup, render } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { ChangePasswordForm } from "./change-password-form";
import { create } from "@zitadel/client";
import { PasswordComplexitySettingsSchema } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { afterEach, describe, expect, test, vi } from "vitest";
import { ChangePasswordForm } from "./change-password-form";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
@@ -1,23 +1,23 @@
"use client";
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from "@/helpers/validators";
import { handleServerActionResponse } from "@/lib/client-utils";
import { checkSessionAndSetPassword, sendPassword } from "@/lib/server/password";
import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { FieldValues, useForm } from "react-hook-form";
import { Alert } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { PasswordComplexity } from "./password-complexity";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { handleServerActionResponse } from "@/lib/client-utils";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
@@ -124,71 +124,71 @@ export function ChangePasswordForm({ passwordComplexitySettings, sessionId, logi
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<div className="mb-4 grid grid-cols-1 gap-4 pt-4">
<div className="">
<TextInput
type="password"
autoComplete="current-password"
autoFocus
required
{...register("currentPassword", {
required: t("change.required.currentPassword"),
})}
label={t("change.labels.currentPassword")}
error={errors.currentPassword?.message as string}
data-testid="password-change-current-text-input"
/>
<div className="mb-4 grid grid-cols-1 gap-4 pt-4">
<div className="">
<TextInput
type="password"
autoComplete="current-password"
autoFocus
required
{...register("currentPassword", {
required: t("change.required.currentPassword"),
})}
label={t("change.labels.currentPassword")}
error={errors.currentPassword?.message as string}
data-testid="password-change-current-text-input"
/>
</div>
<div className="">
<TextInput
type="password"
autoComplete="new-password"
required
{...register("password", {
required: t("change.required.newPassword"),
})}
label={t("change.labels.newPassword")}
error={errors.password?.message as string}
data-testid="password-change-text-input"
/>
</div>
<div className="">
<TextInput
type="password"
required
autoComplete="new-password"
{...register("confirmPassword", {
required: t("change.required.confirmPassword"),
})}
label={t("change.labels.confirmPassword")}
error={errors.confirmPassword?.message as string}
data-testid="password-change-confirm-text-input"
/>
</div>
</div>
<div className="">
<TextInput
type="password"
autoComplete="new-password"
required
{...register("password", {
required: t("change.required.newPassword"),
})}
label={t("change.labels.newPassword")}
error={errors.password?.message as string}
data-testid="password-change-text-input"
{passwordComplexitySettings && (
<PasswordComplexity
passwordComplexitySettings={passwordComplexitySettings}
password={watchPassword}
equals={!!watchPassword && watchPassword === watchConfirmPassword}
/>
)}
{error && <Alert>{error}</Alert>}
<div className="mt-8 flex w-full flex-row items-center justify-between">
<BackButton data-testid="back-button" />
<Button
type="submit"
variant={ButtonVariants.Primary}
disabled={loading || !policyIsValid || !formState.isValid || watchPassword !== watchConfirmPassword}
onClick={handleSubmit(submitChange)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="change.submit" namespace="password" />
</Button>
</div>
<div className="">
<TextInput
type="password"
required
autoComplete="new-password"
{...register("confirmPassword", {
required: t("change.required.confirmPassword"),
})}
label={t("change.labels.confirmPassword")}
error={errors.confirmPassword?.message as string}
data-testid="password-change-confirm-text-input"
/>
</div>
</div>
{passwordComplexitySettings && (
<PasswordComplexity
passwordComplexitySettings={passwordComplexitySettings}
password={watchPassword}
equals={!!watchPassword && watchPassword === watchConfirmPassword}
/>
)}
{error && <Alert>{error}</Alert>}
<div className="mt-8 flex w-full flex-row items-center justify-between">
<BackButton data-testid="back-button" />
<Button
type="submit"
variant={ButtonVariants.Primary}
disabled={loading || !policyIsValid || !formState.isValid || watchPassword !== watchConfirmPassword}
onClick={handleSubmit(submitChange)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="change.submit" namespace="password" />
</Button>
</div>
</form>
</>
);
+33 -51
View File
@@ -1,62 +1,44 @@
import classNames from "clsx";
import {
DetailedHTMLProps,
forwardRef,
InputHTMLAttributes,
useEffect,
useState,
} from "react";
import { DetailedHTMLProps, forwardRef, InputHTMLAttributes, useEffect, useState } from "react";
export type CheckboxProps = DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> & {
export type CheckboxProps = DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {
checked: boolean;
disabled?: boolean;
onChangeVal?: (checked: boolean) => void;
};
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
function Checkbox(
{
className = "",
checked = false,
disabled = false,
onChangeVal,
children,
...props
},
ref,
) {
const [enabled, setEnabled] = useState<boolean>(checked);
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(
{ className = "", checked = false, disabled = false, onChangeVal, children, ...props },
ref,
) {
const [enabled, setEnabled] = useState<boolean>(checked);
useEffect(() => {
setEnabled(checked);
}, [checked]);
useEffect(() => {
setEnabled(checked);
}, [checked]);
return (
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<div className="box-sizing block">
<input
ref={ref}
checked={enabled}
onChange={(event) => {
setEnabled(event.target?.checked);
onChangeVal && onChangeVal(event.target?.checked);
}}
disabled={disabled}
type="checkbox"
className={classNames(
"form-checkbox rounded border-gray-300 text-primary-light-500 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 focus:ring-offset-0 dark:text-primary-dark-500",
className,
)}
{...props}
/>
</div>
return (
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<div className="box-sizing block">
<input
ref={ref}
checked={enabled}
onChange={(event) => {
setEnabled(event.target?.checked);
onChangeVal && onChangeVal(event.target?.checked);
}}
disabled={disabled}
type="checkbox"
className={classNames(
"form-checkbox rounded border-gray-300 text-primary-light-500 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 focus:ring-offset-0 dark:text-primary-dark-500",
className,
)}
{...props}
/>
</div>
{children}
</div>
);
},
);
{children}
</div>
);
});
@@ -1,7 +1,4 @@
import {
LoginSettings,
PasskeysType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { LoginSettings, PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { PASSKEYS, PASSWORD } from "./auth-methods";
import { Translated } from "./translated";
@@ -12,25 +9,20 @@ type Props = {
loginSettings: LoginSettings | undefined;
};
export function ChooseAuthenticatorToLogin({
authMethods,
params,
loginSettings,
}: Props) {
export function ChooseAuthenticatorToLogin({ authMethods, params, loginSettings }: Props) {
return (
<>
{authMethods.includes(AuthenticationMethodType.PASSWORD) &&
loginSettings?.allowLocalAuthentication && (
<div className="ztdl-p">
<Translated i18nKey="chooseAlternativeMethod" namespace="idp" />
</div>
)}
{authMethods.includes(AuthenticationMethodType.PASSWORD) && loginSettings?.allowLocalAuthentication && (
<div className="ztdl-p">
<Translated i18nKey="chooseAlternativeMethod" namespace="idp" />
</div>
)}
<div className="grid w-full grid-cols-1 gap-5 pt-4">
{authMethods.includes(AuthenticationMethodType.PASSWORD) &&
loginSettings?.allowLocalAuthentication &&
PASSWORD(false, "/password?" + params)}
{authMethods.includes(AuthenticationMethodType.PASSKEY) &&
loginSettings?.allowLocalAuthentication &&
loginSettings?.allowLocalAuthentication &&
loginSettings?.passkeysType == PasskeysType.ALLOWED &&
PASSKEYS(false, "/passkey?" + params)}
</div>
@@ -1,15 +1,15 @@
"use client";
import { handleServerActionResponse } from "@/lib/client-utils";
import { skipMFAAndContinueWithNextUrl } from "@/lib/server/session";
import { LoginSettings, SecondFactorType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useRouter } from "next/navigation";
import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
import { Translated } from "./translated";
import { useState } from "react";
import { handleServerActionResponse } from "@/lib/client-utils";
import { AutoSubmitForm } from "./auto-submit-form";
import { Alert } from "./alert";
import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
import { AutoSubmitForm } from "./auto-submit-form";
import { Translated } from "./translated";
type Props = {
userId: string;
@@ -11,13 +11,7 @@ type Props = {
userMethods: AuthenticationMethodType[];
};
export function ChooseSecondFactor({
loginName,
sessionId,
requestId,
organization,
userMethods,
}: Props) {
export function ChooseSecondFactor({ loginName, sessionId, requestId, organization, userMethods }: Props) {
const params = new URLSearchParams({});
if (loginName) {
@@ -38,14 +32,10 @@ export function ChooseSecondFactor({
{userMethods.map((method, i) => {
return (
<div key={"method-" + i}>
{method === AuthenticationMethodType.TOTP &&
TOTP(false, "/otp/time-based?" + params)}
{method === AuthenticationMethodType.U2F &&
U2F(false, "/u2f?" + params)}
{method === AuthenticationMethodType.OTP_EMAIL &&
EMAIL(false, "/otp/email?" + params)}
{method === AuthenticationMethodType.OTP_SMS &&
SMS(false, "/otp/sms?" + params)}
{method === AuthenticationMethodType.TOTP && TOTP(false, "/otp/time-based?" + params)}
{method === AuthenticationMethodType.U2F && U2F(false, "/u2f?" + params)}
{method === AuthenticationMethodType.OTP_EMAIL && EMAIL(false, "/otp/email?" + params)}
{method === AuthenticationMethodType.OTP_SMS && SMS(false, "/otp/sms?" + params)}
</div>
);
})}
+4 -16
View File
@@ -28,9 +28,7 @@ export function ConsentScreen({
async function denyDeviceAuth() {
setLoading(true);
const response = await completeDeviceAuthorization(
deviceAuthorizationRequestId,
)
const response = await completeDeviceAuthorization(deviceAuthorizationRequestId)
.catch(() => {
setError("Could not register user");
return;
@@ -59,8 +57,7 @@ export function ConsentScreen({
const description = t(translationKey);
// Check if the key itself is returned and provide a fallback
const resolvedDescription =
description === translationKey ? "" : description;
const resolvedDescription = description === translationKey ? "" : description;
return (
<li
@@ -74,11 +71,7 @@ export function ConsentScreen({
</ul>
<p className="ztdl-p text-left text-xs">
<Translated
i18nKey="request.disclaimer"
namespace="device"
data={{ appName: appName }}
/>
<Translated i18nKey="request.disclaimer" namespace="device" data={{ appName: appName }} />
</p>
{error && (
@@ -101,12 +94,7 @@ export function ConsentScreen({
<span className="flex-grow"></span>
<Link href={nextUrl}>
<Button
data-testid="submit-button"
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
>
<Button data-testid="submit-button" type="submit" className="self-end" variant={ButtonVariants.Primary}>
<Translated i18nKey="device.request.submit" namespace="device" />
</Button>
</Link>
@@ -1,9 +1,6 @@
"use client";
import {
ClipboardDocumentCheckIcon,
ClipboardIcon,
} from "@heroicons/react/20/solid";
import { ClipboardDocumentCheckIcon, ClipboardIcon } from "@heroicons/react/20/solid";
import copy from "copy-to-clipboard";
import { useEffect, useState } from "react";
@@ -30,11 +27,7 @@ export function CopyToClipboard({ value }: Props) {
className="text-primary-light-500 dark:text-primary-dark-500"
onClick={() => setCopied(true)}
>
{!copied ? (
<ClipboardIcon className="h-5 w-5" />
) : (
<ClipboardDocumentCheckIcon className="h-5 w-5" />
)}
{!copied ? <ClipboardIcon className="h-5 w-5" /> : <ClipboardDocumentCheckIcon className="h-5 w-5" />}
</button>
</div>
);
+3 -17
View File
@@ -3,23 +3,9 @@ export function DefaultTags() {
return (
<>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="/favicon/apple-touch-icon.png"
rel="apple-touch-icon"
sizes="180x180"
/>
<link
href="/favicon/favicon-32x32.png"
rel="icon"
sizes="32x32"
type="image/png"
/>
<link
href="/favicon/favicon-16x16.png"
rel="icon"
sizes="16x16"
type="image/png"
/>
<link href="/favicon/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" />
<link href="/favicon/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png" />
<link href="/favicon/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png" />
<link href="/favicon/site.webmanifest" rel="manifest" />
{/* <link
color="#000000"
@@ -2,9 +2,9 @@
import { Alert } from "@/components/alert";
import { getDeviceAuthorizationRequest } from "@/lib/server/oidc";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
+12 -12
View File
@@ -1,11 +1,11 @@
"use client";
import { Logo } from "@/components/logo";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import React, { ReactNode, Children } from "react";
import { ThemeWrapper } from "./theme-wrapper";
import { Card } from "./card";
import { useResponsiveLayout } from "@/lib/theme-hooks";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import React, { Children, ReactNode } from "react";
import { Card } from "./card";
import { ThemeWrapper } from "./theme-wrapper";
/**
* DynamicTheme component handles layout switching between traditional top-to-bottom
@@ -49,12 +49,12 @@ export function DynamicTheme({
const hasLeftRightStructure = childArray.length === 2;
return (
<div className="relative mx-auto w-full max-w-[1100px] py-4 px-8">
<div className="relative mx-auto w-full max-w-[1100px] px-8 py-4">
<Card>
<div className="flex min-h-[400px]">
{/* Left side: First child + branding */}
<div className="flex w-1/2 flex-col justify-center p-4 lg:p-8 bg-gradient-to-br from-primary-50 to-primary-100 dark:from-primary-900/20 dark:to-primary-800/20">
<div className="max-w-[440px] mx-auto space-y-8">
<div className="from-primary-50 to-primary-100 dark:from-primary-900/20 dark:to-primary-800/20 flex w-1/2 flex-col justify-center bg-gradient-to-br p-4 lg:p-8">
<div className="mx-auto max-w-[440px] space-y-8">
{/* Logo and branding */}
{branding && (
<Logo
@@ -67,9 +67,9 @@ export function DynamicTheme({
{/* First child content (title, description) - only if we have left/right structure */}
{hasLeftRightStructure && (
<div className="space-y-4 text-left flex flex-col items-start">
<div className="flex flex-col items-start space-y-4 text-left">
{/* Apply larger styling to the content */}
<div className="space-y-6 [&_h1]:text-4xl [&_h1]:lg:text-4xl [&_h1]:text-left [&_h1]:text-gray-900 [&_h1]:dark:text-white [&_h1]:leading-tight [&_p]:text-left [&_p]:leading-relaxed [&_p]:text-gray-700 [&_p]:dark:text-gray-300">
<div className="space-y-6 [&_h1]:text-left [&_h1]:text-4xl [&_h1]:leading-tight [&_h1]:text-gray-900 [&_h1]:dark:text-white [&_h1]:lg:text-4xl [&_p]:text-left [&_p]:leading-relaxed [&_p]:text-gray-700 [&_p]:dark:text-gray-300">
{leftContent}
</div>
</div>
@@ -96,10 +96,10 @@ export function DynamicTheme({
const hasMultipleChildren = childArray.length > 1;
return (
<div className="relative mx-auto w-full max-w-[440px] py-4 px-4">
<div className="relative mx-auto w-full max-w-[440px] px-4 py-4">
<Card>
<div className="mx-auto flex flex-col items-center space-y-8">
<div className="relative flex flex-row items-center justify-center -mb-4">
<div className="relative -mb-4 flex flex-row items-center justify-center">
{branding && (
<Logo
lightSrc={branding.lightTheme?.logoUrl}
@@ -113,7 +113,7 @@ export function DynamicTheme({
{hasMultipleChildren ? (
<>
{/* Title and description - center aligned */}
<div className="w-full text-center flex flex-col items-center mb-4">{titleContent}</div>
<div className="mb-4 flex w-full flex-col items-center text-center">{titleContent}</div>
{/* Form content - left aligned */}
<div className="w-full">{formContent}</div>
+1 -7
View File
@@ -1,13 +1,7 @@
import { ArrowRightIcon } from "@heroicons/react/24/solid";
import { ReactNode } from "react";
export const ExternalLink = ({
children,
href,
}: {
children: ReactNode;
href: string;
}) => {
export const ExternalLink = ({ children, href }: { children: ReactNode; href: string }) => {
return (
<a
href={href}
@@ -1,11 +1,11 @@
"use client";
import { processIDPCallback } from "@/lib/server/idp-intent";
import { AutoSubmitForm } from "./auto-submit-form";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { Alert } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { Spinner } from "./spinner";
type Props = {
@@ -1,10 +1,10 @@
"use client";
import { APPEARANCE_STYLES, getComponentRoundness, getThemeConfig } from "@/lib/theme";
import { clsx } from "clsx";
import { Loader2Icon } from "lucide-react";
import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react";
import { useFormStatus } from "react-dom";
import { getComponentRoundness, getThemeConfig, APPEARANCE_STYLES } from "@/lib/theme";
export type SignInWithIdentityProviderProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
@@ -4,33 +4,26 @@ import { forwardRef } from "react";
import { Translated } from "../translated";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
export const SignInWithApple = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(function SignInWithApple(props, ref) {
const { children, name, ...restProps } = props;
export const SignInWithApple = forwardRef<HTMLButtonElement, SignInWithIdentityProviderProps>(
function SignInWithApple(props, ref) {
const { children, name, ...restProps } = props;
return (
<BaseButton {...restProps} ref={ref}>
<div className="flex h-12 w-12 items-center justify-center">
<div className="h-6 w-6">
<svg viewBox="0 0 170 170" fill="currentColor">
<title>Apple Logo</title>
<path d="M150.37 130.25c-2.45 5.66-5.35 10.87-8.71 15.66-4.58 6.53-8.33 11.05-11.22 13.56-4.48 4.12-9.28 6.23-14.42 6.35-3.69 0-8.14-1.05-13.32-3.18-5.197-2.12-9.973-3.17-14.34-3.17-4.58 0-9.492 1.05-14.746 3.17-5.262 2.13-9.501 3.24-12.742 3.35-4.929.21-9.842-1.96-14.746-6.52-3.13-2.73-7.045-7.41-11.735-14.04-5.032-7.08-9.169-15.29-12.41-24.65-3.471-10.11-5.211-19.9-5.211-29.378 0-10.857 2.346-20.221 7.045-28.068 3.693-6.303 8.606-11.275 14.755-14.925s12.793-5.51 19.948-5.629c3.915 0 9.049 1.211 15.429 3.591 6.362 2.388 10.447 3.599 12.238 3.599 1.339 0 5.877-1.416 13.57-4.239 7.275-2.618 13.415-3.702 18.445-3.275 13.63 1.1 23.87 6.473 30.68 16.153-12.19 7.386-18.22 17.731-18.1 31.002.11 10.337 3.86 18.939 11.23 25.769 3.34 3.17 7.07 5.62 11.22 7.36-.9 2.61-1.85 5.11-2.86 7.51zM119.11 7.24c0 8.102-2.96 15.667-8.86 22.669-7.12 8.324-15.732 13.134-25.071 12.375a25.222 25.222 0 0 1-.188-3.07c0-7.778 3.386-16.102 9.399-22.908 3.002-3.446 6.82-6.311 11.45-8.597 4.62-2.252 8.99-3.497 13.1-3.71.12 1.083.17 2.166.17 3.24z" />
</svg>
return (
<BaseButton {...restProps} ref={ref}>
<div className="flex h-12 w-12 items-center justify-center">
<div className="h-6 w-6">
<svg viewBox="0 0 170 170" fill="currentColor">
<title>Apple Logo</title>
<path d="M150.37 130.25c-2.45 5.66-5.35 10.87-8.71 15.66-4.58 6.53-8.33 11.05-11.22 13.56-4.48 4.12-9.28 6.23-14.42 6.35-3.69 0-8.14-1.05-13.32-3.18-5.197-2.12-9.973-3.17-14.34-3.17-4.58 0-9.492 1.05-14.746 3.17-5.262 2.13-9.501 3.24-12.742 3.35-4.929.21-9.842-1.96-14.746-6.52-3.13-2.73-7.045-7.41-11.735-14.04-5.032-7.08-9.169-15.29-12.41-24.65-3.471-10.11-5.211-19.9-5.211-29.378 0-10.857 2.346-20.221 7.045-28.068 3.693-6.303 8.606-11.275 14.755-14.925s12.793-5.51 19.948-5.629c3.915 0 9.049 1.211 15.429 3.591 6.362 2.388 10.447 3.599 12.238 3.599 1.339 0 5.877-1.416 13.57-4.239 7.275-2.618 13.415-3.702 18.445-3.275 13.63 1.1 23.87 6.473 30.68 16.153-12.19 7.386-18.22 17.731-18.1 31.002.11 10.337 3.86 18.939 11.23 25.769 3.34 3.17 7.07 5.62 11.22 7.36-.9 2.61-1.85 5.11-2.86 7.51zM119.11 7.24c0 8.102-2.96 15.667-8.86 22.669-7.12 8.324-15.732 13.134-25.071 12.375a25.222 25.222 0 0 1-.188-3.07c0-7.778 3.386-16.102 9.399-22.908 3.002-3.446 6.82-6.311 11.45-8.597 4.62-2.252 8.99-3.497 13.1-3.71.12 1.083.17 2.166.17 3.24z" />
</svg>
</div>
</div>
</div>
{children ? (
children
) : (
<span className="ml-4">
{name ? (
name
) : (
<Translated i18nKey="signInWithApple" namespace="idp" />
)}
</span>
)}
</BaseButton>
);
});
{children ? (
children
) : (
<span className="ml-4">{name ? name : <Translated i18nKey="signInWithApple" namespace="idp" />}</span>
)}
</BaseButton>
);
},
);
@@ -4,39 +4,26 @@ import { forwardRef } from "react";
import { Translated } from "../translated";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
export const SignInWithAzureAd = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(function SignInWithAzureAd(props, ref) {
const { children, name, ...restProps } = props;
export const SignInWithAzureAd = forwardRef<HTMLButtonElement, SignInWithIdentityProviderProps>(
function SignInWithAzureAd(props, ref) {
const { children, name, ...restProps } = props;
return (
<BaseButton {...restProps} ref={ref}>
<div className="flex h-12 w-12 items-center justify-center p-[10px]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="21"
height="21"
viewBox="0 0 21 21"
className="h-full w-full"
>
<path fill="#f25022" d="M1 1H10V10H1z"></path>
<path fill="#00a4ef" d="M1 11H10V20H1z"></path>
<path fill="#7fba00" d="M11 1H20V10H11z"></path>
<path fill="#ffb900" d="M11 11H20V20H11z"></path>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">
{name ? (
name
) : (
<Translated i18nKey="signInWithAzureAD" namespace="idp" />
)}
</span>
)}
</BaseButton>
);
});
return (
<BaseButton {...restProps} ref={ref}>
<div className="flex h-12 w-12 items-center justify-center p-[10px]">
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21" className="h-full w-full">
<path fill="#f25022" d="M1 1H10V10H1z"></path>
<path fill="#00a4ef" d="M1 11H10V20H1z"></path>
<path fill="#7fba00" d="M11 1H20V10H11z"></path>
<path fill="#ffb900" d="M11 11H20V20H11z"></path>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">{name ? name : <Translated i18nKey="signInWithAzureAD" namespace="idp" />}</span>
)}
</BaseButton>
);
},
);
@@ -3,19 +3,13 @@
import { forwardRef } from "react";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
export const SignInWithGeneric = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(function SignInWithGeneric(props, ref) {
const {
children,
name = "",
className = "h-[50px] pl-20",
...restProps
} = props;
return (
<BaseButton {...restProps} ref={ref} className={className}>
{children ? children : <span>{name}</span>}
</BaseButton>
);
});
export const SignInWithGeneric = forwardRef<HTMLButtonElement, SignInWithIdentityProviderProps>(
function SignInWithGeneric(props, ref) {
const { children, name = "", className = "h-[50px] pl-20", ...restProps } = props;
return (
<BaseButton {...restProps} ref={ref} className={className}>
{children ? children : <span>{name}</span>}
</BaseButton>
);
},
);
@@ -7,12 +7,7 @@ import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
function GitHubLogo() {
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
className="hidden h-8 w-8 dark:block"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1024 1024" className="hidden h-8 w-8 dark:block">
<path
fill="#fafafa"
fillRule="evenodd"
@@ -20,12 +15,7 @@ function GitHubLogo() {
clipRule="evenodd"
></path>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
className="block h-8 w-8 dark:hidden"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1024 1024" className="block h-8 w-8 dark:hidden">
<path
fill="#1B1F23"
fillRule="evenodd"
@@ -37,28 +27,21 @@ function GitHubLogo() {
);
}
export const SignInWithGithub = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(function SignInWithGithub(props, ref) {
const { children, name, ...restProps } = props;
export const SignInWithGithub = forwardRef<HTMLButtonElement, SignInWithIdentityProviderProps>(
function SignInWithGithub(props, ref) {
const { children, name, ...restProps } = props;
return (
<BaseButton {...restProps} ref={ref}>
<div className="mx-2 my-2 flex items-center justify-center">
<GitHubLogo />
</div>
{children ? (
children
) : (
<span className="ml-4">
{name ? (
name
) : (
<Translated i18nKey="signInWithGithub" namespace="idp" />
)}
</span>
)}
</BaseButton>
);
});
return (
<BaseButton {...restProps} ref={ref}>
<div className="mx-2 my-2 flex items-center justify-center">
<GitHubLogo />
</div>
{children ? (
children
) : (
<span className="ml-4">{name ? name : <Translated i18nKey="signInWithGithub" namespace="idp" />}</span>
)}
</BaseButton>
);
},
);
@@ -4,50 +4,38 @@ import { forwardRef } from "react";
import { Translated } from "../translated";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
export const SignInWithGitlab = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(function SignInWithGitlab(props, ref) {
const { children, name, ...restProps } = props;
export const SignInWithGitlab = forwardRef<HTMLButtonElement, SignInWithIdentityProviderProps>(
function SignInWithGitlab(props, ref) {
const { children, name, ...restProps } = props;
return (
<BaseButton {...restProps} ref={ref}>
<div className="flex h-12 w-12 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width={25}
height={24}
fill="none"
>
<path
fill="#e24329"
d="m24.507 9.5-.034-.09L21.082.562a.896.896 0 0 0-1.694.091l-2.29 7.01H7.825L5.535.653a.898.898 0 0 0-1.694-.09L.451 9.411.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 2.56 1.935 1.554 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5z"
/>
<path
fill="#fc6d26"
d="m24.507 9.5-.034-.09a11.44 11.44 0 0 0-4.56 2.051l-7.447 5.632 4.742 3.584 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5z"
/>
<path
fill="#fca326"
d="m7.707 20.677 2.56 1.935 1.555 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935-4.743-3.584-4.755 3.584z"
/>
<path
fill="#fc6d26"
d="M5.01 11.461a11.43 11.43 0 0 0-4.56-2.05L.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 4.745-3.584-7.444-5.632z"
/>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">
{name ? (
name
) : (
<Translated i18nKey="signInWithGitlab" namespace="idp" />
)}
</span>
)}
</BaseButton>
);
});
return (
<BaseButton {...restProps} ref={ref}>
<div className="flex h-12 w-12 items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width={25} height={24} fill="none">
<path
fill="#e24329"
d="m24.507 9.5-.034-.09L21.082.562a.896.896 0 0 0-1.694.091l-2.29 7.01H7.825L5.535.653a.898.898 0 0 0-1.694-.09L.451 9.411.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 2.56 1.935 1.554 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5z"
/>
<path
fill="#fc6d26"
d="m24.507 9.5-.034-.09a11.44 11.44 0 0 0-4.56 2.051l-7.447 5.632 4.742 3.584 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5z"
/>
<path
fill="#fca326"
d="m7.707 20.677 2.56 1.935 1.555 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935-4.743-3.584-4.755 3.584z"
/>
<path
fill="#fc6d26"
d="M5.01 11.461a11.43 11.43 0 0 0-4.56-2.05L.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 4.745-3.584-7.444-5.632z"
/>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">{name ? name : <Translated i18nKey="signInWithGitlab" namespace="idp" />}</span>
)}
</BaseButton>
);
},
);
@@ -4,63 +4,51 @@ import { forwardRef } from "react";
import { Translated } from "../translated";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
export const SignInWithGoogle = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(function SignInWithGoogle(props, ref) {
const { children, name, ...restProps } = props;
export const SignInWithGoogle = forwardRef<HTMLButtonElement, SignInWithIdentityProviderProps>(
function SignInWithGoogle(props, ref) {
const { children, name, ...restProps } = props;
return (
<BaseButton {...restProps} ref={ref}>
<div className="flex h-12 w-12 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
id="Capa_1"
viewBox="0 0 150 150"
>
<style>
{
".st0{fill:#1a73e8}.st1{fill:#ea4335}.st2{fill:#4285f4}.st3{fill:#fbbc04}.st4{fill:#34a853}.st5{fill:#4caf50}.st6{fill:#1e88e5}.st7{fill:#e53935}.st8{fill:#c62828}.st9{fill:#fbc02d}.st10{fill:#1565c0}.st11{fill:#2e7d32}.st16{clip-path:url(#SVGID_2_)}.st17{fill:#188038}.st18,.st19{opacity:.2;fill:#fff;enable-background:new}.st19{opacity:.3;fill:#0d652d}.st20{clip-path:url(#SVGID_4_)}.st21{opacity:.3;fill:url(#_45_shadow_1_);enable-background:new}.st22{clip-path:url(#SVGID_6_)}.st23{fill:#fa7b17}.st24,.st25,.st26{opacity:.3;fill:#174ea6;enable-background:new}.st25,.st26{fill:#a50e0e}.st26{fill:#e37400}.st27{fill:url(#Finish_mask_1_)}.st28{fill:#fff}.st29{fill:#0c9d58}.st30,.st31{opacity:.2;fill:#004d40;enable-background:new}.st31{fill:#3e2723}.st32{fill:#ffc107}.st33{fill:#1a237e;enable-background:new}.st33,.st34{opacity:.2}.st35{fill:#1a237e}.st36{fill:url(#SVGID_7_)}.st37{fill:#fbbc05}.st38{clip-path:url(#SVGID_9_);fill:#e53935}.st39{clip-path:url(#SVGID_11_);fill:#fbc02d}.st40{clip-path:url(#SVGID_13_);fill:#e53935}.st41{clip-path:url(#SVGID_15_);fill:#fbc02d}"
}
</style>
<path
d="M120 76.1c0-3.1-.3-6.3-.8-9.3H75.9v17.7h24.8c-1 5.7-4.3 10.7-9.2 13.9l14.8 11.5C115 101.8 120 90 120 76.1z"
style={{
fill: "#4280ef",
}}
/>
<path
d="M75.9 120.9c12.4 0 22.8-4.1 30.4-11.1L91.5 98.4c-4.1 2.8-9.4 4.4-15.6 4.4-12 0-22.1-8.1-25.8-18.9L34.9 95.6c7.8 15.5 23.6 25.3 41 25.3z"
style={{
fill: "#34a353",
}}
/>
<path
d="M50.1 83.8c-1.9-5.7-1.9-11.9 0-17.6L34.9 54.4c-6.5 13-6.5 28.3 0 41.2l15.2-11.8z"
style={{
fill: "#f6b704",
}}
/>
<path
d="M75.9 47.3c6.5-.1 12.9 2.4 17.6 6.9L106.6 41c-8.3-7.8-19.3-12-30.7-11.9-17.4 0-33.2 9.8-41 25.3l15.2 11.8c3.7-10.9 13.8-18.9 25.8-18.9z"
style={{
fill: "#e54335",
}}
/>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">
{name ? (
name
) : (
<Translated i18nKey="signInWithGoogle" namespace="idp" />
)}
</span>
)}
</BaseButton>
);
});
return (
<BaseButton {...restProps} ref={ref}>
<div className="flex h-12 w-12 items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" id="Capa_1" viewBox="0 0 150 150">
<style>
{
".st0{fill:#1a73e8}.st1{fill:#ea4335}.st2{fill:#4285f4}.st3{fill:#fbbc04}.st4{fill:#34a853}.st5{fill:#4caf50}.st6{fill:#1e88e5}.st7{fill:#e53935}.st8{fill:#c62828}.st9{fill:#fbc02d}.st10{fill:#1565c0}.st11{fill:#2e7d32}.st16{clip-path:url(#SVGID_2_)}.st17{fill:#188038}.st18,.st19{opacity:.2;fill:#fff;enable-background:new}.st19{opacity:.3;fill:#0d652d}.st20{clip-path:url(#SVGID_4_)}.st21{opacity:.3;fill:url(#_45_shadow_1_);enable-background:new}.st22{clip-path:url(#SVGID_6_)}.st23{fill:#fa7b17}.st24,.st25,.st26{opacity:.3;fill:#174ea6;enable-background:new}.st25,.st26{fill:#a50e0e}.st26{fill:#e37400}.st27{fill:url(#Finish_mask_1_)}.st28{fill:#fff}.st29{fill:#0c9d58}.st30,.st31{opacity:.2;fill:#004d40;enable-background:new}.st31{fill:#3e2723}.st32{fill:#ffc107}.st33{fill:#1a237e;enable-background:new}.st33,.st34{opacity:.2}.st35{fill:#1a237e}.st36{fill:url(#SVGID_7_)}.st37{fill:#fbbc05}.st38{clip-path:url(#SVGID_9_);fill:#e53935}.st39{clip-path:url(#SVGID_11_);fill:#fbc02d}.st40{clip-path:url(#SVGID_13_);fill:#e53935}.st41{clip-path:url(#SVGID_15_);fill:#fbc02d}"
}
</style>
<path
d="M120 76.1c0-3.1-.3-6.3-.8-9.3H75.9v17.7h24.8c-1 5.7-4.3 10.7-9.2 13.9l14.8 11.5C115 101.8 120 90 120 76.1z"
style={{
fill: "#4280ef",
}}
/>
<path
d="M75.9 120.9c12.4 0 22.8-4.1 30.4-11.1L91.5 98.4c-4.1 2.8-9.4 4.4-15.6 4.4-12 0-22.1-8.1-25.8-18.9L34.9 95.6c7.8 15.5 23.6 25.3 41 25.3z"
style={{
fill: "#34a353",
}}
/>
<path
d="M50.1 83.8c-1.9-5.7-1.9-11.9 0-17.6L34.9 54.4c-6.5 13-6.5 28.3 0 41.2l15.2-11.8z"
style={{
fill: "#f6b704",
}}
/>
<path
d="M75.9 47.3c6.5-.1 12.9 2.4 17.6 6.9L106.6 41c-8.3-7.8-19.3-12-30.7-11.9-17.4 0-33.2 9.8-41 25.3l15.2 11.8c3.7-10.9 13.8-18.9 25.8-18.9z"
style={{
fill: "#e54335",
}}
/>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">{name ? name : <Translated i18nKey="signInWithGoogle" namespace="idp" />}</span>
)}
</BaseButton>
);
},
);
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { TextInput } from "./input";
describe("TextInput Component", () => {
+1 -1
View File
@@ -1,9 +1,9 @@
"use client";
import { getComponentRoundness } from "@/lib/theme";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import { clsx } from "clsx";
import { ChangeEvent, DetailedHTMLProps, forwardRef, InputHTMLAttributes, ReactNode } from "react";
import { getComponentRoundness } from "@/lib/theme";
export type TextInputProps = DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {
label: string;
@@ -5,9 +5,5 @@ import { ReactNode } from "react";
export async function LanguageProvider({ children }: { children: ReactNode }) {
const messages = await getMessages();
return (
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
);
return <NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>;
}
@@ -2,7 +2,7 @@
import { setLanguageCookie } from "@/lib/cookies";
import { Lang } from "@/lib/i18n";
import { getThemeConfig, getComponentRoundness, APPEARANCE_STYLES } from "@/lib/theme";
import { APPEARANCE_STYLES, getComponentRoundness, getThemeConfig } from "@/lib/theme";
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/react";
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
@@ -57,7 +57,7 @@ export function LanguageSwitcher({ languages }: { languages: Lang[] }) {
anchor="bottom"
transition
className={clsx(
`w-[var(--button-width)] border border-black/5 bg-background-light-500 p-1 [--anchor-gap:var(--spacing-1)] focus:outline-none dark:border-white/5 dark:bg-background-dark-500 rounded-md`,
`w-[var(--button-width)] rounded-md border border-black/5 bg-background-light-500 p-1 [--anchor-gap:var(--spacing-1)] focus:outline-none dark:border-white/5 dark:bg-background-dark-500`,
"transition duration-100 ease-in data-[leave]:data-[closed]:opacity-0",
)}
>
@@ -11,7 +11,5 @@ export function LayoutProviders({ children }: Props) {
const { resolvedTheme } = useTheme();
const isDark = resolvedTheme === "dark";
return (
<div className={`${isDark ? "ui-dark" : "ui-light"} `}>{children}</div>
);
return <div className={`${isDark ? "ui-dark" : "ui-light"} `}>{children}</div>;
}
@@ -18,9 +18,7 @@ describe("LDAPUsernamePasswordForm", () => {
afterEach(cleanup);
test("should autofocus the username input on mount", () => {
const { getByTestId } = render(
<LDAPUsernamePasswordForm idpId="idp-1" link={false} />,
);
const { getByTestId } = render(<LDAPUsernamePasswordForm idpId="idp-1" link={false} />);
expect(getByTestId("username-text-input")).toHaveFocus();
});
});
+1 -3
View File
@@ -23,9 +23,7 @@ describe("LoginOTP", () => {
afterEach(cleanup);
test("should autofocus the code input on mount", () => {
const { getByTestId } = render(
<LoginOTP host={null} method="time-based" />,
);
const { getByTestId } = render(<LoginOTP host={null} method="time-based" />);
expect(getByTestId("code-text-input")).toHaveFocus();
});
});
+3 -3
View File
@@ -1,23 +1,23 @@
"use client";
import { completeFlowOrGetUrl } from "@/lib/client";
import { handleServerActionResponse } from "@/lib/client-utils";
import { updateOrCreateSession } from "@/lib/server/session";
import { create } from "@zitadel/client";
import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { Alert, AlertType } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { BackButton } from "./back-button";
import { useTranslations } from "next-intl";
import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
import { completeFlowOrGetUrl } from "@/lib/client";
// either loginName or sessionId must be provided
type Props = {
@@ -1,7 +1,7 @@
import { describe, expect, test, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { LoginPasskey } from "./login-passkey";
import { NextIntlClientProvider } from "next-intl";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { LoginPasskey } from "./login-passkey";
// Mock next/navigation
const mockPush = vi.fn();
+3 -3
View File
@@ -1,21 +1,21 @@
"use client";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
import { handleServerActionResponse } from "@/lib/client-utils";
import { sendPasskey } from "@/lib/server/passkeys";
import { updateOrCreateSession } from "@/lib/server/session";
import { create, JsonObject } from "@zitadel/client";
import { RequestChallengesSchema, UserVerificationRequirement } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useTranslations } from "next-intl";
import { handleServerActionResponse } from "@/lib/client-utils";
import { Alert } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
// either loginName or sessionId must be provided
type Props = {
@@ -1,7 +1,7 @@
import { cleanup, render, screen } from "@testing-library/react";
import { NextIntlClientProvider } from "next-intl";
import { afterEach, describe, expect, test } from "vitest";
import { PasswordComplexity } from "./password-complexity";
import { NextIntlClientProvider } from "next-intl";
describe("<PasswordComplexity/>", () => {
const messages = {
@@ -1,12 +1,7 @@
import {
lowerCaseValidator,
numberValidator,
symbolValidator,
upperCaseValidator,
} from "@/helpers/validators";
import { Translated } from "@/components/translated";
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from "@/helpers/validators";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useTranslations } from "next-intl";
import { Translated } from "@/components/translated";
type Props = {
passwordComplexitySettings: PasswordComplexitySettings;
@@ -26,11 +21,7 @@ function CheckIcon({ title }: { title: string }) {
role="img"
>
<title>{title}</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
);
}
@@ -47,30 +38,17 @@ function CrossIcon({ title }: { title: string }) {
role="img"
>
<title>{title}</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
);
}
function renderIcon(matched: boolean, t: ReturnType<typeof useTranslations>) {
return matched ? (
<CheckIcon title={t("complexity.matches")} />
) : (
<CrossIcon title={t("complexity.doesNotMatch")} />
);
return matched ? <CheckIcon title={t("complexity.matches")} /> : <CrossIcon title={t("complexity.doesNotMatch")} />;
}
const desc =
"text-14px leading-4 text-input-light-label dark:text-input-dark-label";
const desc = "text-14px leading-4 text-input-light-label dark:text-input-dark-label";
export function PasswordComplexity({
passwordComplexitySettings,
password,
equals,
}: Props) {
export function PasswordComplexity({ passwordComplexitySettings, password, equals }: Props) {
const t = useTranslations("password");
const hasMinLength = password?.length >= passwordComplexitySettings.minLength;
const hasSymbol = symbolValidator(password);
@@ -19,12 +19,7 @@ describe("PasswordForm", () => {
afterEach(cleanup);
test("should autofocus the password input on mount", () => {
const { getByTestId } = render(
<PasswordForm
loginSettings={undefined}
loginName="test@example.com"
/>,
);
const { getByTestId } = render(<PasswordForm loginSettings={undefined} loginName="test@example.com" />);
expect(getByTestId("password-text-input")).toHaveFocus();
});
});
+3 -3
View File
@@ -1,21 +1,21 @@
"use client";
import { handleServerActionResponse } from "@/lib/client-utils";
import { resetPassword, sendPassword } from "@/lib/server/password";
import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import { Alert, AlertType } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { handleServerActionResponse } from "@/lib/client-utils";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs = {
password: string;
@@ -27,10 +27,7 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
const hasPrivacyLink = !!legal?.privacyPolicyLink;
// Check that all required checkboxes are accepted
return (
(!hasTosLink || newState.tosAccepted) &&
(!hasPrivacyLink || newState.privacyPolicyAccepted)
);
return (!hasTosLink || newState.tosAccepted) && (!hasPrivacyLink || newState.privacyPolicyAccepted);
};
return (
@@ -29,16 +29,12 @@ describe("RegisterFormIDPIncomplete", () => {
afterEach(cleanup);
test("should autofocus the username input when idpUserName is not provided", () => {
const { getByTestId } = render(
<RegisterFormIDPIncomplete {...defaultProps} />,
);
const { getByTestId } = render(<RegisterFormIDPIncomplete {...defaultProps} />);
expect(getByTestId("username-text-input")).toHaveFocus();
});
test("should autofocus the firstname input when idpUserName is provided", () => {
const { getByTestId } = render(
<RegisterFormIDPIncomplete {...defaultProps} idpUserName="existing-user" />,
);
const { getByTestId } = render(<RegisterFormIDPIncomplete {...defaultProps} idpUserName="existing-user" />);
expect(getByTestId("firstname-text-input")).toHaveFocus();
});
});
@@ -1,18 +1,18 @@
"use client";
import { registerUserAndLinkToIDP } from "@/lib/server/register";
import { handleServerActionResponse } from "@/lib/client-utils";
import { useState } from "react";
import { registerUserAndLinkToIDP } from "@/lib/server/register";
import { useTranslations } from "next-intl";
import { FieldValues, useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form";
import { Alert } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
@@ -1,8 +1,8 @@
import { cleanup, render } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { RegisterForm } from "./register-form";
import { create } from "@zitadel/client";
import { LegalAndSupportSettingsSchema } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
import { afterEach, describe, expect, test, vi } from "vitest";
import { RegisterForm } from "./register-form";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
@@ -26,13 +26,7 @@ describe("RegisterForm", () => {
afterEach(cleanup);
test("should autofocus the firstname input on mount", () => {
const { getByTestId } = render(
<RegisterForm
legal={defaultLegal}
organization="org-1"
idpCount={0}
/>,
);
const { getByTestId } = render(<RegisterForm legal={defaultLegal} organization="org-1" idpCount={0} />);
expect(getByTestId("firstname-text-input")).toHaveFocus();
});
});
+3 -3
View File
@@ -1,22 +1,22 @@
"use client";
import { registerUser } from "@/lib/server/register";
import { handleServerActionResponse } from "@/lib/client-utils";
import { registerUser } from "@/lib/server/register";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
import { LoginSettings, PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { FieldValues, useForm } from "react-hook-form";
import { Alert, AlertType } from "./alert";
import { AuthenticationMethod, AuthenticationMethodRadio, methods } from "./authentication-method-radio";
import { AutoSubmitForm } from "./auto-submit-form";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { PrivacyPolicyCheckboxes } from "./privacy-policy-checkboxes";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
@@ -3,7 +3,7 @@
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
import { registerPasskeyLink, verifyPasskeyRegistration } from "@/lib/server/passkeys";
import { useRouter } from "next/navigation";
import { useState, useEffect, useCallback } from "react";
import { useCallback, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { Alert } from "./alert";
import { BackButton } from "./back-button";
+3 -3
View File
@@ -1,19 +1,19 @@
"use client";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
import { completeFlowOrGetUrl } from "@/lib/client";
import { handleServerActionResponse } from "@/lib/client-utils";
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 { useRouter } from "next/navigation";
import { useState } from "react";
import { Alert } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
import { handleServerActionResponse } from "@/lib/client-utils";
import { completeFlowOrGetUrl } from "@/lib/client";
type Props = {
loginName?: string;
@@ -17,13 +17,7 @@ export function SelfServiceMenu() {
return (
<div className="flex w-full flex-col space-y-2">
{list.map((menuitem, index) => {
return (
<SelfServiceItem
link={menuitem.link}
key={"self-service-" + index}
name={menuitem.name}
/>
);
return <SelfServiceItem link={menuitem.link} key={"self-service-" + index} name={menuitem.name} />;
})}
</div>
);
@@ -11,13 +11,7 @@ import { Avatar } from "./avatar";
import { isSessionValid } from "./session-item";
import { Translated } from "./translated";
export function SessionClearItem({
session,
reload,
}: {
session: Session;
reload: () => void;
}) {
export function SessionClearItem({ session, reload }: { session: Session; reload: () => void }) {
const currentLocale = useLocale();
moment.locale(currentLocale === "zh" ? "zh-cn" : currentLocale);
@@ -65,9 +59,7 @@ export function SessionClearItem({
<div className="flex flex-col items-start overflow-hidden">
<span className="">{session.factors?.user?.displayName}</span>
<span className="text-ellipsis text-xs opacity-80">
{session.factors?.user?.loginName}
</span>
<span className="text-ellipsis text-xs opacity-80">{session.factors?.user?.loginName}</span>
{valid ? (
<span className="text-ellipsis text-xs opacity-80">
{verifiedAt && (
@@ -81,9 +73,7 @@ export function SessionClearItem({
) : (
verifiedAt && (
<span className="text-ellipsis text-xs opacity-80">
expired{" "}
{session.expirationDate &&
moment(timestampDate(session.expirationDate)).fromNow()}
expired {session.expirationDate && moment(timestampDate(session.expirationDate)).fromNow()}
</span>
)
)}
+2 -2
View File
@@ -1,5 +1,6 @@
"use client";
import { handleServerActionResponse } from "@/lib/client-utils";
import { sendLoginname } from "@/lib/server/loginname";
import { clearSession, continueWithSession, ContinueWithSessionCommand } from "@/lib/server/session";
import { XCircleIcon } from "@heroicons/react/24/outline";
@@ -10,10 +11,9 @@ import moment from "moment";
import { useLocale } from "next-intl";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import { AutoSubmitForm } from "./auto-submit-form";
import { Avatar } from "./avatar";
import { Translated } from "./translated";
import { handleServerActionResponse } from "@/lib/client-utils";
import { AutoSubmitForm } from "./auto-submit-form";
export function isSessionValid(session: Partial<Session>): {
valid: boolean;
+2 -6
View File
@@ -20,12 +20,8 @@ export function SessionsList({ sessions, requestId }: Props) {
.filter((session) => session?.factors?.user?.loginName)
// sort by change date descending
.sort((a, b) => {
const dateA = a.changeDate
? timestampDate(a.changeDate).getTime()
: 0;
const dateB = b.changeDate
? timestampDate(b.changeDate).getTime()
: 0;
const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0;
const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0;
return dateB - dateA;
})
// TODO: add sorting to move invalid sessions to the bottom
@@ -1,8 +1,8 @@
import { cleanup, render } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { SetPasswordForm } from "./set-password-form";
import { create } from "@zitadel/client";
import { PasswordComplexitySettingsSchema } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { afterEach, describe, expect, test, vi } from "vitest";
import { SetPasswordForm } from "./set-password-form";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
+83 -83
View File
@@ -1,23 +1,23 @@
"use client";
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from "@/helpers/validators";
import { handleServerActionResponse } from "@/lib/client-utils";
import { changePassword, resetPassword, sendPassword } from "@/lib/server/password";
import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { FieldValues, useForm } from "react-hook-form";
import { Alert, AlertType } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { PasswordComplexity } from "./password-complexity";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { handleServerActionResponse } from "@/lib/client-utils";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
@@ -175,95 +175,95 @@ export function SetPasswordForm({
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<div className="mb-4 grid grid-cols-1 gap-4 pt-4">
{codeRequired && (
<Alert type={AlertType.INFO}>
<div className="flex flex-row">
<span className="mr-auto flex-1 text-left">
<Translated i18nKey="set.noCodeReceived" namespace="password" />
</span>
<button
aria-label="Resend OTP Code"
disabled={loading}
type="button"
className="ml-4 cursor-pointer text-primary-light-500 hover:text-primary-light-400 disabled:cursor-default disabled:text-gray-400 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 dark:disabled:text-gray-700"
onClick={() => {
resendCode();
}}
data-testid="resend-button"
>
<Translated i18nKey="set.resend" namespace="password" />
</button>
<div className="mb-4 grid grid-cols-1 gap-4 pt-4">
{codeRequired && (
<Alert type={AlertType.INFO}>
<div className="flex flex-row">
<span className="mr-auto flex-1 text-left">
<Translated i18nKey="set.noCodeReceived" namespace="password" />
</span>
<button
aria-label="Resend OTP Code"
disabled={loading}
type="button"
className="ml-4 cursor-pointer text-primary-light-500 hover:text-primary-light-400 disabled:cursor-default disabled:text-gray-400 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 dark:disabled:text-gray-700"
onClick={() => {
resendCode();
}}
data-testid="resend-button"
>
<Translated i18nKey="set.resend" namespace="password" />
</button>
</div>
</Alert>
)}
{codeRequired && (
<div>
<TextInput
type="text"
autoFocus
required
{...register("code", {
required: t("set.required.code"),
})}
label={t("set.labels.code")}
autoComplete="one-time-code"
error={errors.code?.message as string}
data-testid="code-text-input"
/>
</div>
</Alert>
)}
{codeRequired && (
)}
<div>
<TextInput
type="text"
autoFocus
type="password"
autoComplete="new-password"
autoFocus={!codeRequired}
required
{...register("code", {
required: t("set.required.code"),
{...register("password", {
required: t("set.required.newPassword"),
})}
label={t("set.labels.code")}
autoComplete="one-time-code"
error={errors.code?.message as string}
data-testid="code-text-input"
label={t("set.labels.newPassword")}
error={errors.password?.message as string}
data-testid="password-set-text-input"
/>
</div>
<div>
<TextInput
type="password"
required
autoComplete="new-password"
{...register("confirmPassword", {
required: t("set.required.confirmPassword"),
})}
label={t("set.labels.confirmPassword")}
error={errors.confirmPassword?.message as string}
data-testid="password-set-confirm-text-input"
/>
</div>
</div>
{passwordComplexitySettings && (
<PasswordComplexity
passwordComplexitySettings={passwordComplexitySettings}
password={watchPassword}
equals={!!watchPassword && watchPassword === watchConfirmPassword}
/>
)}
<div>
<TextInput
type="password"
autoComplete="new-password"
autoFocus={!codeRequired}
required
{...register("password", {
required: t("set.required.newPassword"),
})}
label={t("set.labels.newPassword")}
error={errors.password?.message as string}
data-testid="password-set-text-input"
/>
{error && <Alert>{error}</Alert>}
<div className="mt-8 flex w-full flex-row items-center justify-between">
<BackButton data-testid="back-button" />
<Button
type="submit"
variant={ButtonVariants.Primary}
disabled={loading || !policyIsValid || !formState.isValid || watchPassword !== watchConfirmPassword}
onClick={handleSubmit(submitPassword)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="set.submit" namespace="password" />
</Button>
</div>
<div>
<TextInput
type="password"
required
autoComplete="new-password"
{...register("confirmPassword", {
required: t("set.required.confirmPassword"),
})}
label={t("set.labels.confirmPassword")}
error={errors.confirmPassword?.message as string}
data-testid="password-set-confirm-text-input"
/>
</div>
</div>
{passwordComplexitySettings && (
<PasswordComplexity
passwordComplexitySettings={passwordComplexitySettings}
password={watchPassword}
equals={!!watchPassword && watchPassword === watchConfirmPassword}
/>
)}
{error && <Alert>{error}</Alert>}
<div className="mt-8 flex w-full flex-row items-center justify-between">
<BackButton data-testid="back-button" />
<Button
type="submit"
variant={ButtonVariants.Primary}
disabled={loading || !policyIsValid || !formState.isValid || watchPassword !== watchConfirmPassword}
onClick={handleSubmit(submitPassword)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="set.submit" namespace="password" />
</Button>
</div>
</form>
</>
);
@@ -1,8 +1,8 @@
import { cleanup, render } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { SetRegisterPasswordForm } from "./set-register-password-form";
import { create } from "@zitadel/client";
import { PasswordComplexitySettingsSchema } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { afterEach, describe, expect, test, vi } from "vitest";
import { SetRegisterPasswordForm } from "./set-register-password-form";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
@@ -1,21 +1,21 @@
"use client";
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from "@/helpers/validators";
import { registerUser } from "@/lib/server/register";
import { handleServerActionResponse } from "@/lib/client-utils";
import { registerUser } from "@/lib/server/register";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { FieldValues, useForm } from "react-hook-form";
import { Alert } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { PasswordComplexity } from "./password-complexity";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
@@ -5,6 +5,7 @@ import { redirectToIdp } from "@/lib/server/idp";
import { IdentityProvider, IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { ReactNode, useActionState } from "react";
import { Alert } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { SignInWithIdentityProviderProps } from "./idps/base-button";
import { SignInWithApple } from "./idps/sign-in-with-apple";
import { SignInWithAzureAd } from "./idps/sign-in-with-azure-ad";
@@ -13,7 +14,6 @@ import { SignInWithGithub } from "./idps/sign-in-with-github";
import { SignInWithGitlab } from "./idps/sign-in-with-gitlab";
import { SignInWithGoogle } from "./idps/sign-in-with-google";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
export interface SignInWithIDPProps {
children?: ReactNode;
+2 -2
View File
@@ -1,6 +1,6 @@
import { clsx } from "clsx";
import { getComponentRoundness, getThemeConfig, SPACING_STYLES } from "@/lib/theme";
import { ThemeableProps } from "@/lib/themeUtils";
import { getThemeConfig, SPACING_STYLES, getComponentRoundness } from "@/lib/theme";
import { clsx } from "clsx";
interface SkeletonCardProps extends ThemeableProps {
isLoading?: boolean;
+2 -8
View File
@@ -28,12 +28,6 @@ const getBadgeClasses = (state: BadgeState, evenPadding: boolean) =>
"p-[2px]": evenPadding,
});
export function StateBadge({
state = BadgeState.Success,
evenPadding = false,
children,
}: StateBadgeProps) {
return (
<span className={`${getBadgeClasses(state, evenPadding)}`}>{children}</span>
);
export function StateBadge({ state = BadgeState.Success, evenPadding = false, children }: StateBadgeProps) {
return <span className={`${getBadgeClasses(state, evenPadding)}`}>{children}</span>;
}
+2 -9
View File
@@ -5,13 +5,7 @@ import { clsx } from "clsx";
import Link from "next/link";
import { useSelectedLayoutSegment } from "next/navigation";
export const Tab = ({
path,
item: { slug, text },
}: {
path: string;
item: Item;
}) => {
export const Tab = ({ path, item: { slug, text } }: { path: string; item: Item }) => {
const segment = useSelectedLayoutSegment();
const href = slug ? path + "/" + slug : path;
const isActive =
@@ -24,8 +18,7 @@ export const Tab = ({
<Link
href={href}
className={clsx("mr-2 mt-2 rounded-lg px-3 py-1 text-sm font-medium", {
"bg-gray-700 text-gray-100 hover:bg-gray-500 hover:text-white":
!isActive,
"bg-gray-700 text-gray-100 hover:bg-gray-500 hover:text-white": !isActive,
"bg-blue-500 text-white": isActive,
})}
>
+1 -6
View File
@@ -4,12 +4,7 @@ import { ReactNode } from "react";
export function ThemeProvider({ children }: { children: ReactNode }) {
return (
<ThemeP
attribute="class"
defaultTheme="system"
storageKey="cp-theme"
value={{ dark: "dark" }}
>
<ThemeP attribute="class" defaultTheme="system" storageKey="cp-theme" value={{ dark: "dark" }}>
{children}
</ThemeP>
);
+3 -3
View File
@@ -1,9 +1,9 @@
"use client";
import { APPEARANCE_STYLES, getComponentRoundness, getThemeConfig } from "@/lib/theme";
import { MoonIcon, SunIcon } from "@heroicons/react/24/outline";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { getThemeConfig, getComponentRoundness, APPEARANCE_STYLES } from "@/lib/theme";
function getThemeToggleRoundness() {
return getComponentRoundness("themeSwitch");
@@ -51,14 +51,14 @@ export default function ThemeSwitch() {
return (
<div className={`flex space-x-1 p-1 ${toggleRoundness} ${cardAppearance}`}>
<button
className={`w-8 h-8 flex flex-row items-center justify-center ${toggleRoundness} transition-colors ${getSelectedButtonStyle(theme === "light")}`}
className={`flex h-8 w-8 flex-row items-center justify-center ${toggleRoundness} transition-colors ${getSelectedButtonStyle(theme === "light")}`}
onClick={() => setTheme("light")}
aria-label="Switch to light mode"
>
<SunIcon className="h-5 w-5" />
</button>
<button
className={`w-8 h-8 flex flex-row items-center justify-center ${toggleRoundness} transition-colors ${getSelectedButtonStyle(theme === "dark")}`}
className={`flex h-8 w-8 flex-row items-center justify-center ${toggleRoundness} transition-colors ${getSelectedButtonStyle(theme === "dark")}`}
onClick={() => setTheme("dark")}
aria-label="Switch to dark mode"
>
+1 -1
View File
@@ -2,8 +2,8 @@
import { setTheme } from "@/helpers/colors";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { ReactNode, useEffect } from "react";
import { useTheme } from "next-themes";
import { ReactNode, useEffect } from "react";
type Props = {
branding: BrandingSettings | undefined;
@@ -27,9 +27,7 @@ describe("TotpRegister", () => {
afterEach(cleanup);
test("should autofocus the code input on mount", () => {
const { getByTestId } = render(
<TotpRegister uri="otpauth://totp/test" secret="SECRET" />,
);
const { getByTestId } = render(<TotpRegister uri="otpauth://totp/test" secret="SECRET" />);
expect(getByTestId("code-text-input")).toHaveFocus();
});
});
+4 -4
View File
@@ -1,22 +1,22 @@
"use client";
import { completeFlowOrGetUrl } from "@/lib/client";
import { handleServerActionResponse } from "@/lib/client-utils";
import { verifyTOTP } from "@/lib/server/verify";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { QRCodeSVG } from "qrcode.react";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import { Alert } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { Button, ButtonVariants } from "./button";
import { CopyToClipboard } from "./copy-to-clipboard";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
import { handleServerActionResponse } from "@/lib/client-utils";
import { completeFlowOrGetUrl } from "@/lib/client";
type Inputs = {
code: string;
+1 -1
View File
@@ -1,7 +1,7 @@
import { Avatar } from "@/components/avatar";
import { getComponentRoundness } from "@/lib/theme";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import { getComponentRoundness } from "@/lib/theme";
// Helper function to get user avatar container roundness from theme
function getUserAvatarRoundness(): string {
@@ -19,13 +19,7 @@ describe("UsernameForm", () => {
test("should autofocus the loginName input on mount", () => {
const { getByTestId } = render(
<UsernameForm
loginName=""
requestId={undefined}
loginSettings={undefined}
submit={false}
allowRegister={false}
/>,
<UsernameForm loginName="" requestId={undefined} loginSettings={undefined} submit={false} allowRegister={false} />,
);
expect(getByTestId("username-text-input")).toHaveFocus();
});
+3 -3
View File
@@ -1,19 +1,19 @@
"use client";
import { sendLoginname } from "@/lib/server/loginname";
import { handleServerActionResponse } from "@/lib/client-utils";
import { sendLoginname } from "@/lib/server/loginname";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { Alert } from "./alert";
import { AutoSubmitForm } from "./auto-submit-form";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { useTranslations } from "next-intl";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs = {
loginName: string;

Some files were not shown because too many files have changed in this diff Show More