feat: rebrand Hemmelig to paste.es for cloudhost.es

- Set Spanish as default language with ephemeral/encrypted privacy focus
- Translate all user-facing strings and legal pages to Spanish
- Replace Norwegian flag with Spanish flag in footer
- Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship
- Rewrite PrivacyPage: zero data collection, ephemeral design emphasis
- Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references
- Update PWA manifest, HTML meta tags, package.json branding
- Rename webhook headers to X-Paste-Event / X-Paste-Signature
- Update API docs title and contact to paste.es / cloudhost.es

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:30:19 +01:00
commit bc9f96cbd4
268 changed files with 45773 additions and 0 deletions

83
tests/e2e/auth.spec.ts Normal file
View File

@@ -0,0 +1,83 @@
import { expect, test } from '@playwright/test';
const TEST_USER = {
email: 'e2etest@hemmelig.local',
username: 'e2etestuser',
password: 'TestPassword123!',
name: 'E2E Test User',
};
test.describe('Authentication', () => {
test('should complete initial setup if needed', async ({ page, request }) => {
// Check if setup is needed
const statusResponse = await request.get('/api/setup/status');
const statusData = await statusResponse.json();
if (statusData.needsSetup) {
await page.goto('/setup');
// Fill in the setup form
await page.getByPlaceholder(/email/i).fill(TEST_USER.email);
await page.getByPlaceholder(/username/i).fill(TEST_USER.username);
await page.getByPlaceholder(/name/i).first().fill(TEST_USER.name);
await page.getByPlaceholder(/create.*password/i).fill(TEST_USER.password);
// Submit setup
await page.getByRole('button', { name: /create|setup|submit/i }).click();
// Should redirect to home or login
await expect(page).toHaveURL(/^\/$|\/login/, { timeout: 10000 });
} else {
// Setup already done, just verify we can access the app
await page.goto('/');
// Either we see the home page or login page
const url = page.url();
expect(url.includes('/') || url.includes('/login')).toBe(true);
}
});
test('should allow user registration when enabled', async ({ page, request }) => {
// Check if registration is allowed
const settingsResponse = await request.get('/api/instance/settings/public');
const settings = await settingsResponse.json();
if (!settings.allowRegistration) {
test.skip();
return;
}
await page.goto('/register');
// Check that registration form is visible
await expect(page.getByPlaceholder(/email/i)).toBeVisible();
await expect(page.getByPlaceholder(/username/i)).toBeVisible();
await expect(page.getByPlaceholder(/create.*password/i)).toBeVisible();
});
test('should show login page', async ({ page }) => {
await page.goto('/login');
// Check that login form is visible
await expect(page.getByPlaceholder(/username/i)).toBeVisible();
await expect(page.getByPlaceholder(/password/i)).toBeVisible();
await expect(page.getByRole('button', { name: /sign in/i })).toBeVisible();
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByPlaceholder(/username/i).fill('nonexistent@test.com');
await page.getByPlaceholder(/password/i).fill('wrongpassword');
await page.getByRole('button', { name: /sign in/i }).click();
// Should stay on login page or show error
await page.waitForTimeout(2000);
const isStillOnLogin = page.url().includes('/login');
const hasError = await page
.getByText(/invalid|error|incorrect|failed/i)
.isVisible()
.catch(() => false);
expect(isStillOnLogin || hasError).toBe(true);
});
});

36
tests/e2e/fixtures.ts Normal file
View File

@@ -0,0 +1,36 @@
import { test as base, expect, Page } from '@playwright/test';
import { TEST_USER } from './global-setup';
async function loginUser(page: Page, email: string, password: string): Promise<boolean> {
await page.goto('/login');
// Fill login form
await page.getByPlaceholder(/username/i).fill(email);
await page.getByPlaceholder(/password/i).fill(password);
await page.getByRole('button', { name: /sign in/i }).click();
// Wait for navigation - could be home, dashboard, or 2FA
try {
await page.waitForURL(/^\/$|\/dashboard|\/verify-2fa/, { timeout: 10000 });
return true;
} catch {
return false;
}
}
// Extend the base test with auth fixture
export const test = base.extend<{ authenticatedPage: Page }>({
authenticatedPage: async ({ page }, use) => {
// Check if authentication is required
await page.goto('/');
if (page.url().includes('/login')) {
// Login with test user (created in global setup)
await loginUser(page, TEST_USER.email, TEST_USER.password);
}
await use(page);
},
});
export { expect, TEST_USER };

71
tests/e2e/global-setup.ts Normal file
View File

@@ -0,0 +1,71 @@
import { FullConfig } from '@playwright/test';
import { execSync } from 'child_process';
import { existsSync, unlinkSync } from 'fs';
const TEST_DB_PATH = './database/hemmelig-test.db';
export const TEST_USER = {
email: 'e2e-test@hemmelig.local',
username: 'e2etestuser',
password: 'E2ETestPassword123!',
name: 'E2E Test User',
};
async function globalSetup(config: FullConfig) {
const baseURL = config.projects[0].use.baseURL || 'http://localhost:5173';
// Delete existing test database to start fresh
if (existsSync(TEST_DB_PATH)) {
unlinkSync(TEST_DB_PATH);
console.log('Deleted existing test database');
}
// Run migrations on test database
console.log('Running migrations on test database...');
execSync('npx prisma migrate deploy', {
env: {
...process.env,
DATABASE_URL: `file:${TEST_DB_PATH}`,
},
stdio: 'inherit',
});
// Wait for server to be ready
let attempts = 0;
while (attempts < 30) {
try {
const response = await fetch(`${baseURL}/api/setup/status`);
if (response.ok) break;
} catch {
// Server not ready yet
}
await new Promise((resolve) => setTimeout(resolve, 1000));
attempts++;
}
// Check if setup is needed (should always be true with fresh DB)
const statusResponse = await fetch(`${baseURL}/api/setup/status`);
const statusData = await statusResponse.json();
if (statusData.needsSetup) {
console.log('Creating test user...');
const setupResponse = await fetch(`${baseURL}/api/setup/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: TEST_USER.email,
password: TEST_USER.password,
username: TEST_USER.username,
name: TEST_USER.name,
}),
});
if (!setupResponse.ok) {
console.error('Failed to complete setup:', await setupResponse.text());
} else {
console.log('Test user created successfully');
}
}
}
export default globalSetup;

View File

@@ -0,0 +1,19 @@
import { existsSync, unlinkSync } from 'fs';
const TEST_DB_PATH = './database/hemmelig-test.db';
async function globalTeardown() {
// Delete test database after tests complete
if (existsSync(TEST_DB_PATH)) {
unlinkSync(TEST_DB_PATH);
console.log('Cleaned up test database');
}
// Also clean up journal file if it exists
const journalPath = `${TEST_DB_PATH}-journal`;
if (existsSync(journalPath)) {
unlinkSync(journalPath);
}
}
export default globalTeardown;

37
tests/e2e/home.spec.ts Normal file
View File

@@ -0,0 +1,37 @@
import { expect, test } from './fixtures';
test.describe('Home Page', () => {
test('should display the secret creation form', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/');
// Check that the editor is present
await expect(authenticatedPage.locator('.ProseMirror')).toBeVisible();
// Check that the create button exists (there are two, use first())
await expect(
authenticatedPage.getByRole('button', { name: /create/i }).first()
).toBeVisible();
});
test('should have working dark/light mode toggle', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/');
// Check initial theme (could be light or dark based on system preference)
const html = authenticatedPage.locator('html');
// Find and click theme toggle button
const themeToggle = authenticatedPage
.locator(
'button[aria-label*="theme"], button:has([class*="Moon"]), button:has([class*="Sun"])'
)
.first();
if (await themeToggle.isVisible()) {
const initialClass = await html.getAttribute('class');
await themeToggle.click();
// Theme should have changed
await expect(html).not.toHaveClass(initialClass || '');
}
});
});

View File

@@ -0,0 +1,33 @@
import { expect, test } from './fixtures';
test.describe('Navigation', () => {
test('should navigate to home page', async ({ authenticatedPage: page }) => {
await page.goto('/');
// Check that we're on the home page with the secret form
await expect(page.locator('.ProseMirror')).toBeVisible();
});
test('should have working logo link', async ({ authenticatedPage: page }) => {
await page.goto('/');
// Check for logo/brand link that goes to home
const logoLink = page.locator('a[href="/"]').first();
await expect(logoLink).toBeVisible();
await logoLink.click();
await expect(page).toHaveURL('/');
});
test('should handle 404 pages gracefully', async ({ authenticatedPage: page }) => {
await page.goto('/nonexistent-page-12345');
// Should show 404 page or similar error
const pageContent = await page.content();
const is404 =
pageContent.toLowerCase().includes('not found') ||
pageContent.toLowerCase().includes('404') ||
page.url().includes('/nonexistent-page');
expect(is404).toBe(true);
});
});

View File

@@ -0,0 +1,124 @@
import { expect, test } from '@playwright/test';
import { TEST_USER } from './global-setup';
const WEAK_PASSWORD_USER = {
email: 'weakpass@hemmelig.local',
username: 'weakpassuser',
password: 'pass',
name: 'Weak Password User',
};
const NEW_STRONG_PASSWORD = 'NewStrongPass123!';
/**
* Helper: sign in via the better-auth API and return session cookies.
*/
async function signInViaAPI(
baseURL: string,
email: string,
password: string
): Promise<{ cookie: string }> {
const res = await fetch(`${baseURL}/api/auth/sign-in/email`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Origin: baseURL,
},
body: JSON.stringify({ email, password }),
redirect: 'manual',
});
const setCookieHeaders = res.headers.getSetCookie();
const cookie = setCookieHeaders
.map((c) => c.split(';')[0])
.filter(Boolean)
.join('; ');
return { cookie };
}
test.describe('Password Change', () => {
test.beforeAll(async ({}, testInfo) => {
const baseURL = testInfo.project.use.baseURL || 'http://localhost:5173';
// Sign in as admin to get session cookies
const { cookie: adminCookie } = await signInViaAPI(
baseURL,
TEST_USER.email,
TEST_USER.password
);
// Use better-auth admin endpoint to create a user with a weak password.
// This bypasses the sign-up hook (which only runs on /sign-up/email),
// and since minPasswordLength is 1, better-auth accepts it.
const createRes = await fetch(`${baseURL}/api/auth/admin/create-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Origin: baseURL,
Cookie: adminCookie,
},
body: JSON.stringify({
email: WEAK_PASSWORD_USER.email,
password: WEAK_PASSWORD_USER.password,
name: WEAK_PASSWORD_USER.name,
role: 'user',
data: {
username: WEAK_PASSWORD_USER.username,
},
}),
});
if (!createRes.ok) {
const body = await createRes.text();
// Ignore if user already exists (e.g. from a previous test run)
if (!body.includes('already') && !body.includes('exists')) {
throw new Error(`Failed to create weak-password user: ${createRes.status} ${body}`);
}
}
});
test('should allow a user with a weak password to change their password and log in with the new one', async ({
page,
}) => {
// Log in as the weak-password user
await page.goto('/login');
await page.getByPlaceholder(/username/i).fill(WEAK_PASSWORD_USER.username);
await page.getByPlaceholder(/password/i).fill(WEAK_PASSWORD_USER.password);
await page.getByRole('button', { name: /sign in/i }).click();
// Wait for login to complete
await page.waitForURL(/^\/$|\/dashboard/, { timeout: 10000 });
// Navigate to account settings
await page.goto('/dashboard/account');
// Click on the Security tab
await page.getByRole('button', { name: /security/i }).click();
// Fill in the password change form
await page.getByPlaceholder(/enter current password/i).fill(WEAK_PASSWORD_USER.password);
await page.getByPlaceholder(/enter new password/i).fill(NEW_STRONG_PASSWORD);
await page.getByPlaceholder(/confirm new password/i).fill(NEW_STRONG_PASSWORD);
// Submit the password change
await page.getByRole('button', { name: /change password/i }).click();
// Verify success message appears
await expect(page.getByText(/password changed successfully/i)).toBeVisible({
timeout: 10000,
});
// Sign out by clearing cookies and navigating to login
await page.context().clearCookies();
await page.goto('/login');
// Log in with the new password
await page.getByPlaceholder(/username/i).fill(WEAK_PASSWORD_USER.username);
await page.getByPlaceholder(/password/i).fill(NEW_STRONG_PASSWORD);
await page.getByRole('button', { name: /sign in/i }).click();
// Verify login succeeds
await page.waitForURL(/^\/$|\/dashboard/, { timeout: 10000 });
});
});

189
tests/e2e/secret.spec.ts Normal file
View File

@@ -0,0 +1,189 @@
import { expect, test } from './fixtures';
test.describe('Secret Creation and Viewing', () => {
test('should create a secret and display the secret URL', async ({
authenticatedPage: page,
}) => {
await page.goto('/');
// Type secret content in the editor
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('This is my test secret message');
// Click create button
await page
.getByRole('button', { name: /create/i })
.first()
.click();
// Wait for success state - check for the success icon or URL field
await expect(page.getByText(/secret.*created/i)).toBeVisible({ timeout: 10000 });
// Verify the secret URL is displayed
const urlInput = page.locator('input[readonly]').first();
await expect(urlInput).toBeVisible();
const secretUrl = await urlInput.inputValue();
expect(secretUrl).toContain('/secret/');
expect(secretUrl).toContain('#decryptionKey=');
});
test('should create a secret with a title', async ({ authenticatedPage: page }) => {
await page.goto('/');
// Type secret content
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Secret with a title');
// Add a title
const titleInput = page.getByPlaceholder(/title/i);
if (await titleInput.isVisible()) {
await titleInput.fill('My Secret Title');
}
// Create the secret
await page
.getByRole('button', { name: /create/i })
.first()
.click();
// Verify success
await expect(page.getByText(/secret.*created/i)).toBeVisible({ timeout: 10000 });
});
test('should create and view a secret end-to-end', async ({ authenticatedPage: page }) => {
await page.goto('/');
const secretText = `Test secret created at ${Date.now()}`;
// Create a secret
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill(secretText);
await page
.getByRole('button', { name: /create/i })
.first()
.click();
// Wait for the URL to appear
await expect(page.getByText(/secret.*created/i)).toBeVisible({ timeout: 10000 });
// Get the secret URL
const urlInput = page.locator('input[readonly]').first();
const secretUrl = await urlInput.inputValue();
// Navigate to the secret URL
await page.goto(secretUrl);
// Click the unlock/view button
const unlockButton = page.getByRole('button', { name: /unlock|view/i });
await expect(unlockButton).toBeVisible({ timeout: 5000 });
await unlockButton.click();
// Verify the secret content is displayed
await expect(page.locator('.ProseMirror')).toContainText(secretText, { timeout: 10000 });
});
test('should create a password-protected secret', async ({ authenticatedPage: page }) => {
await page.goto('/');
const secretText = 'This is a password protected secret';
const password = 'mysecretpassword123';
// Type secret content
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill(secretText);
// Look for password protection toggle in security settings section
// The toggle might be in a section that needs to be scrolled to
const passwordSection = page.locator('text=Password Protection');
if (await passwordSection.isVisible({ timeout: 2000 }).catch(() => false)) {
// Click the toggle switch near "Password Protection"
const toggleSwitch = passwordSection
.locator('xpath=..')
.locator('button[role="switch"], input[type="checkbox"]');
if (await toggleSwitch.isVisible()) {
await toggleSwitch.click();
// Wait for password input to appear
await page.waitForTimeout(500);
// Find the password input that appeared
const passwordInput = page
.locator('input[placeholder*="password" i], input[type="password"]')
.first();
if (await passwordInput.isVisible()) {
await passwordInput.fill(password);
}
}
} else {
// Password protection not available, skip this assertion
test.skip();
return;
}
// Create the secret
await page
.getByRole('button', { name: /create/i })
.first()
.click();
// Wait for success
await expect(page.getByText(/secret.*created/i)).toBeVisible({ timeout: 10000 });
// Get the secret URL
const urlInput = page.locator('input[readonly]').first();
const secretUrl = await urlInput.inputValue();
// Navigate to the secret
await page.goto(secretUrl);
// If password was set, the secret page should prompt for password
// (URL might still have decryptionKey depending on implementation)
const unlockButton = page.getByRole('button', { name: /unlock|view/i });
await expect(unlockButton).toBeVisible({ timeout: 5000 });
await unlockButton.click();
// Verify content is visible (secret should decrypt with the key in URL)
await expect(page.locator('.ProseMirror')).toContainText(secretText, { timeout: 10000 });
});
test('should burn a secret after viewing', async ({ authenticatedPage: page }) => {
await page.goto('/');
// Create a secret
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Secret to be burned');
await page
.getByRole('button', { name: /create/i })
.first()
.click();
// Wait for success and get URL
await expect(page.getByText(/secret.*created/i)).toBeVisible({ timeout: 10000 });
const urlInput = page.locator('input[readonly]').first();
const secretUrl = await urlInput.inputValue();
// View the secret
await page.goto(secretUrl);
await page.getByRole('button', { name: /unlock|view/i }).click();
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10000 });
// Delete the secret
await page.getByRole('button', { name: /delete/i }).click();
// Confirm deletion in modal
const confirmButton = page.getByRole('button', { name: /delete|confirm/i }).last();
await confirmButton.click();
// Should redirect to home
await expect(page).toHaveURL('/');
});
});