feat: added desktop sso endpoints

This commit is contained in:
2026-05-28 16:05:47 -07:00
parent 91a376ac0a
commit bf6171a667
11 changed files with 1077 additions and 5 deletions

View File

@@ -0,0 +1,46 @@
/**
* GET /api/auth/me
*
* Exposes a profile endpoint for the VibnCode desktop app.
* Resolves the Bearer vibn_sk_... Workspace API key, queries the database,
* and returns the corresponding owner's user details.
*/
import { NextResponse } from "next/server";
import { requireWorkspacePrincipal } from "@/lib/auth/workspace-auth";
import { queryOne } from "@/lib/db-postgres";
export async function GET(request: Request) {
// 1. Authenticate the Workspace API key or Browser Session
const principal = await requireWorkspacePrincipal(request);
if (principal instanceof NextResponse) return principal;
try {
// 2. Query user details from fs_users
const user = await queryOne<{
id: string;
data: any;
}>(
`SELECT id, data FROM fs_users WHERE id = $1 LIMIT 1`,
[principal.userId]
);
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
// 3. Return user profile compatible with the desktop client's User expectations
return NextResponse.json({
user: {
id: user.id,
name: user.data?.name || user.data?.display_name || "Vibn Owner",
email: user.data?.email || "",
photoUrl: user.data?.image || user.data?.photo_url || null,
workspace: principal.workspace.slug,
}
});
} catch (err) {
console.error("[api/auth/me GET]", err);
return NextResponse.json({ error: "Failed to resolve profile" }, { status: 500 });
}
}

View File

@@ -0,0 +1,61 @@
/**
* GET /api/auth/token
*
* Secure endpoint called by the browser during desktop SSO.
* Verifies the user has a valid NextAuth browser session, resolves or mints
* a Workspace API key, and returns it.
*/
import { NextResponse } from "next/server";
import { authSession } from "@/lib/auth/session-server";
import { queryOne } from "@/lib/db-postgres";
import { getWorkspaceByOwner } from "@/lib/workspaces";
import { mintWorkspaceApiKey, listWorkspaceApiKeys, revealWorkspaceApiKey } from "@/lib/auth/workspace-auth";
export async function GET() {
// 1. Verify caller has an active NextAuth browser session cookie
const session = await authSession();
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// 2. Fetch the corresponding Postgres user
const user = await queryOne<{ id: string }>(
`SELECT id FROM fs_users WHERE data->>'email' = $1 LIMIT 1`,
[session.user.email]
);
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 401 });
}
// 3. Get the user's active workspace
const workspace = await getWorkspaceByOwner(user.id);
if (!workspace) {
return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
}
try {
// 4. Try to reuse their existing, active workspace API key to avoid key bloating
const keys = await listWorkspaceApiKeys(workspace.id);
const activeKey = keys.find((k) => !k.revoked_at);
if (activeKey) {
const revealed = await revealWorkspaceApiKey(workspace.id, activeKey.id);
if (revealed) {
return NextResponse.json({ token: revealed.token });
}
}
// 5. Otherwise, mint a fresh key for the desktop client
const minted = await mintWorkspaceApiKey({
workspaceId: workspace.id,
name: "VibnCode Desktop SSO",
createdBy: user.id,
});
return NextResponse.json({ token: minted.token });
} catch (err) {
console.error("[api/auth/token GET]", err);
return NextResponse.json({ error: "Failed to resolve workspace token" }, { status: 500 });
}
}

View File

@@ -21,14 +21,39 @@ function AuthPageInner() {
const router = useRouter();
const searchParams = useSearchParams();
const [ssoProcessing, setSsoProcessing] = React.useState(false);
useEffect(() => {
if (status === "authenticated" && session?.user?.email) {
const isVibnCodeSSO = searchParams?.get("vibncode") === "true";
if (isVibnCodeSSO) {
setSsoProcessing(true);
// Call our new secure token endpoint
fetch("/api/auth/token")
.then((r) => r.json())
.then((data) => {
if (data.token) {
// Deep-link redirect back to the VibnCode desktop app
window.location.href = `vibncode://auth/callback?token=${data.token}`;
} else {
console.error("SSO Token missing from response", data);
setSsoProcessing(false);
}
})
.catch((err) => {
console.error("Desktop SSO failed:", err);
setSsoProcessing(false);
});
return;
}
const workspace = deriveWorkspace(session.user.email);
// Check if user has projects. If 0, go to onboarding, else go to projects.
fetch("/api/projects")
.then(r => r.json())
.then(d => {
.then((r) => r.json())
.then((d) => {
if (d.projects && d.projects.length > 0) {
router.push(`/${workspace}/projects`);
} else {
@@ -39,7 +64,7 @@ function AuthPageInner() {
}
}, [status, session, router, searchParams]);
if (status === "loading") {
if (status === "loading" || ssoProcessing) {
return (
<div
className="new-site-wrapper"
@@ -80,7 +105,9 @@ function AuthPageInner() {
textTransform: "uppercase",
}}
>
Checking session
{ssoProcessing
? "Authorizing VibnCode Desktop..."
: "Checking session"}
</div>
</div>
</div>