diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index 44601e18450..8f96927dd0b 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -54,6 +54,7 @@ export default async function Page(props: { loginName={loginName} authRequestId={authRequestId} organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user + loginSettings={loginSettings} submit={submit} allowRegister={!!loginSettings?.allowRegister} > diff --git a/apps/login/src/components/username-form.tsx b/apps/login/src/components/username-form.tsx index 887c454333c..3a35bdd120a 100644 --- a/apps/login/src/components/username-form.tsx +++ b/apps/login/src/components/username-form.tsx @@ -1,6 +1,7 @@ "use client"; 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 { ReactNode, useEffect, useState } from "react"; @@ -18,6 +19,7 @@ type Inputs = { type Props = { loginName: string | undefined; authRequestId: string | undefined; + loginSettings: LoginSettings | undefined; organization?: string; submit: boolean; allowRegister: boolean; @@ -28,6 +30,7 @@ export function UsernameForm({ loginName, authRequestId, organization, + loginSettings, submit, allowRegister, children, @@ -80,6 +83,18 @@ export function UsernameForm({ } }, []); + let inputLabel = "Loginname"; + if ( + loginSettings?.disableLoginWithEmail && + loginSettings?.disableLoginWithPhone + ) { + inputLabel = "Username"; + } else if (loginSettings?.disableLoginWithEmail) { + inputLabel = "Username or phone number"; + } else if (loginSettings?.disableLoginWithPhone) { + inputLabel = "Username or email"; + } + return (
@@ -87,7 +102,7 @@ export function UsernameForm({ type="text" autoComplete="username" {...register("loginName", { required: "This field is required" })} - label="Loginname" + label={inputLabel} data-testid="username-text-input" /> {allowRegister && ( diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 7cb74b8d7a4..704c01b9670 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -16,8 +16,8 @@ import { getOrgsByDomain, listAuthenticationMethodTypes, listIDPLinks, - listUsers, - ListUsersCommand, + searchUsers, + SearchUsersCommand, startIdentityProviderFlow, } from "../zitadel"; import { createSessionAndUpdateCookie } from "./cookie"; @@ -33,20 +33,17 @@ const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; export async function sendLoginname(command: SendLoginnameCommand) { const loginSettingsByContext = await getLoginSettings(command.organization); - let listUsersRequest: ListUsersCommand = { - userName: command.loginName, + if (!loginSettingsByContext) { + return { error: "Could not get login settings" }; + } + + let searchUsersRequest: SearchUsersCommand = { + searchValue: command.loginName, organizationId: command.organization, + loginSettings: loginSettingsByContext, }; - if (!loginSettingsByContext?.disableLoginWithEmail) { - listUsersRequest.email = command.loginName; - } - - if (!loginSettingsByContext?.disableLoginWithPhone) { - listUsersRequest.phone = command.loginName; - } - - const { result: potentialUsers } = await listUsers(listUsersRequest); + const { result: potentialUsers } = await searchUsers(searchUsersRequest); const redirectUserToSingleIDPIfAvailable = async () => { const identityProviders = await getActiveIdentityProviders( diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 112b7bd81d8..bda31b0ab97 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -26,6 +26,7 @@ import { create, Duration } from "@zitadel/client"; import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb"; import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb"; import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb"; import { @@ -408,6 +409,118 @@ export async function listUsers({ ); } + if (organizationId) { + queries.push( + create(SearchQuerySchema, { + query: { + case: "organizationIdQuery", + value: { + organizationId, + }, + }, + }), + ); + } + console.log(queries); + + return userService.listUsers({ queries: queries }); +} + +export type SearchUsersCommand = { + searchValue: string; + loginSettings: LoginSettings; + organizationId?: string; +}; + +const PhoneQuery = (searchValue: string) => + create(SearchQuerySchema, { + query: { + case: "phoneQuery", + value: { + number: searchValue, + method: TextQueryMethod.EQUALS, + }, + }, + }); + +const UserNameQuery = (searchValue: string) => + create(SearchQuerySchema, { + query: { + case: "userNameQuery", + value: { + userName: searchValue, + method: TextQueryMethod.EQUALS, + }, + }, + }); + +const EmailQuery = (searchValue: string) => + create(SearchQuerySchema, { + query: { + case: "emailQuery", + value: { + emailAddress: searchValue, + method: TextQueryMethod.EQUALS, + }, + }, + }); + +export async function searchUsers({ + searchValue, + loginSettings, + organizationId, +}: SearchUsersCommand) { + console.log(loginSettings); + const queries: SearchQuery[] = []; + const orQueries: SearchQuery[] = []; + + // either use loginName or userName, email, phone + if ( + loginSettings.disableLoginWithEmail && + loginSettings.disableLoginWithPhone + ) { + const userNameQuery = UserNameQuery(searchValue); + queries.push(userNameQuery); + } else if (loginSettings.disableLoginWithEmail) { + const userNameQuery = UserNameQuery(searchValue); + orQueries.push(userNameQuery); + + if (searchValue.length <= 20) { + const phoneQuery = PhoneQuery(searchValue); + orQueries.push(phoneQuery); + } + } else if (loginSettings.disableLoginWithPhone) { + const userNameQuery = UserNameQuery(searchValue); + orQueries.push(userNameQuery); + + const emailQuery = EmailQuery(searchValue); + orQueries.push(emailQuery); + } else { + const userNameQuery = UserNameQuery(searchValue); + orQueries.push(userNameQuery); + + const emailQuery = EmailQuery(searchValue); + orQueries.push(emailQuery); + + if (searchValue.length <= 20) { + const phoneQuery = PhoneQuery(searchValue); + orQueries.push(phoneQuery); + } + } + + if (orQueries.length > 0) { + queries.push( + create(SearchQuerySchema, { + query: { + case: "orQuery", + value: { + queries: orQueries, + }, + }, + }), + ); + } + if (organizationId) { queries.push( create(SearchQuerySchema, {