Files
zooqueenandHanzo Dev ecee5e40aa Sign in through Hanzo IAM only, and make accounts for invited people only
The IAM callback at /auth/callback reads IAM's userinfo answer and refuses
an address IAM has not verified. An account is found by its linked IAM
identity alone (provider hanzo, the IAM subject); an existing account that
only shares the address is refused, never merged. A first sign-in makes an
account only for an admin of the org the application serves or for someone
an organisation invited at that verified address. Every refusal returns to
/signin with a sentence saying what to do.

Accepting an organisation invitation joins the signed-in person, and only
when their verified address is the invited one; opening the link no longer
adds whoever holds the address.

The local email/password sign-up, sign-in, reset, verify-email and update
routes, pages, jobs and mail are gone, as are passkey sign-in, the Google,
Microsoft and generic OIDC providers, the per-organisation SSO portal and
its account-link flow. Passkeys and two-factor stay for confirming a
signature. The e2e suite starts sessions the way a finished IAM sign-in
leaves them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 01:52:24 -07:00

88 lines
2.8 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@hanzo/esign-lib/constants/app';
import { seedTeamEmailVerification } from '@hanzo/esign-prisma/seed/teams';
import { seedUser } from '@hanzo/esign-prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
import { openDropdownMenu } from '../fixtures/generic';
test('[TEAMS]: send team email request', async ({ page }) => {
const { user, team } = await seedUser();
await apiSignin({
page,
email: user.email,
redirectPath: `/t/${team.url}/settings`,
});
await page.getByRole('button', { name: 'Add email' }).click();
await page.getByPlaceholder('eg. Legal').click();
await page.getByPlaceholder('eg. Legal').fill('[email protected]');
await page.getByPlaceholder('[email protected]').click();
await page.getByPlaceholder('[email protected]').fill('[email protected]');
await page.getByRole('button', { name: 'Add' }).click();
await expect(
page
.getByRole('status')
.filter({ hasText: 'We have sent a confirmation email for verification.' })
.first(),
).toBeVisible();
});
test('[TEAMS]: accept team email request', async ({ page }) => {
const { user, team } = await seedUser();
const teamEmailVerification = await seedTeamEmailVerification({
email: `team-email-verification--${team.url}@test.esign.hanzo.ai`,
teamId: team.id,
});
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/team/verify/email/${teamEmailVerification.token}`);
await expect(page.getByRole('heading')).toContainText('Team email verified!');
});
test('[TEAMS]: delete team email', async ({ page }) => {
const { user, team } = await seedUser({
setTeamEmailAsOwner: true,
});
await apiSignin({
page,
email: user.email,
redirectPath: `/t/${team.url}/settings`,
});
const settingsBtn = page
.locator('section div')
.filter({ hasText: 'Team email' })
.getByRole('button');
await openDropdownMenu(page, settingsBtn);
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
await page.getByRole('menuitem', { name: 'Remove' }).click();
await page.getByRole('button', { name: 'Remove' }).click();
await expect(page.getByText('Team email has been removed').first()).toBeVisible();
});
test('[TEAMS]: team email owner removes access', async ({ page }) => {
const teamEmailOwner = await seedUser();
const { user: secondUser } = await seedUser({
teamEmail: teamEmailOwner.user.email,
});
await apiSignin({
page,
email: teamEmailOwner.user.email,
redirectPath: `/settings/profile`,
});
await page.getByRole('button', { name: 'Revoke access' }).click();
await page.getByRole('button', { name: 'Revoke' }).click();
await expect(page.getByText('You have successfully revoked').first()).toBeVisible();
});