fix(admin-web): break /admin/login returnTo redirect loop in dev

vite dev only proxied /api and /auth, so /admin/login hit the SPA root
layout, re-ran loadSession, got 401, and redirected back to /admin/login
with an ever-nesting returnTo. Proxy /admin/login to the backend (which
owns it before the SPA fallback) and guard redirectToLogin against
re-entering /admin/login.

Replace the only emoji-as-icon (the back-arrow on project detail) with
the Icon component (new arrow-left glyph).

Add scripts/dev-bootstrap.ts for seeding a local Silo (stub probes) so
npm run dev can start on a fresh dev DB.
This commit is contained in:
2026-07-15 17:39:22 +08:00
parent 11de9e81db
commit fb66614e38
5 changed files with 74 additions and 6 deletions
@@ -15,6 +15,7 @@
| 'logout'
| 'org'
| 'chevron'
| 'arrow-left'
| 'folder'
| 'file'
| 'check';
@@ -100,6 +101,14 @@
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
{:else if name === 'arrow-left'}
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
/>
</svg>
{:else if name === 'folder'}
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path
+1
View File
@@ -52,6 +52,7 @@ export function resolveLoginOrgSlug(): string | null {
}
export function redirectToLogin(): void {
if (window.location.pathname === '/admin/login') return;
const ret = encodeURIComponent(
window.location.pathname + window.location.search + window.location.hash,
);
@@ -15,6 +15,7 @@
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
import SelectField from '$lib/components/SelectField.svelte';
import Icon from '$lib/components/Icon.svelte';
import { toastError, toastSuccess } from '$lib/toast';
const slug = $derived(page.params.slug ?? '');
@@ -158,7 +159,8 @@
href={`/admin/org/${slug}/projects`}
class="inline-flex items-center gap-1 text-sm text-surface-700 hover:text-primary-600"
>
← 返回项目列表
<Icon name="arrow-left" class="h-4 w-4" />
返回项目列表
</a>
</div>
+4
View File
@@ -8,6 +8,10 @@ export default defineConfig({
proxy: {
'/api': 'http://127.0.0.1:8788',
'/auth': 'http://127.0.0.1:8788',
// Backend owns /admin/login (registered before the SPA fallback in
// src/admin/static.ts). Proxy it in dev so the SPA doesn't re-render
// its layout on that path and 401-redirect into a returnTo loop.
'/admin/login': 'http://127.0.0.1:8788',
},
},
});
+52
View File
@@ -0,0 +1,52 @@
import { config } from "dotenv";
config();
import { PrismaClient } from "@prisma/client";
import { bootstrapAlphaSilo } from "../src/deployment/bootstrap-silo.js";
import { loadLocalSecretKeyring, LocalSecretEnvelope } from "../src/security/secretEnvelope.js";
async function main(): Promise<void> {
const keyringFile = process.env["HUB_SECRET_KEYRING_FILE"];
if (!keyringFile) throw new Error("HUB_SECRET_KEYRING_FILE is required");
const secrets = new LocalSecretEnvelope(await loadLocalSecretKeyring());
const prisma = new PrismaClient();
try {
const result = await bootstrapAlphaSilo(
prisma,
secrets,
{
organization: {
id: "org_default",
slug: "default",
name: "Default Organization",
},
owner: {
openId: process.env["FEISHU_BOT_OPEN_ID"] ?? "dev-owner",
displayName: "Dev Owner",
},
feishu: {
appId: process.env["FEISHU_APP_ID"] ?? "dev-app",
appSecret: process.env["FEISHU_APP_SECRET"] ?? "dev-secret",
botOpenId: process.env["FEISHU_BOT_OPEN_ID"] ?? "dev-bot",
},
provider: {
providerId: "openrouter",
baseUrl: process.env["ANTHROPIC_BASE_URL"] ?? "https://openrouter.ai/api",
authToken: process.env["ANTHROPIC_AUTH_TOKEN"] ?? "dev-token",
},
},
{
feishu: async () => undefined,
provider: async () => undefined,
},
);
console.log("bootstrap ok:", JSON.stringify(result, null, 2));
} finally {
await prisma.$disconnect();
}
}
main().catch((error: unknown) => {
console.error("[dev-bootstrap] failed:", error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});