mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-09-18 00:49:04 +02:00
feat: restrict signup invite links to a specific email domain
This commit is contained in:
@@ -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()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user