101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
|
|
import { createClient } from "@/lib/supabase/server";
|
||
|
|
import { redirect } from "next/navigation";
|
||
|
|
import { createClient as createAdminClient } from "@supabase/supabase-js";
|
||
|
|
import { cookies } from "next/headers";
|
||
|
|
import { createOrganization } from "./actions";
|
||
|
|
import { Suspense } from "react";
|
||
|
|
|
||
|
|
async function OnboardingContent() {
|
||
|
|
const supabase = await createClient();
|
||
|
|
const { data } = await supabase.auth.getUser();
|
||
|
|
|
||
|
|
if (!data?.user) {
|
||
|
|
redirect("/auth/login");
|
||
|
|
}
|
||
|
|
|
||
|
|
// If already in an org, redirect to dashboard
|
||
|
|
const { data: profile } = await supabase
|
||
|
|
.from("profiles")
|
||
|
|
.select("organization_id")
|
||
|
|
.eq("id", data.user.id)
|
||
|
|
.single();
|
||
|
|
|
||
|
|
if (profile?.organization_id) {
|
||
|
|
redirect("/dashboard");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for invite token cookie
|
||
|
|
const cookieStore = await cookies();
|
||
|
|
const token = cookieStore.get("invite_token")?.value;
|
||
|
|
|
||
|
|
if (token) {
|
||
|
|
const supabaseAdmin = createAdminClient(
|
||
|
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||
|
|
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
||
|
|
);
|
||
|
|
|
||
|
|
// Attempt to redeem it automatically
|
||
|
|
const { data: invite } = await supabaseAdmin
|
||
|
|
.from("invitations")
|
||
|
|
.select("*")
|
||
|
|
.eq("token", token)
|
||
|
|
.eq("status", "pending")
|
||
|
|
.single();
|
||
|
|
|
||
|
|
if (invite && new Date(invite.expires_at) > new Date()) {
|
||
|
|
await supabase.from("profiles").update({ organization_id: invite.organization_id, role: "member" }).eq("id", data.user.id);
|
||
|
|
await supabaseAdmin.from("invitations").update({ status: "accepted" }).eq("id", invite.id);
|
||
|
|
// Optional: Clear the cookie, but next redirect will naturally ignore it since organization_id is set
|
||
|
|
redirect("/dashboard");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex h-screen items-center justify-center bg-background text-foreground">
|
||
|
|
<div className="w-full max-w-md p-8 bg-card rounded-lg border border-border shadow-sm">
|
||
|
|
<div className="mb-6 text-center">
|
||
|
|
<h1 className="text-2xl font-semibold mb-2">Welcome</h1>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
Let's get started by creating an organization for your team.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form action={createOrganization} className="space-y-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label htmlFor="name" className="text-sm font-medium">
|
||
|
|
Organization Name
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
id="name"
|
||
|
|
name="name"
|
||
|
|
type="text"
|
||
|
|
required
|
||
|
|
placeholder="E.g. Acme Corp"
|
||
|
|
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
className="w-full bg-primary text-primary-foreground py-2 rounded-md text-sm font-medium hover:bg-primary/90 transition-colors"
|
||
|
|
>
|
||
|
|
Create Organization
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
<div className="mt-6 text-center text-sm text-muted-foreground">
|
||
|
|
Waiting for an invite? Ask your team owner to send you a link.
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function OnboardingPage() {
|
||
|
|
return (
|
||
|
|
<Suspense fallback={<div className="flex h-screen items-center justify-center">Loading...</div>}>
|
||
|
|
<OnboardingContent />
|
||
|
|
</Suspense>
|
||
|
|
);
|
||
|
|
}
|