diff --git a/acceptance/docker-compose.yaml b/acceptance/docker-compose.yaml index de6990387d..240d91553a 100644 --- a/acceptance/docker-compose.yaml +++ b/acceptance/docker-compose.yaml @@ -1,7 +1,7 @@ services: zitadel: 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' ports: - "8080:8080" @@ -22,7 +22,7 @@ services: - 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 healthcheck: - test: [ "CMD-SHELL", "pg_isready" ] + test: ["CMD-SHELL", "pg_isready"] interval: "10s" timeout: "30s" retries: 5 diff --git a/acceptance/setup.sh b/acceptance/setup.sh index e6277854a7..596c985d78 100755 --- a/acceptance/setup.sh +++ b/acceptance/setup.sh @@ -40,6 +40,7 @@ echo "ZITADEL_API_URL=${ZITADEL_API_URL} ZITADEL_SERVICE_USER_ID=${ZITADEL_SERVICE_USER_ID} ZITADEL_SERVICE_USER_TOKEN=${PAT} SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL} +EMAIL_VERIFICATION=true DEBUG=true"| tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}" cat ${WRITE_ENVIRONMENT_FILE} diff --git a/acceptance/tests/email-verify-screen.ts b/acceptance/tests/email-verify-screen.ts new file mode 100644 index 0000000000..b077ecb424 --- /dev/null +++ b/acceptance/tests/email-verify-screen.ts @@ -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"); +} diff --git a/acceptance/tests/email-verify.spec.ts b/acceptance/tests/email-verify.spec.ts new file mode 100644 index 0000000000..d95c1f691d --- /dev/null +++ b/acceptance/tests/email-verify.spec.ts @@ -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); +}); diff --git a/acceptance/tests/email-verify.ts b/acceptance/tests/email-verify.ts new file mode 100644 index 0000000000..dd7f74b29a --- /dev/null +++ b/acceptance/tests/email-verify.ts @@ -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(); +} diff --git a/acceptance/tests/password-screen.ts b/acceptance/tests/password-screen.ts index 57334a07d2..f207b17035 100644 --- a/acceptance/tests/password-screen.ts +++ b/acceptance/tests/password-screen.ts @@ -4,6 +4,10 @@ import { getCodeFromSink } from "./sink"; const codeField = "code-text-input"; const passwordField = "password-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 symbolCheck = "symbol-check"; const numberCheck = "number-check"; @@ -15,8 +19,8 @@ const matchText = "Matches"; const noMatchText = "Doesn't match"; export async function changePasswordScreen(page: Page, password1: string, password2: string) { - await page.getByTestId(passwordField).pressSequentially(password1); - await page.getByTestId(passwordConfirmField).pressSequentially(password2); + await page.getByTestId(passwordChangeField).pressSequentially(password1); + await page.getByTestId(passwordChangeConfirmField).pressSequentially(password2); } export async function passwordScreen(page: Page, password: string) { @@ -39,9 +43,21 @@ export async function changePasswordScreenExpect( lowercase: boolean, equals: boolean, ) { - await expect(page.getByTestId(passwordField)).toHaveValue(password1); - await expect(page.getByTestId(passwordConfirmField)).toHaveValue(password2); + await expect(page.getByTestId(passwordChangeField)).toHaveValue(password1); + 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, symbolCheck, symbol); await checkContent(page, numberCheck, number); @@ -63,8 +79,8 @@ export async function resetPasswordScreen(page: Page, username: string, password await page.waitForTimeout(3000); const c = await getCodeFromSink(username); await page.getByTestId(codeField).pressSequentially(c); - await page.getByTestId(passwordField).pressSequentially(password1); - await page.getByTestId(passwordConfirmField).pressSequentially(password2); + await page.getByTestId(passwordSetField).pressSequentially(password1); + await page.getByTestId(passwordSetConfirmField).pressSequentially(password2); } export async function resetPasswordScreenExpect( @@ -78,5 +94,8 @@ export async function resetPasswordScreenExpect( lowercase: 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); } diff --git a/acceptance/tests/password.ts b/acceptance/tests/password.ts index b3d31fcaee..1dc304cc84 100644 --- a/acceptance/tests/password.ts +++ b/acceptance/tests/password.ts @@ -8,8 +8,7 @@ export async function startChangePassword(page: Page, loginname: string) { await page.goto("/password/change?" + new URLSearchParams({ loginName: loginname })); } -export async function changePassword(page: Page, loginname: string, password: string) { - await startChangePassword(page, loginname); +export async function changePassword(page: Page, password: string) { await changePasswordScreen(page, password, password); await page.getByTestId(passwordSubmitButton).click(); } diff --git a/acceptance/tests/register.ts b/acceptance/tests/register.ts index 693bdfbc0d..19cb0f04fd 100644 --- a/acceptance/tests/register.ts +++ b/acceptance/tests/register.ts @@ -1,6 +1,8 @@ import { Page } from "@playwright/test"; +import { emailVerify } from "./email-verify"; import { passkeyRegister } from "./passkey"; import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen"; +import { getCodeFromSink } from "./sink"; export async function registerWithPassword( page: Page, @@ -15,6 +17,9 @@ export async function registerWithPassword( await page.getByTestId("submit-button").click(); await registerPasswordScreen(page, password1, password2); 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 { @@ -23,7 +28,15 @@ export async function registerWithPasskey(page: Page, firstname: string, lastnam await page.getByTestId("submit-button").click(); // 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); } diff --git a/acceptance/tests/user.ts b/acceptance/tests/user.ts index 3daefdff08..68a8eecd2b 100644 --- a/acceptance/tests/user.ts +++ b/acceptance/tests/user.ts @@ -4,11 +4,14 @@ import { activateOTP, addTOTP, addUser, getUserByUsername, removeUser } from "./ export interface userProps { email: string; + isEmailVerified?: boolean; firstName: string; lastName: string; organization: string; password: string; + passwordChangeRequired?: boolean; phone: string; + isPhoneVerified?: boolean; } class User { @@ -77,11 +80,14 @@ export enum OtpType { export interface otpUserProps { email: string; + isEmailVerified?: boolean; firstName: string; lastName: string; organization: string; password: string; + passwordChangeRequired?: boolean; phone: string; + isPhoneVerified?: boolean; type: OtpType; } @@ -96,6 +102,9 @@ export class PasswordUserWithOTP extends User { organization: props.organization, password: props.password, phone: props.phone, + isEmailVerified: props.isEmailVerified, + isPhoneVerified: props.isPhoneVerified, + passwordChangeRequired: props.passwordChangeRequired, }); this.type = props.type; } @@ -133,6 +142,8 @@ export interface passkeyUserProps { lastName: string; organization: string; phone: string; + isEmailVerified?: boolean; + isPhoneVerified?: boolean; } export class PasskeyUser extends User { @@ -146,6 +157,8 @@ export class PasskeyUser extends User { organization: props.organization, password: "", phone: props.phone, + isEmailVerified: props.isEmailVerified, + isPhoneVerified: props.isPhoneVerified, }); } diff --git a/acceptance/tests/username-passkey.spec.ts b/acceptance/tests/username-passkey.spec.ts index e73de3547f..54b1bf0a29 100644 --- a/acceptance/tests/username-passkey.spec.ts +++ b/acceptance/tests/username-passkey.spec.ts @@ -12,10 +12,12 @@ const test = base.extend<{ user: PasskeyUser }>({ user: async ({ page }, use) => { const user = new PasskeyUser({ email: faker.internet.email(), + isEmailVerified: true, firstName: faker.person.firstName(), lastName: faker.person.lastName(), organization: "", phone: faker.phone.number(), + isPhoneVerified: false, }); await user.ensure(page); await use(user); diff --git a/acceptance/tests/username-password-change-required.spec.ts b/acceptance/tests/username-password-change-required.spec.ts new file mode 100644 index 0000000000..50177d95e9 --- /dev/null +++ b/acceptance/tests/username-password-change-required.spec.ts @@ -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()); +}); diff --git a/acceptance/tests/username-password-changed.spec.ts b/acceptance/tests/username-password-changed.spec.ts index c185e51ec9..c43ec13797 100644 --- a/acceptance/tests/username-password-changed.spec.ts +++ b/acceptance/tests/username-password-changed.spec.ts @@ -2,8 +2,8 @@ import { faker } from "@faker-js/faker"; import { test as base } from "@playwright/test"; import dotenv from "dotenv"; import path from "path"; -import { loginWithPassword } from "./login"; -import { startChangePassword } from "./password"; +import { loginScreenExpect, loginWithPassword } from "./login"; +import { changePassword, startChangePassword } from "./password"; import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen"; import { PasswordUser } from "./user"; @@ -14,11 +14,14 @@ 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: false, }); await user.ensure(page); await use(user); @@ -27,20 +30,18 @@ const test = base.extend<{ user: PasswordUser }>({ }); 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 - await page.waitForTimeout(2000); + // wait for projection of token + await page.waitForTimeout(2000); - await changePassword(page, user.getUsername(), changedPw); - await loginScreenExpect(page, user.getFullName()); + await startChangePassword(page, user.getUsername()); + await changePassword(page, changedPw); + await loginScreenExpect(page, user.getFullName()); - await loginWithPassword(page, user.getUsername(), changedPw); - await loginScreenExpect(page, user.getFullName()); - */ + await loginWithPassword(page, user.getUsername(), changedPw); + await loginScreenExpect(page, user.getFullName()); }); test("password change not with desired complexity", async ({ user, page }) => { diff --git a/acceptance/tests/username-password-otp_email.spec.ts b/acceptance/tests/username-password-otp_email.spec.ts index daa2e0a429..d06cc87834 100644 --- a/acceptance/tests/username-password-otp_email.spec.ts +++ b/acceptance/tests/username-password-otp_email.spec.ts @@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ user: async ({ page }, use) => { const user = new PasswordUserWithOTP({ email: faker.internet.email(), + isEmailVerified: true, firstName: faker.person.firstName(), lastName: faker.person.lastName(), organization: "", phone: faker.phone.number(), + isPhoneVerified: false, password: "Password1!", + passwordChangeRequired: false, type: OtpType.email, }); diff --git a/acceptance/tests/username-password-otp_sms.spec.ts b/acceptance/tests/username-password-otp_sms.spec.ts index 8fb91a66c7..ac69b25f08 100644 --- a/acceptance/tests/username-password-otp_sms.spec.ts +++ b/acceptance/tests/username-password-otp_sms.spec.ts @@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ user: async ({ page }, use) => { const user = new PasswordUserWithOTP({ email: faker.internet.email(), + isEmailVerified: true, firstName: faker.person.firstName(), lastName: faker.person.lastName(), organization: "", phone: faker.phone.number({ style: "international" }), + isPhoneVerified: true, password: "Password1!", + passwordChangeRequired: false, type: OtpType.sms, }); diff --git a/acceptance/tests/username-password-set.spec.ts b/acceptance/tests/username-password-set.spec.ts index 30d442df50..dcdfbb1c52 100644 --- a/acceptance/tests/username-password-set.spec.ts +++ b/acceptance/tests/username-password-set.spec.ts @@ -15,11 +15,14 @@ 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: false, }); await user.ensure(page); await use(user); @@ -28,8 +31,6 @@ const test = base.extend<{ user: PasswordUser }>({ }); test("username and password set login", async ({ user, page }) => { - // commented, fix in https://github.com/zitadel/zitadel/pull/8807 - const changedPw = "ChangedPw1!"; await startLogin(page); await loginname(page, user.getUsername()); diff --git a/acceptance/tests/username-password-totp.spec.ts b/acceptance/tests/username-password-totp.spec.ts index 4b6e678931..e897cd7748 100644 --- a/acceptance/tests/username-password-totp.spec.ts +++ b/acceptance/tests/username-password-totp.spec.ts @@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({ user: async ({ page }, use) => { const user = new PasswordUserWithTOTP({ email: faker.internet.email(), + isEmailVerified: true, firstName: faker.person.firstName(), lastName: faker.person.lastName(), organization: "", phone: faker.phone.number({ style: "international" }), + isPhoneVerified: true, password: "Password1!", + passwordChangeRequired: false, }); await user.ensure(page); diff --git a/acceptance/tests/username-password.spec.ts b/acceptance/tests/username-password.spec.ts index fcb6aad037..209c415511 100644 --- a/acceptance/tests/username-password.spec.ts +++ b/acceptance/tests/username-password.spec.ts @@ -16,11 +16,14 @@ 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: false, }); await user.ensure(page); await use(user); diff --git a/acceptance/tests/zitadel.ts b/acceptance/tests/zitadel.ts index 587723f10d..1923b63dce 100644 --- a/acceptance/tests/zitadel.ts +++ b/acceptance/tests/zitadel.ts @@ -19,13 +19,20 @@ export async function addUser(props: userProps) { isVerified: true, }, phone: { - phone: props.phone!, + phone: props.phone, isVerified: true, }, 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); } diff --git a/apps/login/readme.md b/apps/login/readme.md index a411b9963f..fd6ba6f48c 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -395,6 +395,5 @@ Timebased features like the multifactor init prompt or password expiry, are not - Password Expiry Settings - Login Settings: multifactor init prompt - 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. diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index 44601e1845..5400f64b9d 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -20,6 +20,7 @@ export default async function Page(props: { const loginName = searchParams?.loginName; const authRequestId = searchParams?.authRequestId; const organization = searchParams?.organization; + const suffix = searchParams?.suffix; const submit: boolean = searchParams?.submit === "true"; let defaultOrganization; @@ -34,6 +35,8 @@ export default async function Page(props: { organization ?? defaultOrganization, ); + const contextLoginSettings = await getLoginSettings(organization); + const identityProviders = await getActiveIdentityProviders( organization ?? defaultOrganization, ).then((resp) => { @@ -54,6 +57,8 @@ 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={contextLoginSettings} + suffix={suffix} submit={submit} allowRegister={!!loginSettings?.allowRegister} > diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index e96326518f..f5741bb39c 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -300,6 +300,7 @@ export async function GET(request: NextRequest) { const { authRequest } = await getAuthRequest({ authRequestId }); let organization = ""; + let suffix = ""; let idpId = ""; if (authRequest?.scope) { @@ -326,6 +327,7 @@ export async function GET(request: NextRequest) { const orgs = await getOrgsByDomain(orgDomain); if (orgs.result && orgs.result.length === 1) { organization = orgs.result[0].id ?? ""; + suffix = orgDomain; } } } @@ -448,6 +450,9 @@ export async function GET(request: NextRequest) { if (organization) { loginNameUrl.searchParams.set("organization", organization); } + if (suffix) { + loginNameUrl.searchParams.set("suffix", suffix); + } return NextResponse.redirect(loginNameUrl); } else if (authRequest.prompt.includes(Prompt.NONE)) { /** diff --git a/apps/login/src/components/change-password-form.tsx b/apps/login/src/components/change-password-form.tsx index f8c22029f7..c581a40b8d 100644 --- a/apps/login/src/components/change-password-form.tsx +++ b/apps/login/src/components/change-password-form.tsx @@ -161,7 +161,7 @@ export function ChangePasswordForm({ })} label="New Password" error={errors.password?.message as string} - data-testid="password-text-input" + data-testid="password-change-text-input" />
@@ -174,7 +174,7 @@ export function ChangePasswordForm({ })} label="Confirm Password" error={errors.confirmPassword?.message as string} - data-testid="password-confirm-text-input" + data-testid="password-change-confirm-text-input" />
diff --git a/apps/login/src/components/input.tsx b/apps/login/src/components/input.tsx index 0cb9e58467..de19156b91 100644 --- a/apps/login/src/components/input.tsx +++ b/apps/login/src/components/input.tsx @@ -15,6 +15,7 @@ export type TextInputProps = DetailedHTMLProps< HTMLInputElement > & { label: string; + suffix?: string; placeholder?: string; defaultValue?: string; error?: string | ReactNode; @@ -45,6 +46,7 @@ export const TextInput = forwardRef( label, placeholder, defaultValue, + suffix, required = false, error, disabled, @@ -56,7 +58,7 @@ export const TextInput = forwardRef( ref, ) => { return ( -