feat(hub): drop redundant /admin/org/:slug path + release v0.0.35 (#11)

Silo hostname already carries tenancy. Admin SPA routes become /admin/..., legacy bookmarks redirect, login lands on /admin.

Co-authored-by: Hong Jiarong <me@jrhim.com>
Co-committed-by: Hong Jiarong <me@jrhim.com>
This commit is contained in:
2026-07-19 01:36:10 +08:00
committed by 洪佳荣
parent eb0be43eac
commit 7f09fb1f13
25 changed files with 221 additions and 129 deletions
@@ -33,7 +33,7 @@
<div class="space-y-0.5">
{#each childProjects as p (p.id)}
<a
href={`/admin/org/${slug}/projects/${p.id}`}
href={`/admin/projects/${p.id}`}
class="flex items-center gap-2.5 px-3 py-2.5 text-sm transition hover:bg-surface-100"
>
<span class="flex h-7 w-7 items-center justify-center border border-primary-200 bg-primary-50 text-primary-700">
+40
View File
@@ -0,0 +1,40 @@
import type { MeResponse, OrgMembership } from './api';
/** Alpha Silo host prefix: <slug>.educraft[.dev].… */
export function hostOrgSlug(hostname: string = typeof window !== 'undefined' ? window.location.hostname : ''): string | null {
const host = hostname.toLowerCase();
const m = host.match(/^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.educraft(?:-dev)?\./);
return m?.[1] ?? null;
}
export function isOrgAdmin(org: OrgMembership | null | undefined): boolean {
if (!org) return false;
const role = String(org.role ?? '').toUpperCase();
return role === 'OWNER' || role === 'ADMIN';
}
/**
* Resolve the tenancy for this browser session.
* Prefers hostname slug (silo), then ?org=, then first admin membership, then first membership.
*/
export function resolveOrg(me: MeResponse | null | undefined, search: string = ''): OrgMembership | null {
if (!me || me.organizations.length === 0) return null;
const host = hostOrgSlug();
if (host) {
const byHost = me.organizations.find((o) => o.slug === host);
if (byHost) return byHost;
}
const q = new URLSearchParams(search).get('org')?.trim();
if (q) {
const byQuery = me.organizations.find((o) => o.slug === q);
if (byQuery) return byQuery;
}
const admin = me.organizations.find((o) => isOrgAdmin(o));
return admin ?? me.organizations[0] ?? null;
}
/** SPA paths no longer embed org slug (subdomain carries tenancy). */
export function adminPath(rest: string = ''): string {
const cleaned = rest.replace(/^\/+/, '');
return cleaned === '' ? '/admin' : `/admin/${cleaned}`;
}
+7 -4
View File
@@ -35,12 +35,9 @@ export async function loadSession(): Promise<void> {
/**
* Resolve org slug for org-scoped Feishu OAuth (`GET /auth/feishu/:orgSlug`).
* Unscoped `/auth/feishu` is disabled unless allowLegacyFeishuOAuth is on.
* Path no longer carries tenancy: prefer hostname silo slug, then ?org=.
*/
export function resolveLoginOrgSlug(): string | null {
const path = window.location.pathname.split('/').filter(Boolean);
if (path[0] === 'admin' && path[1] === 'org' && path[2]) {
return decodeURIComponent(path[2]);
}
const q = new URLSearchParams(window.location.search).get('org');
if (q && q.trim() !== '') return q.trim();
@@ -48,6 +45,12 @@ export function resolveLoginOrgSlug(): string | null {
const host = window.location.hostname.toLowerCase();
const m = host.match(/^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.educraft(?:-dev)?\./);
if (m?.[1]) return m[1];
// Legacy path while old bookmarks still land briefly before redirect.
const path = window.location.pathname.split('/').filter(Boolean);
if (path[0] === 'admin' && path[1] === 'org' && path[2]) {
return decodeURIComponent(path[2]);
}
return null;
}