feat: restrict signup invite links to a specific email domain

This commit is contained in:
ItalyPaleAle
2026-07-16 06:44:59 -07:00
parent 525946e94e
commit fdfaac8a35
25 changed files with 442 additions and 35 deletions
+9
View File
@@ -192,5 +192,14 @@ export const signupTokens = {
usageLimit: 1,
usageCount: 1,
createdAt: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString()
},
domainRestricted: {
id: 'b2c3d4e5-f6a7-4890-bcde-f01234567891',
token: 'DOMAIN1234567890',
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
usageLimit: 5,
usageCount: 0,
emailDomain: 'example.com',
createdAt: new Date().toISOString()
}
};
+17 -4
View File
@@ -271,7 +271,8 @@
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"token": "VALID1234567890A",
"usage_count": 0,
"usage_limit": 1
"usage_limit": 1,
"email_domain": null
},
{
"created_at": "2025-11-25T12:39:02Z",
@@ -279,7 +280,8 @@
"id": "dc3c9c96-714e-48eb-926e-2d7c7858e6cf",
"token": "PARTIAL567890ABC",
"usage_count": 2,
"usage_limit": 5
"usage_limit": 5,
"email_domain": null
},
{
"created_at": "2025-11-25T12:39:02Z",
@@ -287,7 +289,8 @@
"id": "44de1863-ffa5-4db1-9507-4887cd7a1e3f",
"token": "EXPIRED34567890B",
"usage_count": 1,
"usage_limit": 3
"usage_limit": 3,
"email_domain": null
},
{
"created_at": "2025-11-25T12:39:02Z",
@@ -295,7 +298,17 @@
"id": "f1b1678b-7720-4d8b-8f91-1dbff1e2d02b",
"token": "FULLYUSED567890C",
"usage_count": 1,
"usage_limit": 1
"usage_limit": 1,
"email_domain": null
},
{
"created_at": "2025-11-25T12:39:02Z",
"expires_at": "2025-11-26T12:39:02Z",
"id": "b2c3d4e5-f6a7-4890-bcde-f01234567891",
"token": "DOMAIN1234567890",
"usage_count": 0,
"usage_limit": 5,
"email_domain": "example.com"
}
],
"signup_tokens_user_groups": [
+77
View File
@@ -62,6 +62,20 @@ test.describe('Signup Token Creation', () => {
await expect(row.getByRole('cell', { name: userGroups.developers.name })).toBeVisible();
await expect(row.getByRole('cell', { name: userGroups.designers.name })).toBeVisible();
});
test('Create signup token with email domain restriction', async ({ page }) => {
await page.goto('/settings/admin/users');
await page.getByRole('button', { name: 'Create options' }).click();
await page.getByRole('menuitem', { name: 'Create Signup Token' }).click();
await page.getByLabel('Email Domain').fill('@example.com');
await page.getByRole('button', { name: 'Create', exact: true }).click();
// The success view shows the configured domain restriction
await expect(page.getByText('@example.com').first()).toBeVisible();
});
});
test.describe('Initial User Signup', () => {
@@ -241,6 +255,69 @@ test.describe('User Signup', () => {
await expect(page.getByText('Passkey missing')).toBeVisible();
});
test('Signup with token - email domain hint and placeholder are shown', async ({ page }) => {
await setSignupMode(page, 'Signup with token');
await page.goto(`/st/${signupTokens.domainRestricted.token}`);
await expect(
page.getByText('Must be an email address with the domain @example.com')
).toBeVisible();
await expect(page.getByPlaceholder('you@example.com')).toBeVisible();
});
test('Signup with token - wrong email domain shows validation error', async ({ page }) => {
await setSignupMode(page, 'Signup with token');
await page.goto(`/st/${signupTokens.domainRestricted.token}`);
await page.getByLabel('First name').fill('Domain');
await page.getByLabel('Last name').fill('User');
await page.getByLabel('Username').fill('domainuser');
await page.getByLabel('Email').fill('domain.user@wrong.com');
await page.getByRole('button', { name: 'Sign Up' }).click();
await expect(
page.getByText('The email address must use the domain @example.com')
).toBeVisible();
});
test('Signup with token - matching email domain succeeds', async ({ page }) => {
await setSignupMode(page, 'Signup with token');
await page.goto(`/st/${signupTokens.domainRestricted.token}`);
await page.getByLabel('First name').fill('Domain');
await page.getByLabel('Last name').fill('User');
await page.getByLabel('Username').fill('domainuser');
await page.getByLabel('Email').fill('domain.user@example.com');
await page.getByRole('button', { name: 'Sign Up' }).click();
await page.waitForURL('/signup/add-passkey');
await expect(page.getByText('Set up your passkey')).toBeVisible();
});
test('Signup with token - server enforces email domain restriction', async ({ page }) => {
await setSignupMode(page, 'Signup with token');
// Bypass the client-side validation to verify the domain is enforced server-side
const res = await page.request.post('/api/signup', {
data: {
username: 'apidomainuser',
email: 'api.user@wrong.com',
firstName: 'Api',
lastName: 'User',
token: signupTokens.domainRestricted.token
}
});
expect(res.status()).toBe(400);
const body = await res.json();
expect(body.error).toContain('example.com');
});
test('Token usage limit is enforced', async ({ page }) => {
await setSignupMode(page, 'Signup with token');