Merge branch 'main' into dependabot/github_actions/actions/setup-node-4

This commit is contained in:
Max Peintner
2025-01-14 11:12:16 +01:00
committed by GitHub
28 changed files with 569 additions and 84 deletions

View File

@@ -1,7 +1,7 @@
services: services:
zitadel: zitadel:
user: "${ZITADEL_DEV_UID}" user: "${ZITADEL_DEV_UID}"
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.65.0}" image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.67.2}"
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml' command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
ports: ports:
- "8080:8080" - "8080:8080"
@@ -22,7 +22,7 @@ services:
- POSTGRES_HOST_AUTH_METHOD=trust - POSTGRES_HOST_AUTH_METHOD=trust
command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c shared_buffers=1GB -c work_mem=16MB -c effective_io_concurrency=100 -c wal_level=minimal -c archive_mode=off -c max_wal_senders=0 command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c shared_buffers=1GB -c work_mem=16MB -c effective_io_concurrency=100 -c wal_level=minimal -c archive_mode=off -c max_wal_senders=0
healthcheck: healthcheck:
test: [ "CMD-SHELL", "pg_isready" ] test: ["CMD-SHELL", "pg_isready"]
interval: "10s" interval: "10s"
timeout: "30s" timeout: "30s"
retries: 5 retries: 5

View File

@@ -40,6 +40,7 @@ echo "ZITADEL_API_URL=${ZITADEL_API_URL}
ZITADEL_SERVICE_USER_ID=${ZITADEL_SERVICE_USER_ID} ZITADEL_SERVICE_USER_ID=${ZITADEL_SERVICE_USER_ID}
ZITADEL_SERVICE_USER_TOKEN=${PAT} ZITADEL_SERVICE_USER_TOKEN=${PAT}
SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL} SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL}
EMAIL_VERIFICATION=true
DEBUG=true"| tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null DEBUG=true"| tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null
echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}" echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}"
cat ${WRITE_ENVIRONMENT_FILE} cat ${WRITE_ENVIRONMENT_FILE}

View File

@@ -0,0 +1,12 @@
import { expect, Page } from "@playwright/test";
const codeTextInput = "code-text-input";
export async function emailVerifyScreen(page: Page, code: string) {
await page.getByTestId(codeTextInput).pressSequentially(code);
}
export async function emailVerifyScreenExpect(page: Page, code: string) {
await expect(page.getByTestId(codeTextInput)).toHaveValue(code);
await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify email");
}

View File

@@ -0,0 +1,73 @@
import { faker } from "@faker-js/faker";
import { test as base } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { emailVerify, emailVerifyResend } from "./email-verify";
import { emailVerifyScreenExpect } from "./email-verify-screen";
import { loginScreenExpect, loginWithPassword } from "./login";
import { getCodeFromSink } from "./sink";
import { PasswordUser } from "./user";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, ".env.local") });
const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => {
const user = new PasswordUser({
email: faker.internet.email(),
isEmailVerified: false,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
organization: "",
phone: faker.phone.number(),
isPhoneVerified: false,
password: "Password1!",
passwordChangeRequired: false,
});
await user.ensure(page);
await use(user);
await user.cleanup();
},
});
test("user email not verified, verify", async ({ user, page }) => {
await loginWithPassword(page, user.getUsername(), user.getPassword());
// auto-redirect on /verify
// wait for send of the code
await page.waitForTimeout(3000);
const c = await getCodeFromSink(user.getUsername());
await emailVerify(page, c);
await loginScreenExpect(page, user.getFullName());
});
test("user email not verified, resend, verify", async ({ user, page }) => {
await loginWithPassword(page, user.getUsername(), user.getPassword());
// auto-redirect on /verify
await emailVerifyResend(page);
// wait for send of the code
await page.waitForTimeout(3000);
const c = await getCodeFromSink(user.getUsername());
await emailVerify(page, c);
await loginScreenExpect(page, user.getFullName());
});
test("user email not verified, resend, old code", async ({ user, page }) => {
await loginWithPassword(page, user.getUsername(), user.getPassword());
// auto-redirect on /verify
// wait for send of the code
await page.waitForTimeout(3000);
const c = await getCodeFromSink(user.getUsername());
await emailVerifyResend(page);
// wait for resend of the code
await page.waitForTimeout(1000);
await emailVerify(page, c);
await emailVerifyScreenExpect(page, c);
});
test("user email not verified, wrong code", async ({ user, page }) => {
await loginWithPassword(page, user.getUsername(), user.getPassword());
// auto-redirect on /verify
const code = "wrong";
await emailVerify(page, code);
await emailVerifyScreenExpect(page, code);
});

View File

@@ -0,0 +1,15 @@
import { Page } from "@playwright/test";
import { emailVerifyScreen } from "./email-verify-screen";
export async function startEmailVerify(page: Page, loginname: string) {
await page.goto("/verify");
}
export async function emailVerify(page: Page, code: string) {
await emailVerifyScreen(page, code);
await page.getByTestId("submit-button").click();
}
export async function emailVerifyResend(page: Page) {
await page.getByTestId("resend-button").click();
}

View File

@@ -4,6 +4,10 @@ import { getCodeFromSink } from "./sink";
const codeField = "code-text-input"; const codeField = "code-text-input";
const passwordField = "password-text-input"; const passwordField = "password-text-input";
const passwordConfirmField = "password-confirm-text-input"; const passwordConfirmField = "password-confirm-text-input";
const passwordChangeField = "password-change-text-input";
const passwordChangeConfirmField = "password-change-confirm-text-input";
const passwordSetField = "password-set-text-input";
const passwordSetConfirmField = "password-set-confirm-text-input";
const lengthCheck = "length-check"; const lengthCheck = "length-check";
const symbolCheck = "symbol-check"; const symbolCheck = "symbol-check";
const numberCheck = "number-check"; const numberCheck = "number-check";
@@ -15,8 +19,8 @@ const matchText = "Matches";
const noMatchText = "Doesn't match"; const noMatchText = "Doesn't match";
export async function changePasswordScreen(page: Page, password1: string, password2: string) { export async function changePasswordScreen(page: Page, password1: string, password2: string) {
await page.getByTestId(passwordField).pressSequentially(password1); await page.getByTestId(passwordChangeField).pressSequentially(password1);
await page.getByTestId(passwordConfirmField).pressSequentially(password2); await page.getByTestId(passwordChangeConfirmField).pressSequentially(password2);
} }
export async function passwordScreen(page: Page, password: string) { export async function passwordScreen(page: Page, password: string) {
@@ -39,9 +43,21 @@ export async function changePasswordScreenExpect(
lowercase: boolean, lowercase: boolean,
equals: boolean, equals: boolean,
) { ) {
await expect(page.getByTestId(passwordField)).toHaveValue(password1); await expect(page.getByTestId(passwordChangeField)).toHaveValue(password1);
await expect(page.getByTestId(passwordConfirmField)).toHaveValue(password2); await expect(page.getByTestId(passwordChangeConfirmField)).toHaveValue(password2);
await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals);
}
async function checkComplexity(
page: Page,
length: boolean,
symbol: boolean,
number: boolean,
uppercase: boolean,
lowercase: boolean,
equals: boolean,
) {
await checkContent(page, lengthCheck, length); await checkContent(page, lengthCheck, length);
await checkContent(page, symbolCheck, symbol); await checkContent(page, symbolCheck, symbol);
await checkContent(page, numberCheck, number); await checkContent(page, numberCheck, number);
@@ -63,8 +79,8 @@ export async function resetPasswordScreen(page: Page, username: string, password
await page.waitForTimeout(3000); await page.waitForTimeout(3000);
const c = await getCodeFromSink(username); const c = await getCodeFromSink(username);
await page.getByTestId(codeField).pressSequentially(c); await page.getByTestId(codeField).pressSequentially(c);
await page.getByTestId(passwordField).pressSequentially(password1); await page.getByTestId(passwordSetField).pressSequentially(password1);
await page.getByTestId(passwordConfirmField).pressSequentially(password2); await page.getByTestId(passwordSetConfirmField).pressSequentially(password2);
} }
export async function resetPasswordScreenExpect( export async function resetPasswordScreenExpect(
@@ -78,5 +94,8 @@ export async function resetPasswordScreenExpect(
lowercase: boolean, lowercase: boolean,
equals: boolean, equals: boolean,
) { ) {
await changePasswordScreenExpect(page, password1, password2, length, symbol, number, uppercase, lowercase, equals); await expect(page.getByTestId(passwordSetField)).toHaveValue(password1);
await expect(page.getByTestId(passwordSetConfirmField)).toHaveValue(password2);
await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals);
} }

View File

@@ -8,8 +8,7 @@ export async function startChangePassword(page: Page, loginname: string) {
await page.goto("/password/change?" + new URLSearchParams({ loginName: loginname })); await page.goto("/password/change?" + new URLSearchParams({ loginName: loginname }));
} }
export async function changePassword(page: Page, loginname: string, password: string) { export async function changePassword(page: Page, password: string) {
await startChangePassword(page, loginname);
await changePasswordScreen(page, password, password); await changePasswordScreen(page, password, password);
await page.getByTestId(passwordSubmitButton).click(); await page.getByTestId(passwordSubmitButton).click();
} }

View File

@@ -1,6 +1,8 @@
import { Page } from "@playwright/test"; import { Page } from "@playwright/test";
import { emailVerify } from "./email-verify";
import { passkeyRegister } from "./passkey"; import { passkeyRegister } from "./passkey";
import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen"; import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen";
import { getCodeFromSink } from "./sink";
export async function registerWithPassword( export async function registerWithPassword(
page: Page, page: Page,
@@ -15,6 +17,9 @@ export async function registerWithPassword(
await page.getByTestId("submit-button").click(); await page.getByTestId("submit-button").click();
await registerPasswordScreen(page, password1, password2); await registerPasswordScreen(page, password1, password2);
await page.getByTestId("submit-button").click(); await page.getByTestId("submit-button").click();
await page.waitForTimeout(3000);
await verifyEmail(page, email);
} }
export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise<string> { export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise<string> {
@@ -23,7 +28,15 @@ export async function registerWithPasskey(page: Page, firstname: string, lastnam
await page.getByTestId("submit-button").click(); await page.getByTestId("submit-button").click();
// wait for projection of user // wait for projection of user
await page.waitForTimeout(2000); await page.waitForTimeout(3000);
const authId = await passkeyRegister(page);
return await passkeyRegister(page); await verifyEmail(page, email);
return authId;
}
async function verifyEmail(page: Page, email: string) {
await page.waitForTimeout(1000);
const c = await getCodeFromSink(email);
await emailVerify(page, c);
} }

View File

@@ -4,11 +4,14 @@ import { activateOTP, addTOTP, addUser, getUserByUsername, removeUser } from "./
export interface userProps { export interface userProps {
email: string; email: string;
isEmailVerified?: boolean;
firstName: string; firstName: string;
lastName: string; lastName: string;
organization: string; organization: string;
password: string; password: string;
passwordChangeRequired?: boolean;
phone: string; phone: string;
isPhoneVerified?: boolean;
} }
class User { class User {
@@ -77,11 +80,14 @@ export enum OtpType {
export interface otpUserProps { export interface otpUserProps {
email: string; email: string;
isEmailVerified?: boolean;
firstName: string; firstName: string;
lastName: string; lastName: string;
organization: string; organization: string;
password: string; password: string;
passwordChangeRequired?: boolean;
phone: string; phone: string;
isPhoneVerified?: boolean;
type: OtpType; type: OtpType;
} }
@@ -96,6 +102,9 @@ export class PasswordUserWithOTP extends User {
organization: props.organization, organization: props.organization,
password: props.password, password: props.password,
phone: props.phone, phone: props.phone,
isEmailVerified: props.isEmailVerified,
isPhoneVerified: props.isPhoneVerified,
passwordChangeRequired: props.passwordChangeRequired,
}); });
this.type = props.type; this.type = props.type;
} }
@@ -133,6 +142,8 @@ export interface passkeyUserProps {
lastName: string; lastName: string;
organization: string; organization: string;
phone: string; phone: string;
isEmailVerified?: boolean;
isPhoneVerified?: boolean;
} }
export class PasskeyUser extends User { export class PasskeyUser extends User {
@@ -146,6 +157,8 @@ export class PasskeyUser extends User {
organization: props.organization, organization: props.organization,
password: "", password: "",
phone: props.phone, phone: props.phone,
isEmailVerified: props.isEmailVerified,
isPhoneVerified: props.isPhoneVerified,
}); });
} }

View File

@@ -12,10 +12,12 @@ const test = base.extend<{ user: PasskeyUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {
const user = new PasskeyUser({ const user = new PasskeyUser({
email: faker.internet.email(), email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(), firstName: faker.person.firstName(),
lastName: faker.person.lastName(), lastName: faker.person.lastName(),
organization: "", organization: "",
phone: faker.phone.number(), phone: faker.phone.number(),
isPhoneVerified: false,
}); });
await user.ensure(page); await user.ensure(page);
await use(user); await use(user);

View File

@@ -0,0 +1,41 @@
import { faker } from "@faker-js/faker";
import { test as base } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { loginScreenExpect, loginWithPassword } from "./login";
import { changePassword } from "./password";
import { PasswordUser } from "./user";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, ".env.local") });
const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => {
const user = new PasswordUser({
email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
organization: "",
phone: faker.phone.number(),
isPhoneVerified: false,
password: "Password1!",
passwordChangeRequired: true,
});
await user.ensure(page);
await use(user);
await user.cleanup();
},
});
test("username and password login, change required", async ({ user, page }) => {
const changedPw = "ChangedPw1!";
await loginWithPassword(page, user.getUsername(), user.getPassword());
await page.waitForTimeout(100);
await changePassword(page, changedPw);
await loginScreenExpect(page, user.getFullName());
await loginWithPassword(page, user.getUsername(), changedPw);
await loginScreenExpect(page, user.getFullName());
});

View File

@@ -2,8 +2,8 @@ import { faker } from "@faker-js/faker";
import { test as base } from "@playwright/test"; import { test as base } from "@playwright/test";
import dotenv from "dotenv"; import dotenv from "dotenv";
import path from "path"; import path from "path";
import { loginWithPassword } from "./login"; import { loginScreenExpect, loginWithPassword } from "./login";
import { startChangePassword } from "./password"; import { changePassword, startChangePassword } from "./password";
import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen"; import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen";
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
@@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {
const user = new PasswordUser({ const user = new PasswordUser({
email: faker.internet.email(), email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(), firstName: faker.person.firstName(),
lastName: faker.person.lastName(), lastName: faker.person.lastName(),
organization: "", organization: "",
phone: faker.phone.number(), phone: faker.phone.number(),
isPhoneVerified: false,
password: "Password1!", password: "Password1!",
passwordChangeRequired: false,
}); });
await user.ensure(page); await user.ensure(page);
await use(user); await use(user);
@@ -27,20 +30,18 @@ const test = base.extend<{ user: PasswordUser }>({
}); });
test("username and password changed login", async ({ user, page }) => { test("username and password changed login", async ({ user, page }) => {
// commented, fix in https://github.com/zitadel/zitadel/pull/8807 const changedPw = "ChangedPw1!";
/* await loginWithPassword(page, user.getUsername(), user.getPassword());
const changedPw = "ChangedPw1!";
await loginWithPassword(page, user.getUsername(), user.getPassword());
// wait for projection of token // wait for projection of token
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
await changePassword(page, user.getUsername(), changedPw); await startChangePassword(page, user.getUsername());
await loginScreenExpect(page, user.getFullName()); await changePassword(page, changedPw);
await loginScreenExpect(page, user.getFullName());
await loginWithPassword(page, user.getUsername(), changedPw); await loginWithPassword(page, user.getUsername(), changedPw);
await loginScreenExpect(page, user.getFullName()); await loginScreenExpect(page, user.getFullName());
*/
}); });
test("password change not with desired complexity", async ({ user, page }) => { test("password change not with desired complexity", async ({ user, page }) => {

View File

@@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {
const user = new PasswordUserWithOTP({ const user = new PasswordUserWithOTP({
email: faker.internet.email(), email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(), firstName: faker.person.firstName(),
lastName: faker.person.lastName(), lastName: faker.person.lastName(),
organization: "", organization: "",
phone: faker.phone.number(), phone: faker.phone.number(),
isPhoneVerified: false,
password: "Password1!", password: "Password1!",
passwordChangeRequired: false,
type: OtpType.email, type: OtpType.email,
}); });

View File

@@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {
const user = new PasswordUserWithOTP({ const user = new PasswordUserWithOTP({
email: faker.internet.email(), email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(), firstName: faker.person.firstName(),
lastName: faker.person.lastName(), lastName: faker.person.lastName(),
organization: "", organization: "",
phone: faker.phone.number({ style: "international" }), phone: faker.phone.number({ style: "international" }),
isPhoneVerified: true,
password: "Password1!", password: "Password1!",
passwordChangeRequired: false,
type: OtpType.sms, type: OtpType.sms,
}); });

View File

@@ -15,11 +15,14 @@ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {
const user = new PasswordUser({ const user = new PasswordUser({
email: faker.internet.email(), email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(), firstName: faker.person.firstName(),
lastName: faker.person.lastName(), lastName: faker.person.lastName(),
organization: "", organization: "",
phone: faker.phone.number(), phone: faker.phone.number(),
isPhoneVerified: false,
password: "Password1!", password: "Password1!",
passwordChangeRequired: false,
}); });
await user.ensure(page); await user.ensure(page);
await use(user); await use(user);
@@ -28,8 +31,6 @@ const test = base.extend<{ user: PasswordUser }>({
}); });
test("username and password set login", async ({ user, page }) => { test("username and password set login", async ({ user, page }) => {
// commented, fix in https://github.com/zitadel/zitadel/pull/8807
const changedPw = "ChangedPw1!"; const changedPw = "ChangedPw1!";
await startLogin(page); await startLogin(page);
await loginname(page, user.getUsername()); await loginname(page, user.getUsername());

View File

@@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {
const user = new PasswordUserWithTOTP({ const user = new PasswordUserWithTOTP({
email: faker.internet.email(), email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(), firstName: faker.person.firstName(),
lastName: faker.person.lastName(), lastName: faker.person.lastName(),
organization: "", organization: "",
phone: faker.phone.number({ style: "international" }), phone: faker.phone.number({ style: "international" }),
isPhoneVerified: true,
password: "Password1!", password: "Password1!",
passwordChangeRequired: false,
}); });
await user.ensure(page); await user.ensure(page);

View File

@@ -16,11 +16,14 @@ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {
const user = new PasswordUser({ const user = new PasswordUser({
email: faker.internet.email(), email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(), firstName: faker.person.firstName(),
lastName: faker.person.lastName(), lastName: faker.person.lastName(),
organization: "", organization: "",
phone: faker.phone.number(), phone: faker.phone.number(),
isPhoneVerified: false,
password: "Password1!", password: "Password1!",
passwordChangeRequired: false,
}); });
await user.ensure(page); await user.ensure(page);
await use(user); await use(user);

View File

@@ -19,13 +19,20 @@ export async function addUser(props: userProps) {
isVerified: true, isVerified: true,
}, },
phone: { phone: {
phone: props.phone!, phone: props.phone,
isVerified: true, isVerified: true,
}, },
password: { password: {
password: props.password!, password: props.password,
changeRequired: props.passwordChangeRequired ?? false,
}, },
}; };
if (!props.isEmailVerified) {
delete body.email.isVerified;
}
if (!props.isPhoneVerified) {
delete body.phone.isVerified;
}
return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body); return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body);
} }

View File

@@ -395,6 +395,5 @@ Timebased features like the multifactor init prompt or password expiry, are not
- Password Expiry Settings - Password Expiry Settings
- Login Settings: multifactor init prompt - Login Settings: multifactor init prompt
- forceMFA on login settings is not checked for IDPs - forceMFA on login settings is not checked for IDPs
- disablePhone / disableEmail from loginSettings will be implemented right after https://github.com/zitadel/zitadel/issues/9016 is merged
Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced. Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced.

View File

@@ -20,6 +20,7 @@ export default async function Page(props: {
const loginName = searchParams?.loginName; const loginName = searchParams?.loginName;
const authRequestId = searchParams?.authRequestId; const authRequestId = searchParams?.authRequestId;
const organization = searchParams?.organization; const organization = searchParams?.organization;
const suffix = searchParams?.suffix;
const submit: boolean = searchParams?.submit === "true"; const submit: boolean = searchParams?.submit === "true";
let defaultOrganization; let defaultOrganization;
@@ -34,6 +35,8 @@ export default async function Page(props: {
organization ?? defaultOrganization, organization ?? defaultOrganization,
); );
const contextLoginSettings = await getLoginSettings(organization);
const identityProviders = await getActiveIdentityProviders( const identityProviders = await getActiveIdentityProviders(
organization ?? defaultOrganization, organization ?? defaultOrganization,
).then((resp) => { ).then((resp) => {
@@ -54,6 +57,8 @@ export default async function Page(props: {
loginName={loginName} loginName={loginName}
authRequestId={authRequestId} 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 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={contextLoginSettings}
suffix={suffix}
submit={submit} submit={submit}
allowRegister={!!loginSettings?.allowRegister} allowRegister={!!loginSettings?.allowRegister}
> >

View File

@@ -300,6 +300,7 @@ export async function GET(request: NextRequest) {
const { authRequest } = await getAuthRequest({ authRequestId }); const { authRequest } = await getAuthRequest({ authRequestId });
let organization = ""; let organization = "";
let suffix = "";
let idpId = ""; let idpId = "";
if (authRequest?.scope) { if (authRequest?.scope) {
@@ -326,6 +327,7 @@ export async function GET(request: NextRequest) {
const orgs = await getOrgsByDomain(orgDomain); const orgs = await getOrgsByDomain(orgDomain);
if (orgs.result && orgs.result.length === 1) { if (orgs.result && orgs.result.length === 1) {
organization = orgs.result[0].id ?? ""; organization = orgs.result[0].id ?? "";
suffix = orgDomain;
} }
} }
} }
@@ -448,6 +450,9 @@ export async function GET(request: NextRequest) {
if (organization) { if (organization) {
loginNameUrl.searchParams.set("organization", organization); loginNameUrl.searchParams.set("organization", organization);
} }
if (suffix) {
loginNameUrl.searchParams.set("suffix", suffix);
}
return NextResponse.redirect(loginNameUrl); return NextResponse.redirect(loginNameUrl);
} else if (authRequest.prompt.includes(Prompt.NONE)) { } else if (authRequest.prompt.includes(Prompt.NONE)) {
/** /**

View File

@@ -161,7 +161,7 @@ export function ChangePasswordForm({
})} })}
label="New Password" label="New Password"
error={errors.password?.message as string} error={errors.password?.message as string}
data-testid="password-text-input" data-testid="password-change-text-input"
/> />
</div> </div>
<div className=""> <div className="">
@@ -174,7 +174,7 @@ export function ChangePasswordForm({
})} })}
label="Confirm Password" label="Confirm Password"
error={errors.confirmPassword?.message as string} error={errors.confirmPassword?.message as string}
data-testid="password-confirm-text-input" data-testid="password-change-confirm-text-input"
/> />
</div> </div>
</div> </div>

View File

@@ -15,6 +15,7 @@ export type TextInputProps = DetailedHTMLProps<
HTMLInputElement HTMLInputElement
> & { > & {
label: string; label: string;
suffix?: string;
placeholder?: string; placeholder?: string;
defaultValue?: string; defaultValue?: string;
error?: string | ReactNode; error?: string | ReactNode;
@@ -45,6 +46,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
label, label,
placeholder, placeholder,
defaultValue, defaultValue,
suffix,
required = false, required = false,
error, error,
disabled, disabled,
@@ -56,7 +58,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
ref, ref,
) => { ) => {
return ( return (
<label className="flex flex-col text-12px text-input-light-label dark:text-input-dark-label"> <label className="relative flex flex-col text-12px text-input-light-label dark:text-input-dark-label">
<span <span
className={`leading-3 mb-1 ${ className={`leading-3 mb-1 ${
error ? "text-warn-light-500 dark:text-warn-dark-500" : "" error ? "text-warn-light-500 dark:text-warn-dark-500" : ""
@@ -78,6 +80,12 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
{...props} {...props}
/> />
{suffix && (
<span className="z-30 absolute right-[3px] bottom-[22px] transform translate-y-1/2 bg-background-light-500 dark:bg-background-dark-500 p-2 rounded-sm">
@{suffix}
</span>
)}
<div className="leading-14.5px h-14.5px text-warn-light-500 dark:text-warn-dark-500 flex flex-row items-center text-12px"> <div className="leading-14.5px h-14.5px text-warn-light-500 dark:text-warn-dark-500 flex flex-row items-center text-12px">
<span>{error ? error : " "}</span> <span>{error ? error : " "}</span>
</div> </div>

View File

@@ -237,7 +237,7 @@ export function SetPasswordForm({
})} })}
label="New Password" label="New Password"
error={errors.password?.message as string} error={errors.password?.message as string}
data-testid="password-text-input" data-testid="password-set-text-input"
/> />
</div> </div>
<div> <div>
@@ -250,7 +250,7 @@ export function SetPasswordForm({
})} })}
label="Confirm Password" label="Confirm Password"
error={errors.confirmPassword?.message as string} error={errors.confirmPassword?.message as string}
data-testid="password-confirm-text-input" data-testid="password-set-confirm-text-input"
/> />
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { sendLoginname } from "@/lib/server/loginname"; import { sendLoginname } from "@/lib/server/loginname";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
@@ -18,7 +19,9 @@ type Inputs = {
type Props = { type Props = {
loginName: string | undefined; loginName: string | undefined;
authRequestId: string | undefined; authRequestId: string | undefined;
loginSettings: LoginSettings | undefined;
organization?: string; organization?: string;
suffix?: string;
submit: boolean; submit: boolean;
allowRegister: boolean; allowRegister: boolean;
children?: ReactNode; children?: ReactNode;
@@ -28,6 +31,8 @@ export function UsernameForm({
loginName, loginName,
authRequestId, authRequestId,
organization, organization,
suffix,
loginSettings,
submit, submit,
allowRegister, allowRegister,
children, children,
@@ -52,6 +57,7 @@ export function UsernameForm({
loginName: values.loginName, loginName: values.loginName,
organization, organization,
authRequestId, authRequestId,
suffix,
}) })
.catch(() => { .catch(() => {
setError("An internal error occurred"); setError("An internal error occurred");
@@ -80,6 +86,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 ( return (
<form className="w-full"> <form className="w-full">
<div className=""> <div className="">
@@ -87,8 +105,9 @@ export function UsernameForm({
type="text" type="text"
autoComplete="username" autoComplete="username"
{...register("loginName", { required: "This field is required" })} {...register("loginName", { required: "This field is required" })}
label="Loginname" label={inputLabel}
data-testid="username-text-input" data-testid="username-text-input"
suffix={suffix}
/> />
{allowRegister && ( {allowRegister && (
<button <button

View File

@@ -115,7 +115,7 @@ export function VerifyForm({
{t("verify.noCodeReceived")} {t("verify.noCodeReceived")}
</span> </span>
<button <button
aria-label="Resend OTP Code" aria-label="Resend Code"
disabled={loading} disabled={loading}
type="button" type="button"
className="ml-4 text-primary-light-500 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 hover:text-primary-light-400 cursor-pointer disabled:cursor-default disabled:text-gray-400 dark:disabled:text-gray-700" className="ml-4 text-primary-light-500 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 hover:text-primary-light-400 cursor-pointer disabled:cursor-default disabled:text-gray-400 dark:disabled:text-gray-700"
@@ -134,11 +134,12 @@ export function VerifyForm({
autoComplete="one-time-code" autoComplete="one-time-code"
{...register("code", { required: "This field is required" })} {...register("code", { required: "This field is required" })}
label="Code" label="Code"
data-testid="code-text-input"
/> />
</div> </div>
{error && ( {error && (
<div className="py-4"> <div className="py-4" data-testid="error">
<Alert>{error}</Alert> <Alert>{error}</Alert>
</div> </div>
)} )}
@@ -152,6 +153,7 @@ export function VerifyForm({
variant={ButtonVariants.Primary} variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid} disabled={loading || !formState.isValid}
onClick={handleSubmit(fcn)} onClick={handleSubmit(fcn)}
data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}
{t("verify.submit")} {t("verify.submit")}

View File

@@ -16,7 +16,8 @@ import {
getOrgsByDomain, getOrgsByDomain,
listAuthenticationMethodTypes, listAuthenticationMethodTypes,
listIDPLinks, listIDPLinks,
listUsers, searchUsers,
SearchUsersCommand,
startIdentityProviderFlow, startIdentityProviderFlow,
} from "../zitadel"; } from "../zitadel";
import { createSessionAndUpdateCookie } from "./cookie"; import { createSessionAndUpdateCookie } from "./cookie";
@@ -25,26 +26,36 @@ export type SendLoginnameCommand = {
loginName: string; loginName: string;
authRequestId?: string; authRequestId?: string;
organization?: string; organization?: string;
suffix?: string;
}; };
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
export async function sendLoginname(command: SendLoginnameCommand) { export async function sendLoginname(command: SendLoginnameCommand) {
const users = await listUsers({ const loginSettingsByContext = await getLoginSettings(command.organization);
loginName: command.loginName,
if (!loginSettingsByContext) {
return { error: "Could not get login settings" };
}
let searchUsersRequest: SearchUsersCommand = {
searchValue: command.loginName,
organizationId: command.organization, organizationId: command.organization,
}); loginSettings: loginSettingsByContext,
suffix: command.suffix,
};
const loginSettings = await getLoginSettings(command.organization); const searchResult = await searchUsers(searchUsersRequest);
const potentialUsers = users.result.filter((u) => { if ("error" in searchResult && searchResult.error) {
const human = u.type.case === "human" ? u.type.value : undefined; return searchResult;
return loginSettings?.disableLoginWithEmail }
? human?.email?.isVerified && human?.email?.email !== command.loginName
: loginSettings?.disableLoginWithPhone if (!("result" in searchResult)) {
? human?.phone?.isVerified && human?.phone?.phone !== command.loginName return { error: "Could not search users" };
: true; }
});
const { result: potentialUsers } = searchResult;
const redirectUserToSingleIDPIfAvailable = async () => { const redirectUserToSingleIDPIfAvailable = async () => {
const identityProviders = await getActiveIdentityProviders( const identityProviders = await getActiveIdentityProviders(
@@ -145,9 +156,50 @@ export async function sendLoginname(command: SendLoginnameCommand) {
} }
}; };
if (potentialUsers.length == 1 && potentialUsers[0].userId) { if (potentialUsers.length > 1) {
return { error: "More than one user found. Provide a unique identifier." };
} else if (potentialUsers.length == 1 && potentialUsers[0].userId) {
const user = potentialUsers[0];
const userId = potentialUsers[0].userId; const userId = potentialUsers[0].userId;
const userLoginSettings = await getLoginSettings(
user.details?.resourceOwner,
);
// compare with the concatenated suffix when set
const concatLoginname = command.suffix
? `${command.loginName}@${command.suffix}`
: command.loginName;
const humanUser =
potentialUsers[0].type.case === "human"
? potentialUsers[0].type.value
: undefined;
// recheck login settings after user discovery, as the search might have been done without org scope
if (
userLoginSettings?.disableLoginWithEmail &&
userLoginSettings?.disableLoginWithPhone
) {
if (user.preferredLoginName !== concatLoginname) {
return { error: "User not found in the system!" };
}
} else if (userLoginSettings?.disableLoginWithEmail) {
if (
user.preferredLoginName !== concatLoginname ||
humanUser?.phone?.phone !== command.loginName
) {
return { error: "User not found in the system!" };
}
} else if (userLoginSettings?.disableLoginWithPhone) {
if (
user.preferredLoginName !== concatLoginname ||
humanUser?.email?.email !== command.loginName
) {
return { error: "User not found in the system!" };
}
}
const checks = create(ChecksSchema, { const checks = create(ChecksSchema, {
user: { search: { case: "userId", value: userId } }, user: { search: { case: "userId", value: userId } },
}); });
@@ -163,7 +215,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
} }
// TODO: check if handling of userstate INITIAL is needed // TODO: check if handling of userstate INITIAL is needed
if (potentialUsers[0].state === UserState.INITIAL) { if (user.state === UserState.INITIAL) {
return { error: "Initial User not supported" }; return { error: "Initial User not supported" };
} }
@@ -173,11 +225,6 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// this can be expected to be an invite as users created in console have a password set. // this can be expected to be an invite as users created in console have a password set.
if (!methods.authMethodTypes || !methods.authMethodTypes.length) { if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
const humanUser =
potentialUsers[0].type.case === "human"
? potentialUsers[0].type.value
: undefined;
// redirect to /verify invite if no auth method is set and email is not verified // redirect to /verify invite if no auth method is set and email is not verified
const inviteCheck = checkInvite( const inviteCheck = checkInvite(
session, session,
@@ -213,7 +260,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const method = methods.authMethodTypes[0]; const method = methods.authMethodTypes[0];
switch (method) { switch (method) {
case AuthenticationMethodType.PASSWORD: // user has only password as auth method case AuthenticationMethodType.PASSWORD: // user has only password as auth method
if (!loginSettings?.allowUsernamePassword) { if (!userLoginSettings?.allowUsernamePassword) {
return { return {
error: error:
"Username Password not allowed! Contact your administrator for more information.", "Username Password not allowed! Contact your administrator for more information.",
@@ -240,7 +287,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
}; };
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
if (loginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) { if (userLoginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) {
return { return {
error: error:
"Passkeys not allowed! Contact your administrator for more information.", "Passkeys not allowed! Contact your administrator for more information.",
@@ -303,22 +350,24 @@ export async function sendLoginname(command: SendLoginnameCommand) {
} }
} }
// user not found, check if register is enabled on organization // user not found, check if register is enabled on instance / organization context
if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) { if (
// TODO: do we need to handle login hints for IDPs here? loginSettingsByContext?.allowRegister &&
!loginSettingsByContext?.allowUsernamePassword
) {
const resp = await redirectUserToSingleIDPIfAvailable(); const resp = await redirectUserToSingleIDPIfAvailable();
if (resp) { if (resp) {
return resp; return resp;
} }
return { error: "User not found in the system" }; return { error: "User not found in the system" };
} else if ( } else if (
loginSettings?.allowRegister && loginSettingsByContext?.allowRegister &&
loginSettings?.allowUsernamePassword loginSettingsByContext?.allowUsernamePassword
) { ) {
let orgToRegisterOn: string | undefined = command.organization; let orgToRegisterOn: string | undefined = command.organization;
if ( if (
!loginSettings?.ignoreUnknownUsernames && !loginSettingsByContext?.ignoreUnknownUsernames &&
!orgToRegisterOn && !orgToRegisterOn &&
command.loginName && command.loginName &&
ORG_SUFFIX_REGEX.test(command.loginName) ORG_SUFFIX_REGEX.test(command.loginName)
@@ -338,7 +387,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
} }
// do not register user if ignoreUnknownUsernames is set // do not register user if ignoreUnknownUsernames is set
if (orgToRegisterOn && !loginSettings?.ignoreUnknownUsernames) { if (orgToRegisterOn && !loginSettingsByContext?.ignoreUnknownUsernames) {
const params = new URLSearchParams({ organization: orgToRegisterOn }); const params = new URLSearchParams({ organization: orgToRegisterOn });
if (command.authRequestId) { if (command.authRequestId) {
@@ -353,7 +402,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
} }
} }
if (loginSettings?.ignoreUnknownUsernames) { if (loginSettingsByContext?.ignoreUnknownUsernames) {
const paramsPasswordDefault = new URLSearchParams({ const paramsPasswordDefault = new URLSearchParams({
loginName: command.loginName, loginName: command.loginName,
}); });

View File

@@ -26,6 +26,7 @@ import { create, Duration } from "@zitadel/client";
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb"; import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb"; import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb";
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb"; import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import { import {
@@ -324,19 +325,24 @@ export async function createInviteCode(userId: string, host: string | null) {
); );
} }
export async function listUsers({ export type ListUsersCommand = {
loginName,
userName,
email,
organizationId,
}: {
loginName?: string; loginName?: string;
userName?: string; userName?: string;
email?: string; email?: string;
phone?: string;
organizationId?: string; organizationId?: string;
}) { };
export async function listUsers({
loginName,
userName,
phone,
email,
organizationId,
}: ListUsersCommand) {
const queries: SearchQuery[] = []; const queries: SearchQuery[] = [];
// either use loginName or userName, email, phone
if (loginName) { if (loginName) {
queries.push( queries.push(
create(SearchQuerySchema, { create(SearchQuerySchema, {
@@ -349,11 +355,11 @@ export async function listUsers({
}, },
}), }),
); );
} } else if (userName || email || phone) {
const orQueries: SearchQuery[] = [];
if (userName) { if (userName) {
queries.push( const userNameQuery = create(SearchQuerySchema, {
create(SearchQuerySchema, {
query: { query: {
case: "userNameQuery", case: "userNameQuery",
value: { value: {
@@ -361,6 +367,44 @@ export async function listUsers({
method: TextQueryMethod.EQUALS, method: TextQueryMethod.EQUALS,
}, },
}, },
});
orQueries.push(userNameQuery);
}
if (email) {
const emailQuery = create(SearchQuerySchema, {
query: {
case: "emailQuery",
value: {
emailAddress: email,
method: TextQueryMethod.EQUALS,
},
},
});
orQueries.push(emailQuery);
}
if (phone) {
const phoneQuery = create(SearchQuerySchema, {
query: {
case: "phoneQuery",
value: {
number: phone,
method: TextQueryMethod.EQUALS,
},
},
});
orQueries.push(phoneQuery);
}
queries.push(
create(SearchQuerySchema, {
query: {
case: "orQuery",
value: {
queries: orQueries,
},
},
}), }),
); );
} }
@@ -378,20 +422,165 @@ export async function listUsers({
); );
} }
if (email) { return userService.listUsers({ queries: queries });
}
export type SearchUsersCommand = {
searchValue: string;
loginSettings: LoginSettings;
organizationId?: string;
suffix?: string;
};
const PhoneQuery = (searchValue: string) =>
create(SearchQuerySchema, {
query: {
case: "phoneQuery",
value: {
number: searchValue,
method: TextQueryMethod.EQUALS,
},
},
});
const LoginNameQuery = (searchValue: string) =>
create(SearchQuerySchema, {
query: {
case: "loginNameQuery",
value: {
loginName: searchValue,
method: TextQueryMethod.EQUALS,
},
},
});
const EmailQuery = (searchValue: string) =>
create(SearchQuerySchema, {
query: {
case: "emailQuery",
value: {
emailAddress: searchValue,
method: TextQueryMethod.EQUALS,
},
},
});
/**
* this is a dedicated search function to search for users from the loginname page
* it searches users based on the loginName or userName and org suffix combination, and falls back to email and phone if no users are found
* */
export async function searchUsers({
searchValue,
loginSettings,
organizationId,
suffix,
}: SearchUsersCommand) {
const queries: SearchQuery[] = [];
// if a suffix is provided, we search for the userName concatenated with the suffix
if (suffix) {
const searchValueWithSuffix = `${searchValue}@${suffix}`;
const loginNameQuery = LoginNameQuery(searchValueWithSuffix);
queries.push(loginNameQuery);
} else {
const loginNameQuery = LoginNameQuery(searchValue);
queries.push(loginNameQuery);
}
if (organizationId) {
queries.push( queries.push(
create(SearchQuerySchema, { create(SearchQuerySchema, {
query: { query: {
case: "emailQuery", case: "organizationIdQuery",
value: { value: {
emailAddress: email, organizationId,
}, },
}, },
}), }),
); );
} }
return userService.listUsers({ queries: queries }); const loginNameResult = await userService.listUsers({ queries: queries });
if (!loginNameResult || !loginNameResult.details) {
return { error: "An error occurred." };
}
if (loginNameResult.result.length > 1) {
return { error: "Multiple users found" };
}
if (loginNameResult.result.length == 1) {
return loginNameResult;
}
const emailAndPhoneQueries: SearchQuery[] = [];
if (
loginSettings.disableLoginWithEmail &&
loginSettings.disableLoginWithPhone
) {
return { error: "User not found in the system" };
} else if (loginSettings.disableLoginWithEmail && searchValue.length <= 20) {
const phoneQuery = PhoneQuery(searchValue);
emailAndPhoneQueries.push(phoneQuery);
} else if (loginSettings.disableLoginWithPhone) {
const emailQuery = EmailQuery(searchValue);
emailAndPhoneQueries.push(emailQuery);
} else {
const emailAndPhoneOrQueries: SearchQuery[] = [];
const emailQuery = EmailQuery(searchValue);
emailAndPhoneOrQueries.push(emailQuery);
let phoneQuery;
if (searchValue.length <= 20) {
phoneQuery = PhoneQuery(searchValue);
emailAndPhoneOrQueries.push(phoneQuery);
}
emailAndPhoneQueries.push(
create(SearchQuerySchema, {
query: {
case: "orQuery",
value: {
queries: emailAndPhoneOrQueries,
},
},
}),
);
}
if (organizationId) {
queries.push(
create(SearchQuerySchema, {
query: {
case: "organizationIdQuery",
value: {
organizationId,
},
},
}),
);
}
const emailOrPhoneResult = await userService.listUsers({
queries: emailAndPhoneQueries,
});
if (!emailOrPhoneResult || !emailOrPhoneResult.details) {
return { error: "An error occurred." };
}
if (emailOrPhoneResult.result.length > 1) {
return { error: "Multiple users found." };
}
if (emailOrPhoneResult.result.length == 1) {
return loginNameResult;
}
return { error: "User not found in the system" };
} }
export async function getDefaultOrg(): Promise<Organization | null> { export async function getDefaultOrg(): Promise<Organization | null> {