201 lines
5.9 KiB
Markdown
201 lines
5.9 KiB
Markdown
## Task 1: Login Page And App-Level Session
|
|
|
|
**Files:**
|
|
- Modify: `apps/web/src/features/auth/ServerLogin.svelte`
|
|
- Modify: `apps/web/src/app/App.svelte`
|
|
- Test: `apps/web/src/features/auth/ServerLogin.test.ts`
|
|
|
|
**Interfaces:**
|
|
- Produces: `ServerLogin` Svelte component event `login: { apiBase: string; account: string }`
|
|
- Produces: app state fields `apiBase: string`, `currentUser: { account: string } | null`
|
|
- Consumes: `localStorage['apiBase']`
|
|
|
|
- [ ] **Step 1: Write failing login component tests**
|
|
|
|
Create `apps/web/src/features/auth/ServerLogin.test.ts`:
|
|
|
|
```ts
|
|
import '@testing-library/jest-dom/vitest';
|
|
import { fireEvent, render, screen } from '@testing-library/svelte';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import ServerLogin from './ServerLogin.svelte';
|
|
|
|
describe('ServerLogin', () => {
|
|
it('renders server, account, and password fields', () => {
|
|
render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } });
|
|
|
|
expect(screen.getByLabelText('Server IP or domain')).toBeInTheDocument();
|
|
expect(screen.getByLabelText('Email or username')).toBeInTheDocument();
|
|
expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('normalizes server address and emits login details', async () => {
|
|
const onLogin = vi.fn();
|
|
render(ServerLogin, {
|
|
props: {
|
|
apiBase: 'localhost:8080',
|
|
onLogin,
|
|
},
|
|
});
|
|
|
|
await fireEvent.input(screen.getByLabelText('Server IP or domain'), {
|
|
target: { value: '10.0.0.12:8080' },
|
|
});
|
|
await fireEvent.input(screen.getByLabelText('Email or username'), {
|
|
target: { value: 'david@example.com' },
|
|
});
|
|
await fireEvent.input(screen.getByLabelText('Password'), {
|
|
target: { value: 'secret' },
|
|
});
|
|
await fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
|
|
|
|
expect(onLogin).toHaveBeenCalledWith({
|
|
apiBase: 'http://10.0.0.12:8080',
|
|
account: 'david@example.com',
|
|
});
|
|
});
|
|
|
|
it('shows an error when account or password is missing', async () => {
|
|
render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } });
|
|
|
|
await fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
|
|
|
|
expect(screen.getByText('Enter an account and password.')).toBeInTheDocument();
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run the new test to verify it fails**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
Set-Location D:\work\senlinai\agent\apps\web
|
|
npm test -- --run src/features/auth/ServerLogin.test.ts
|
|
```
|
|
|
|
Expected: FAIL because `ServerLogin` does not render account/password fields and does not expose `onLogin`.
|
|
|
|
- [ ] **Step 3: Replace `ServerLogin.svelte` with the complete login page**
|
|
|
|
Implement `apps/web/src/features/auth/ServerLogin.svelte`:
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
export let apiBase: string;
|
|
export let onLogin: (detail: { apiBase: string; account: string }) => void = () => {};
|
|
|
|
let server = apiBase;
|
|
let account = '';
|
|
let password = '';
|
|
let error = '';
|
|
|
|
function normalizeServer(value: string) {
|
|
const trimmed = value.trim();
|
|
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
return trimmed;
|
|
}
|
|
return `http://${trimmed}`;
|
|
}
|
|
|
|
function submitLogin() {
|
|
error = '';
|
|
if (!account.trim() || !password.trim()) {
|
|
error = 'Enter an account and password.';
|
|
return;
|
|
}
|
|
|
|
const normalized = normalizeServer(server);
|
|
server = normalized;
|
|
onLogin({ apiBase: normalized, account: account.trim() });
|
|
}
|
|
</script>
|
|
|
|
<main class="login-page">
|
|
<section class="login-panel" aria-labelledby="login-title">
|
|
<div class="login-brand" aria-hidden="true">SA</div>
|
|
<div class="login-heading">
|
|
<p>Private workbench</p>
|
|
<h1 id="login-title">SenlinAI Workbench</h1>
|
|
</div>
|
|
|
|
<form class="server-login" aria-label="Server login" on:submit|preventDefault={submitLogin}>
|
|
<label for="server-address">Server IP or domain</label>
|
|
<input id="server-address" name="server" bind:value={server} placeholder="http://localhost:8080" />
|
|
|
|
<label for="login-account">Email or username</label>
|
|
<input id="login-account" name="account" bind:value={account} autocomplete="username" />
|
|
|
|
<label for="login-password">Password</label>
|
|
<input
|
|
id="login-password"
|
|
name="password"
|
|
type="password"
|
|
bind:value={password}
|
|
autocomplete="current-password"
|
|
/>
|
|
|
|
{#if error}
|
|
<p class="form-error" aria-live="polite">{error}</p>
|
|
{/if}
|
|
|
|
<button type="submit">Log in</button>
|
|
<p class="login-note">The server address is remembered on this device.</p>
|
|
</form>
|
|
</section>
|
|
</main>
|
|
```
|
|
|
|
- [ ] **Step 4: Update `App.svelte` to switch from login to workbench**
|
|
|
|
Temporarily render a simple logged-in placeholder; the full workbench arrives in Task 3.
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import ServerLogin from '../features/auth/ServerLogin.svelte';
|
|
|
|
let apiBase = localStorage.getItem('apiBase') ?? 'http://localhost:8080';
|
|
let currentUser: { account: string } | null = null;
|
|
|
|
function handleLogin(detail: { apiBase: string; account: string }) {
|
|
apiBase = detail.apiBase;
|
|
localStorage.setItem('apiBase', apiBase);
|
|
currentUser = { account: detail.account };
|
|
}
|
|
</script>
|
|
|
|
{#if currentUser}
|
|
<main class="workbench-placeholder" aria-label="Project workbench">
|
|
<h1>SenlinAI Workbench</h1>
|
|
<p>Signed in as {currentUser.account}</p>
|
|
</main>
|
|
{:else}
|
|
<ServerLogin {apiBase} onLogin={handleLogin} />
|
|
{/if}
|
|
```
|
|
|
|
- [ ] **Step 5: Run tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
Set-Location D:\work\senlinai\agent\apps\web
|
|
npm test -- --run src/features/auth/ServerLogin.test.ts
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
Set-Location D:\work\senlinai\agent
|
|
git add apps\web\src\features\auth\ServerLogin.svelte apps\web\src\features\auth\ServerLogin.test.ts apps\web\src\app\App.svelte
|
|
git commit -m "feat: add full workbench login page"
|
|
```
|
|
|
|
---
|
|
|