Add local Pinscope multi-user auth for shared projects.

Self-host email/password accounts enable the existing collaborator
invite flow without Clerk; first admin inherits users/local projects.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-11 14:31:37 +02:00
co-authored by Cursor
parent c5b260b404
commit 5a69b380da
22 changed files with 1045 additions and 116 deletions
@@ -0,0 +1,33 @@
"use client";
import { useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useOptionalAuth, useOptionalUser } from "@/hooks/use-optional-auth";
import { localAuthEnabled } from "@/lib/auth";
/** When local auth is on, send unsigned users to /sign-in. */
export function AuthGate({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const { isSignedIn } = useOptionalAuth();
const { isLoaded } = useOptionalUser();
useEffect(() => {
if (!localAuthEnabled || !isLoaded) return;
if (!isSignedIn) {
const next = encodeURIComponent(pathname || "/");
router.replace(`/sign-in?next=${next}`);
}
}, [isLoaded, isSignedIn, pathname, router]);
if (!localAuthEnabled) return <>{children}</>;
if (!isLoaded) {
return (
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
Loading
</div>
);
}
if (!isSignedIn) return null;
return <>{children}</>;
}
@@ -1,12 +1,59 @@
"use client";
// Open-core seam: the cloud/gateway build replaces this file with the Clerk
// user button and live credit balance. The open-source build has neither.
// Open-core seam: cloud/gateway replaces with Clerk user button + credits.
// Local auth shows email and sign-out / sign-in links.
import Link from "next/link";
import { useOptionalAuth, useOptionalUser } from "@/hooks/use-optional-auth";
import { authEnabled, localAuthEnabled } from "@/lib/auth";
import { Button } from "@/components/ui/button";
export function SidebarCredits() {
return null;
}
export function SidebarUserButton() {
return null;
const { user, isLoaded } = useOptionalUser();
const { signOut, isSignedIn } = useOptionalAuth();
if (!authEnabled || !localAuthEnabled) {
return null;
}
if (!isLoaded) {
return <div className="px-2 text-xs text-muted-foreground"></div>;
}
if (!isSignedIn || !user) {
return (
<div className="flex flex-col gap-1 px-1">
<Button asChild size="sm" variant="outline" className="w-full justify-start">
<Link href="/sign-in">Sign in</Link>
</Button>
<Button asChild size="sm" variant="ghost" className="w-full justify-start">
<Link href="/sign-up">Create account</Link>
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-1 px-2 py-1">
<div className="truncate text-xs font-medium">{user.name || user.email}</div>
{user.email && user.name ? (
<div className="truncate text-[11px] text-muted-foreground">{user.email}</div>
) : null}
<Button
size="sm"
variant="ghost"
className="h-7 justify-start px-0 text-xs text-muted-foreground"
onClick={() => {
signOut?.();
window.location.href = "/sign-in";
}}
>
Sign out
</Button>
</div>
);
}