mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 22:59:04 +02:00
* fix(routing): rename /new-workspace to /workspaces/new + extend reserved slug list
Two related changes:
1. Rename the global workspace-creation route from /new-workspace to
/workspaces/new. The hyphenated word-group `new-workspace` is a
common user workspace name (last deploy was blocked by a real user
with exactly this slug). Industry consensus from auditing Linear,
Vercel, Notion, Slack, GitHub: zero major SaaS uses hyphenated
word-group root routes — they all use single words or `/{noun}/{verb}`
pairs. Reserving the noun `workspaces` automatically protects the
entire `/workspaces/*` subtree, so future workspace-related routes
(`/workspaces/{id}/edit`, `/workspaces/{id}/billing`, etc.) need no
additional reserved slugs or audit migrations.
2. Extend the reserved slug list to cover the minimal set recommended by
the URL-design audit: full auth flow vocab, RFC 2142 mailbox names
(postmaster, abuse, noreply...), hostname confusables (mail, ftp,
static, cdn...), and likely-future platform routes (docs, support,
status, legal, privacy, terms, security, etc.). Production data
audit confirmed zero conflicts for every newly added slug, so
migration 047 (the safety net) passes cleanly.
Slugs intentionally NOT added despite being in scope of the audit:
admin, multica, new, setup, www. Each has one production workspace
already using it; adding them now would block deploy. They will be
handled in a follow-up PR via owner outreach + targeted rename.
Also adds a CLAUDE.md convention rule: new global routes MUST use a
single word or `/{noun}/{verb}` pair, never hyphenated word groups.
This prevents the pattern from regenerating itself.
This PR does NOT resolve the currently-blocked prd deploy — that requires
the existing `slug='new-workspace'` workspace (owner: Dhruv Raina) to be
renamed by ops. After that workspace is renamed and migration 046 passes,
this PR's migration 047 will also pass on its first run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* review: drop migration 046, sweep stale comments, drive reserved test from map
Address code review on PR #1188:
1. Delete migration 046 (audit_new_workspace_slug). It audits "new-workspace"
which is no longer a reserved slug after this PR's rename. Removing 046
has an unexpected upside: it directly unblocks the currently-stuck prd
deploy. Migration 046 had never successfully applied (it was the source
of the deploy block); the audit-only nature means down-rollback is a
no-op. The user workspace previously caught by 046 (slug='new-workspace',
owner: Dhruv Raina) is now safe — `new-workspace` is no longer reserved,
so the slug correctly resolves to that workspace and the global route
`/workspaces/new` doesn't shadow it.
2. Refactor workspace_test.go to drive its reserved-slug list from the
reservedSlugs map directly via `for slug := range reservedSlugs`. The
previous hand-copied list was already drifting (40-ish entries vs 58 in
the map). Now drift is impossible.
3. Sweep ~10 stale `/new-workspace` references in code comments to
`/workspaces/new`. Comments only — runtime unchanged. The references
in reserved-slugs.ts/workspace_reserved_slugs.go and CLAUDE.md are
intentionally kept as anti-pattern examples ("don't add hyphenated
word-group root routes like /new-workspace").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
148 lines
5.0 KiB
TypeScript
148 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useEffect, useState } from "react";
|
|
import { useSearchParams, useRouter } from "next/navigation";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { useAuthStore } from "@multica/core/auth";
|
|
import { workspaceKeys } from "@multica/core/workspace/queries";
|
|
import { paths } from "@multica/core/paths";
|
|
import { api } from "@multica/core/api";
|
|
import {
|
|
Card,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardDescription,
|
|
CardContent,
|
|
} from "@multica/ui/components/ui/card";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import { Loader2 } from "lucide-react";
|
|
|
|
function CallbackContent() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const qc = useQueryClient();
|
|
const loginWithGoogle = useAuthStore((s) => s.loginWithGoogle);
|
|
const [error, setError] = useState("");
|
|
const [desktopToken, setDesktopToken] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const code = searchParams.get("code");
|
|
if (!code) {
|
|
setError("Missing authorization code");
|
|
return;
|
|
}
|
|
|
|
const errorParam = searchParams.get("error");
|
|
if (errorParam) {
|
|
setError(errorParam === "access_denied" ? "Access denied" : errorParam);
|
|
return;
|
|
}
|
|
|
|
const state = searchParams.get("state") || "";
|
|
const stateParts = state.split(",");
|
|
const isDesktop = stateParts.includes("platform:desktop");
|
|
const nextPart = stateParts.find((p) => p.startsWith("next:"));
|
|
const nextUrl = nextPart ? nextPart.slice(5) : null; // strip "next:" prefix
|
|
|
|
const redirectUri = `${window.location.origin}/auth/callback`;
|
|
|
|
if (isDesktop) {
|
|
// Desktop flow: exchange code for token, then redirect via deep link
|
|
api
|
|
.googleLogin(code, redirectUri)
|
|
.then(({ token }) => {
|
|
setDesktopToken(token);
|
|
window.location.href = `multica://auth/callback?token=${encodeURIComponent(token)}`;
|
|
})
|
|
.catch((err) => {
|
|
setError(err instanceof Error ? err.message : "Login failed");
|
|
});
|
|
} else {
|
|
// Normal web flow
|
|
loginWithGoogle(code, redirectUri)
|
|
.then(async () => {
|
|
const wsList = await api.listWorkspaces();
|
|
qc.setQueryData(workspaceKeys.list(), wsList);
|
|
// URL is now the source of truth for the current workspace — the
|
|
// [workspaceSlug]/layout syncs stores + cookie once we navigate.
|
|
// Honor ?next= first (e.g. came from /invite/{id}), otherwise land
|
|
// in the first workspace's issues, or /workspaces/new for zero-workspace users.
|
|
const [first] = wsList;
|
|
const defaultDest = first
|
|
? paths.workspace(first.slug).issues()
|
|
: paths.newWorkspace();
|
|
router.push(nextUrl || defaultDest);
|
|
})
|
|
.catch((err) => {
|
|
setError(err instanceof Error ? err.message : "Login failed");
|
|
});
|
|
}
|
|
}, [searchParams, loginWithGoogle, router, qc]);
|
|
|
|
if (desktopToken) {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center">
|
|
<Card className="w-full max-w-sm">
|
|
<CardHeader className="text-center">
|
|
<CardTitle className="text-2xl">Opening Multica</CardTitle>
|
|
<CardDescription>
|
|
You should see a prompt to open the Multica desktop app. If
|
|
nothing happens, click the button below.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="flex justify-center">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
window.location.href = `multica://auth/callback?token=${encodeURIComponent(desktopToken)}`;
|
|
}}
|
|
>
|
|
Open Multica Desktop
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center">
|
|
<Card className="w-full max-w-sm">
|
|
<CardHeader className="text-center">
|
|
<CardTitle className="text-2xl">Login Failed</CardTitle>
|
|
<CardDescription>{error}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="flex justify-center">
|
|
<a href={paths.login()} className="text-primary underline-offset-4 hover:underline">
|
|
Back to login
|
|
</a>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center">
|
|
<Card className="w-full max-w-sm">
|
|
<CardHeader className="text-center">
|
|
<CardTitle className="text-2xl">Signing in...</CardTitle>
|
|
<CardDescription>Please wait while we complete your login</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="flex justify-center">
|
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function CallbackPage() {
|
|
return (
|
|
<Suspense fallback={null}>
|
|
<CallbackContent />
|
|
</Suspense>
|
|
);
|
|
}
|