feat(auth): migrate frontend from username to email-only authentication
- Login page: email input replaces username field (type=email, name=email) - Login server action: reads email, uses i18n error for missing credentials - AccountSection: email input (type=email) replaces username text field - New user server action: sends email as required field, drops username - UsersListPanel: displays and searches by email instead of username - Admin edit user page: heading and delete confirm use email - Profile page: fullName fallback uses email, drops @username display - app.d.ts: email required on User, username removed - Generated API types: AppUser.email required, username removed; CreateUserRequest.email required, username removed - i18n: login_label_email, login_error_missing_credentials, admin_col_login updated (de/en/es) - errors.ts: MISSING_CREDENTIALS → login_error_missing_credentials Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,8 +56,9 @@
|
|||||||
"label_optional": "Optional",
|
"label_optional": "Optional",
|
||||||
"label_required_fields": "Pflichtfelder",
|
"label_required_fields": "Pflichtfelder",
|
||||||
"login_heading": "Anmelden",
|
"login_heading": "Anmelden",
|
||||||
"login_label_username": "Benutzername",
|
"login_label_email": "E-Mail-Adresse",
|
||||||
"login_label_password": "Passwort",
|
"login_label_password": "Passwort",
|
||||||
|
"login_error_missing_credentials": "Bitte E-Mail-Adresse und Passwort eingeben.",
|
||||||
"login_btn_submit": "Anmelden",
|
"login_btn_submit": "Anmelden",
|
||||||
"docs_search_placeholder": "Titel, Personen, Tags durchsuchen…",
|
"docs_search_placeholder": "Titel, Personen, Tags durchsuchen…",
|
||||||
"docs_sort_label": "Sortierung",
|
"docs_sort_label": "Sortierung",
|
||||||
@@ -167,7 +168,7 @@
|
|||||||
"admin_tab_groups": "Gruppen",
|
"admin_tab_groups": "Gruppen",
|
||||||
"admin_tab_tags": "Schlagworte",
|
"admin_tab_tags": "Schlagworte",
|
||||||
"admin_section_users": "Benutzerverwaltung",
|
"admin_section_users": "Benutzerverwaltung",
|
||||||
"admin_col_login": "Login",
|
"admin_col_login": "E-Mail",
|
||||||
"admin_col_groups": "Gruppen",
|
"admin_col_groups": "Gruppen",
|
||||||
"admin_col_password": "Passwort",
|
"admin_col_password": "Passwort",
|
||||||
"admin_multiselect_hint": "Strg+Klick für Auswahl",
|
"admin_multiselect_hint": "Strg+Klick für Auswahl",
|
||||||
|
|||||||
@@ -56,8 +56,9 @@
|
|||||||
"label_optional": "Optional",
|
"label_optional": "Optional",
|
||||||
"label_required_fields": "Required fields",
|
"label_required_fields": "Required fields",
|
||||||
"login_heading": "Sign in",
|
"login_heading": "Sign in",
|
||||||
"login_label_username": "Username",
|
"login_label_email": "Email",
|
||||||
"login_label_password": "Password",
|
"login_label_password": "Password",
|
||||||
|
"login_error_missing_credentials": "Please enter your email address and password.",
|
||||||
"login_btn_submit": "Sign in",
|
"login_btn_submit": "Sign in",
|
||||||
"docs_search_placeholder": "Search title, people, tags…",
|
"docs_search_placeholder": "Search title, people, tags…",
|
||||||
"docs_sort_label": "Sort",
|
"docs_sort_label": "Sort",
|
||||||
@@ -167,7 +168,7 @@
|
|||||||
"admin_tab_groups": "Groups",
|
"admin_tab_groups": "Groups",
|
||||||
"admin_tab_tags": "Tags",
|
"admin_tab_tags": "Tags",
|
||||||
"admin_section_users": "User management",
|
"admin_section_users": "User management",
|
||||||
"admin_col_login": "Login",
|
"admin_col_login": "Email",
|
||||||
"admin_col_groups": "Groups",
|
"admin_col_groups": "Groups",
|
||||||
"admin_col_password": "Password",
|
"admin_col_password": "Password",
|
||||||
"admin_multiselect_hint": "Ctrl+Click to select",
|
"admin_multiselect_hint": "Ctrl+Click to select",
|
||||||
|
|||||||
@@ -56,8 +56,9 @@
|
|||||||
"label_optional": "Opcional",
|
"label_optional": "Opcional",
|
||||||
"label_required_fields": "Campos obligatorios",
|
"label_required_fields": "Campos obligatorios",
|
||||||
"login_heading": "Iniciar sesión",
|
"login_heading": "Iniciar sesión",
|
||||||
"login_label_username": "Usuario",
|
"login_label_email": "Correo electrónico",
|
||||||
"login_label_password": "Contraseña",
|
"login_label_password": "Contraseña",
|
||||||
|
"login_error_missing_credentials": "Por favor, introduzca su correo electrónico y contraseña.",
|
||||||
"login_btn_submit": "Iniciar sesión",
|
"login_btn_submit": "Iniciar sesión",
|
||||||
"docs_search_placeholder": "Buscar título, personas, etiquetas…",
|
"docs_search_placeholder": "Buscar título, personas, etiquetas…",
|
||||||
"docs_sort_label": "Ordenar",
|
"docs_sort_label": "Ordenar",
|
||||||
@@ -167,7 +168,7 @@
|
|||||||
"admin_tab_groups": "Grupos",
|
"admin_tab_groups": "Grupos",
|
||||||
"admin_tab_tags": "Etiquetas",
|
"admin_tab_tags": "Etiquetas",
|
||||||
"admin_section_users": "Gestión de usuarios",
|
"admin_section_users": "Gestión de usuarios",
|
||||||
"admin_col_login": "Login",
|
"admin_col_login": "Correo electrónico",
|
||||||
"admin_col_groups": "Grupos",
|
"admin_col_groups": "Grupos",
|
||||||
"admin_col_password": "Contraseña",
|
"admin_col_password": "Contraseña",
|
||||||
"admin_multiselect_hint": "Ctrl+Clic para seleccionar",
|
"admin_multiselect_hint": "Ctrl+Clic para seleccionar",
|
||||||
|
|||||||
3
frontend/src/app.d.ts
vendored
3
frontend/src/app.d.ts
vendored
@@ -5,11 +5,10 @@ declare global {
|
|||||||
// Define the User structure matching your Java Entity
|
// Define the User structure matching your Java Entity
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
birthDate?: string;
|
birthDate?: string;
|
||||||
email?: string;
|
email: string;
|
||||||
contact?: string;
|
contact?: string;
|
||||||
groups: {
|
groups: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export type ErrorCode =
|
|||||||
| 'TAG_NOT_FOUND'
|
| 'TAG_NOT_FOUND'
|
||||||
| 'TAG_MERGE_SELF'
|
| 'TAG_MERGE_SELF'
|
||||||
| 'TAG_MERGE_INVALID_TARGET'
|
| 'TAG_MERGE_INVALID_TARGET'
|
||||||
|
| 'MISSING_CREDENTIALS'
|
||||||
| 'UNAUTHORIZED'
|
| 'UNAUTHORIZED'
|
||||||
| 'FORBIDDEN'
|
| 'FORBIDDEN'
|
||||||
| 'VALIDATION_ERROR'
|
| 'VALIDATION_ERROR'
|
||||||
@@ -118,6 +119,8 @@ export function getErrorMessage(code: ErrorCode | string | undefined): string {
|
|||||||
return m.error_tag_merge_self();
|
return m.error_tag_merge_self();
|
||||||
case 'TAG_MERGE_INVALID_TARGET':
|
case 'TAG_MERGE_INVALID_TARGET':
|
||||||
return m.error_tag_merge_invalid_target();
|
return m.error_tag_merge_invalid_target();
|
||||||
|
case 'MISSING_CREDENTIALS':
|
||||||
|
return m.login_error_missing_credentials();
|
||||||
case 'UNAUTHORIZED':
|
case 'UNAUTHORIZED':
|
||||||
return m.error_unauthorized();
|
return m.error_unauthorized();
|
||||||
case 'FORBIDDEN':
|
case 'FORBIDDEN':
|
||||||
|
|||||||
@@ -1253,13 +1253,12 @@ export interface components {
|
|||||||
AppUser: {
|
AppUser: {
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
|
||||||
password?: string;
|
password?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
/** Format: date */
|
/** Format: date */
|
||||||
birthDate?: string;
|
birthDate?: string;
|
||||||
email?: string;
|
email: string;
|
||||||
contact?: string;
|
contact?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
notifyOnReply: boolean;
|
notifyOnReply: boolean;
|
||||||
@@ -1406,8 +1405,7 @@ export interface components {
|
|||||||
blockIds?: string[];
|
blockIds?: string[];
|
||||||
};
|
};
|
||||||
CreateUserRequest: {
|
CreateUserRequest: {
|
||||||
username?: string;
|
email: string;
|
||||||
email?: string;
|
|
||||||
initialPassword?: string;
|
initialPassword?: string;
|
||||||
groupIds?: string[];
|
groupIds?: string[];
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type Group = {
|
|||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
email: string;
|
||||||
firstName: string | null;
|
firstName: string | null;
|
||||||
lastName: string | null;
|
lastName: string | null;
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
@@ -41,7 +41,7 @@ const filtered = $derived(
|
|||||||
searchQuery.trim() === ''
|
searchQuery.trim() === ''
|
||||||
? users
|
? users
|
||||||
: users.filter((u) =>
|
: users.filter((u) =>
|
||||||
[u.username, u.firstName, u.lastName]
|
[u.email, u.firstName, u.lastName]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.some((v) => v!.toLowerCase().includes(searchQuery.toLowerCase()))
|
.some((v) => v!.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
)
|
)
|
||||||
@@ -128,7 +128,7 @@ const filtered = $derived(
|
|||||||
? 'border-primary bg-primary/10 dark:bg-primary/15'
|
? 'border-primary bg-primary/10 dark:bg-primary/15'
|
||||||
: 'border-transparent hover:bg-muted'}"
|
: 'border-transparent hover:bg-muted'}"
|
||||||
>
|
>
|
||||||
<div class="text-sm font-bold text-ink">{user.username}</div>
|
<div class="text-sm font-bold text-ink">{user.email}</div>
|
||||||
{#if fullName}
|
{#if fullName}
|
||||||
<div class="mt-0.5 text-xs text-ink-3">{fullName}</div>
|
<div class="mt-0.5 text-xs text-ink-3">{fullName}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ let deleteFormEl = $state<HTMLFormElement | null>(null);
|
|||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
const confirmed = await confirm({
|
const confirmed = await confirm({
|
||||||
title: m.admin_user_delete_confirm({ username: data.editUser.username }),
|
title: m.admin_user_delete_confirm({ username: data.editUser.email }),
|
||||||
destructive: true
|
destructive: true
|
||||||
});
|
});
|
||||||
if (confirmed) deleteFormEl!.requestSubmit();
|
if (confirmed) deleteFormEl!.requestSubmit();
|
||||||
@@ -49,7 +49,7 @@ $effect(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<h2 class="flex-1 font-sans text-sm font-bold text-ink">
|
<h2 class="flex-1 font-sans text-sm font-bold text-ink">
|
||||||
{m.admin_user_edit_heading({ username: data.editUser.username })}
|
{m.admin_user_edit_heading({ username: data.editUser.email })}
|
||||||
</h2>
|
</h2>
|
||||||
<form bind:this={deleteFormEl} method="POST" action="?/delete" use:enhance>
|
<form bind:this={deleteFormEl} method="POST" action="?/delete" use:enhance>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const groups = [
|
|||||||
|
|
||||||
const makeUser = (overrides = {}) => ({
|
const makeUser = (overrides = {}) => ({
|
||||||
id: 'u1',
|
id: 'u1',
|
||||||
username: 'max',
|
|
||||||
firstName: 'Max',
|
firstName: 'Max',
|
||||||
lastName: 'Mustermann',
|
lastName: 'Mustermann',
|
||||||
email: 'max@example.com',
|
email: 'max@example.com',
|
||||||
@@ -52,9 +51,11 @@ afterEach(cleanup);
|
|||||||
// ─── Rendering ────────────────────────────────────────────────────────────────
|
// ─── Rendering ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('Admin edit user page – rendering', () => {
|
describe('Admin edit user page – rendering', () => {
|
||||||
it('renders the heading with username', async () => {
|
it('renders the heading with email', async () => {
|
||||||
renderPage({ data: baseData, form: null });
|
renderPage({ data: baseData, form: null });
|
||||||
await expect.element(page.getByText(/Benutzer bearbeiten: max/i)).toBeInTheDocument();
|
await expect
|
||||||
|
.element(page.getByText(/Benutzer bearbeiten: max@example.com/i))
|
||||||
|
.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pre-fills first name from editUser data', async () => {
|
it('pre-fills first name from editUser data', async () => {
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ beforeEach(() => vi.clearAllMocks());
|
|||||||
describe('admin/users layout load', () => {
|
describe('admin/users layout load', () => {
|
||||||
it('returns the users list', async () => {
|
it('returns the users list', async () => {
|
||||||
mockApi([
|
mockApi([
|
||||||
{ id: 'u1', username: 'alice' },
|
{ id: 'u1', email: 'alice@example.com' },
|
||||||
{ id: 'u2', username: 'bob' }
|
{ id: 'u2', email: 'bob@example.com' }
|
||||||
]);
|
]);
|
||||||
const result = await load({ fetch: vi.fn() as unknown as typeof fetch });
|
const result = await load({ fetch: vi.fn() as unknown as typeof fetch });
|
||||||
expect(result.users).toHaveLength(2);
|
expect(result.users).toHaveLength(2);
|
||||||
expect(result.users[0].username).toBe('alice');
|
expect(result.users[0].email).toBe('alice@example.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an empty array when the API returns nothing', async () => {
|
it('returns an empty array when the API returns nothing', async () => {
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ afterEach(cleanup);
|
|||||||
const users = [
|
const users = [
|
||||||
{
|
{
|
||||||
id: 'u1',
|
id: 'u1',
|
||||||
username: 'reader',
|
email: 'reader@example.com',
|
||||||
firstName: 'Lea',
|
firstName: 'Lea',
|
||||||
lastName: 'Leserin',
|
lastName: 'Leserin',
|
||||||
groups: [{ id: 'g1', name: 'Leser', permissions: ['READ_ALL'] }]
|
groups: [{ id: 'g1', name: 'Leser', permissions: ['READ_ALL'] }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'u2',
|
id: 'u2',
|
||||||
username: 'admin',
|
email: 'admin@example.com',
|
||||||
firstName: null,
|
firstName: null,
|
||||||
lastName: null,
|
lastName: null,
|
||||||
groups: [{ id: 'g2', name: 'Admins', permissions: ['ADMIN'] }]
|
groups: [{ id: 'g2', name: 'Admins', permissions: ['ADMIN'] }]
|
||||||
@@ -46,10 +46,10 @@ describe('UsersListPanel — header', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('UsersListPanel — user items', () => {
|
describe('UsersListPanel — user items', () => {
|
||||||
it('renders each username', async () => {
|
it('renders each email', async () => {
|
||||||
render(UsersListPanel, { users });
|
render(UsersListPanel, { users });
|
||||||
await expect.element(page.getByRole('link', { name: /reader/i })).toBeInTheDocument();
|
await expect.element(page.getByText('reader@example.com')).toBeInTheDocument();
|
||||||
await expect.element(page.getByRole('link', { name: /admin/i })).toBeInTheDocument();
|
await expect.element(page.getByText('admin@example.com')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('each user links to /admin/users/[id]', async () => {
|
it('each user links to /admin/users/[id]', async () => {
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ export const actions: Actions = {
|
|||||||
const birthDateRaw = data.get('birthDate') as string;
|
const birthDateRaw = data.get('birthDate') as string;
|
||||||
const result = await api.POST('/api/users', {
|
const result = await api.POST('/api/users', {
|
||||||
body: {
|
body: {
|
||||||
username: data.get('username') as string,
|
email: data.get('email') as string,
|
||||||
initialPassword: data.get('password') as string,
|
initialPassword: data.get('password') as string,
|
||||||
email: (data.get('email') as string) || undefined,
|
|
||||||
groupIds: data.getAll('groupIds') as string[],
|
groupIds: data.getAll('groupIds') as string[],
|
||||||
firstName: (data.get('firstName') as string) || null,
|
firstName: (data.get('firstName') as string) || null,
|
||||||
lastName: (data.get('lastName') as string) || null,
|
lastName: (data.get('lastName') as string) || null,
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import { m } from '$lib/paraglide/messages.js';
|
|||||||
{m.admin_col_login()}
|
{m.admin_col_login()}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="email"
|
||||||
name="username"
|
name="email"
|
||||||
required
|
required
|
||||||
|
autocomplete="email"
|
||||||
class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ describe('Admin new user page – rendering', () => {
|
|||||||
await expect.element(page.getByText(/Neuen Benutzer anlegen/i)).toBeInTheDocument();
|
await expect.element(page.getByText(/Neuen Benutzer anlegen/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the login input', async () => {
|
it('renders the email input', async () => {
|
||||||
render(Page, { data: baseData, form: null });
|
render(Page, { data: baseData, form: null });
|
||||||
await expect.element(page.getByRole('textbox', { name: /Login/i })).toBeInTheDocument();
|
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
|
||||||
|
expect(input).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders group checkboxes for each available group', async () => {
|
it('renders group checkboxes for each available group', async () => {
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ const tick = () => new Promise((r) => setTimeout(r, 0));
|
|||||||
const makeData = (overrides = {}) => ({
|
const makeData = (overrides = {}) => ({
|
||||||
user: {
|
user: {
|
||||||
id: '1',
|
id: '1',
|
||||||
username: 'max',
|
|
||||||
firstName: 'Max',
|
firstName: 'Max',
|
||||||
lastName: 'Müller',
|
lastName: 'Müller',
|
||||||
|
email: 'max@example.com',
|
||||||
groups: [],
|
groups: [],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
createdAt: ''
|
createdAt: ''
|
||||||
@@ -39,7 +39,7 @@ describe('Layout – user avatar button', () => {
|
|||||||
it('shows fallback icon button when names are not set', async () => {
|
it('shows fallback icon button when names are not set', async () => {
|
||||||
render(Layout, {
|
render(Layout, {
|
||||||
data: makeData({
|
data: makeData({
|
||||||
user: { id: '1', username: 'x', groups: [], enabled: true, createdAt: '' }
|
user: { id: '1', email: 'fallback@example.com', groups: [], enabled: true, createdAt: '' }
|
||||||
}),
|
}),
|
||||||
children: emptySnippet
|
children: emptySnippet
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { getErrorMessage } from '$lib/errors';
|
|||||||
export const actions = {
|
export const actions = {
|
||||||
login: async ({ request, cookies, fetch }) => {
|
login: async ({ request, cookies, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const username = data.get('username') as string;
|
const email = data.get('email') as string;
|
||||||
const password = data.get('password') as string;
|
const password = data.get('password') as string;
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!email || !password) {
|
||||||
return fail(400, { error: 'Bitte Benutzername und Passwort eingeben.' });
|
return fail(400, { error: getErrorMessage('MISSING_CREDENTIALS') });
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = btoa(`${username}:${password}`);
|
const credentials = btoa(`${email}:${password}`);
|
||||||
const authHeader = `Basic ${credentials}`;
|
const authHeader = `Basic ${credentials}`;
|
||||||
|
|
||||||
// Raw fetch is intentional here: we need to pass an explicit Authorization
|
// Raw fetch is intentional here: we need to pass an explicit Authorization
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ let { form }: { form?: { error?: string; success?: boolean } } = $props();
|
|||||||
<form method="POST" action="?/login" class="space-y-5">
|
<form method="POST" action="?/login" class="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
for="username"
|
for="email"
|
||||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||||
>{m.login_label_username()}</label
|
>{m.login_label_email()}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="email"
|
||||||
name="username"
|
name="email"
|
||||||
id="username"
|
id="email"
|
||||||
required
|
required
|
||||||
autocomplete="username"
|
autocomplete="email"
|
||||||
class="block w-full border border-line px-3 py-2.5 font-serif text-sm text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
class="block w-full border border-line px-3 py-2.5 font-serif text-sm text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ describe('Login page – rendering', () => {
|
|||||||
await expect.element(page.getByRole('button', { name: 'Anmelden' })).toBeInTheDocument();
|
await expect.element(page.getByRole('button', { name: 'Anmelden' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the username input', async () => {
|
it('renders the email input', async () => {
|
||||||
render(LoginPage, {});
|
render(LoginPage, {});
|
||||||
await tick();
|
await tick();
|
||||||
const input = document.querySelector<HTMLInputElement>('input[name="username"]');
|
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
|
||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -35,10 +35,10 @@ describe('Login page – rendering', () => {
|
|||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('username field is required', async () => {
|
it('email field is required', async () => {
|
||||||
render(LoginPage, {});
|
render(LoginPage, {});
|
||||||
await tick();
|
await tick();
|
||||||
const input = document.querySelector<HTMLInputElement>('input[name="username"]');
|
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
|
||||||
expect(input?.required).toBe(true);
|
expect(input?.required).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,6 +49,13 @@ describe('Login page – rendering', () => {
|
|||||||
expect(input?.required).toBe(true);
|
expect(input?.required).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('email field has type="email"', async () => {
|
||||||
|
render(LoginPage, {});
|
||||||
|
await tick();
|
||||||
|
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
|
||||||
|
expect(input?.type).toBe('email');
|
||||||
|
});
|
||||||
|
|
||||||
it('password field has type="password"', async () => {
|
it('password field has type="password"', async () => {
|
||||||
render(LoginPage, {});
|
render(LoginPage, {});
|
||||||
await tick();
|
await tick();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ let { data } = $props();
|
|||||||
const fullName = $derived.by(() => {
|
const fullName = $derived.by(() => {
|
||||||
const first = data.profileUser.firstName;
|
const first = data.profileUser.firstName;
|
||||||
const last = data.profileUser.lastName;
|
const last = data.profileUser.lastName;
|
||||||
return first || last ? [first, last].filter(Boolean).join(' ') : data.profileUser.username;
|
return first || last ? [first, last].filter(Boolean).join(' ') : data.profileUser.email;
|
||||||
});
|
});
|
||||||
|
|
||||||
const initials = $derived.by(() => {
|
const initials = $derived.by(() => {
|
||||||
@@ -70,12 +70,9 @@ const initials = $derived.by(() => {
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Name and username -->
|
<!-- Name -->
|
||||||
<div class="mb-5 text-center">
|
<div class="mb-5 text-center">
|
||||||
<h2 class="font-serif text-xl font-bold text-ink">{fullName}</h2>
|
<h2 class="font-serif text-xl font-bold text-ink">{fullName}</h2>
|
||||||
{#if data.profileUser.firstName || data.profileUser.lastName}
|
|
||||||
<p class="mt-0.5 font-sans text-sm text-ink-3">@{data.profileUser.username}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Field rows -->
|
<!-- Field rows -->
|
||||||
|
|||||||
Reference in New Issue
Block a user