Files
agent/apps/web/src/features/auth/ServerLogin.svelte

156 lines
5.4 KiB
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 = '';
let connectionStatus = 'Not connected. Enter a server address to continue.';
function normalizeServer(value: string): { apiBase?: string; error?: string } {
if (!value.trim()) {
return { error: 'Enter a server address.' };
}
if (hasWhitespace(value) || value.includes('\\') || hasMalformedHttpProtocol(value) || hasUnsupportedProtocol(value)) {
return { error: 'Enter a valid HTTP or HTTPS server address.' };
}
const candidate = /^https?:\/\//i.test(value) ? value : `http://${value}`;
if (!/^https?:\/\/(?![\\/])/i.test(candidate) || hasCredentials(candidate)) {
return { error: 'Enter a valid HTTP or HTTPS server address.' };
}
try {
const parsed = new URL(candidate);
if (
!parsed.hostname ||
parsed.username ||
parsed.password ||
!['http:', 'https:'].includes(parsed.protocol) ||
!isValidHostname(parsed.hostname)
) {
return { error: 'Enter a valid HTTP or HTTPS server address.' };
}
return { apiBase: candidate };
} catch {
return { error: 'Enter a valid HTTP or HTTPS server address.' };
}
}
function hasWhitespace(value: string): boolean {
if (/\s/u.test(value)) return true;
try {
return /\s/u.test(decodeURIComponent(value));
} catch {
return true;
}
}
function hasUnsupportedProtocol(value: string): boolean {
const explicitProtocol = value.match(/^([a-z][a-z\d+.-]*):\/\//i);
if (explicitProtocol) {
return !['http', 'https'].includes(explicitProtocol[1].toLowerCase());
}
return /^[a-z][a-z\d+.-]*:(?!\d)/i.test(value);
}
function hasMalformedHttpProtocol(value: string): boolean {
return /^https?:\/(?!\/)/i.test(value) || /^https?\/{2}(?!\/)/i.test(value);
}
function hasCredentials(value: string): boolean {
const authority = value.replace(/^https?:\/\//i, '').split(/[/?#]/, 1)[0];
return authority.includes('@');
}
function isValidHostname(hostname: string): boolean {
if (hostname === 'localhost') return true;
if (/^\[[\da-f:.]+\]$/i.test(hostname)) return true;
if (/^\d+(?:\.\d+){3}$/.test(hostname)) {
return hostname.split('.').every((segment) => Number(segment) <= 255);
}
return hostname.split('.').every((label) => /^[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?$/i.test(label));
}
function submitLogin() {
error = '';
const normalized = normalizeServer(server);
if (normalized.error) {
error = normalized.error;
connectionStatus = 'Check the server address before signing in.';
return;
}
if (!account.trim() || !password.trim()) {
error = 'Enter an account and password.';
return;
}
server = normalized.apiBase ?? server;
connectionStatus = 'Connection settings are ready.';
onLogin({ apiBase: server, account: account.trim() });
}
</script>
<main class="login-page">
<section class="login-panel" aria-labelledby="login-title">
<div class="login-brand-panel">
<div class="login-brand" aria-hidden="true">S</div>
<h1>SenlinAI</h1>
<p>私有部署 · 安全可控的智能协作平台</p>
<small>知识沉淀 · 团队协作 · AI 助理</small>
<nav class="login-links" aria-label="Login resources">
<a href="/" on:click|preventDefault>隐私政策</a>
<a href="/" on:click|preventDefault>服务协议</a>
<a href="/" on:click|preventDefault>关于 SenlinAI</a>
</nav>
</div>
<form class="server-login" aria-label="Server login" on:submit|preventDefault={submitLogin}>
<div class="login-heading">
<p>登录到您的 SenlinAI</p>
<h2 id="login-title">连接私有工作台</h2>
</div>
<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}
<p class="connection-status" role="status">{connectionStatus}</p>
<button type="submit">Log in</button>
<p class="login-note">The server address is remembered on this device.</p>
</form>
<aside class="login-value-panel" aria-label="Product values">
<h2>安全 · 专注 · 高效</h2>
<p>SenlinAI 是面向知识工作者与内部团队的私有部署工作台,帮助团队在统一空间中收集信息、整理知识、协同决策。</p>
<ul>
<li><strong>私有部署,数据自有</strong><span>所有数据与模型运行在您的环境中。</span></li>
<li><strong>知识沉淀,结构清晰</strong><span>从收集到整理,形成可复用的团队知识。</span></li>
<li><strong>协同高效,聚焦成果</strong><span>围绕项目与任务协同,减少沟通成本。</span></li>
</ul>
</aside>
</section>
</main>