feat(team): manual-code invite flow + member management

Multi-tenant invite system without SMTP dependency. Designed for dev/early
stage; promotes to email-driven later by adding SMTP to Appwrite.

New schema:
- invite_links table (code, email, role, status, expiresAt, invitedBy)
  with unique index on code, indexes on (tenantId,status) and (tenantId,email)

New code:
- lib/appwrite/audit.ts: logAudit() helper writes to audit_logs with
  X-Forwarded-For/User-Agent capture; never throws.
- lib/appwrite/tenant-guard.ts: requireTenant() returns
  { user, tenantId, role, settings }; pulls highest role from team
  memberships. requireRole() guard.
- lib/appwrite/team-actions.ts:
  * inviteMemberAction — creates short code (8 char nanoid-style),
    inserts invite_links row with team-scoped perms, returns shortUrl.
    Reuses existing pending invite for same email instead of duplicating.
    Blocks self-invite, blocks invite of existing members.
  * cancelInviteAction — owner/admin only, marks status=cancelled.
  * removeMemberAction — owner/admin only; protects self-removal and
    requires owner-on-owner.
  * updateMemberRoleAction — owner only.
  * resolveInviteCode — public-ish lookup by code (admin SDK).
  * acceptInviteAction — verifies session.email matches invite.email,
    creates membership via admin SDK, marks invite accepted.
  All mutations write to audit_logs.

UI:
- /d/[code] short-URL accept page (server). Logged-in matching user
  sees 'Daveti kabul et' button; non-matching user sees error; logged-out
  user gets sign-up / sign-in CTAs that preserve the code.
- /settings/members page (server): InviteForm, PendingInvitesTable,
  MembersTable. Owner/admin gates respected; only owner can change roles.
- Sign-up and sign-in forms accept ?invite=CODE (and ?email= for sign-up):
  hidden input -> server action redirects to /d/CODE on success.

Other:
- next.config.ts: removed eslint config block (deprecated in Next 16);
  kept typescript.ignoreBuildErrors for template legacy.
This commit is contained in:
kovakmedya
2026-04-30 05:34:47 +03:00
parent 84e5f20afd
commit 643f2de29b
17 changed files with 1258 additions and 13 deletions
@@ -13,7 +13,11 @@ import { cn } from "@/lib/utils";
import { signInAction } from "@/lib/appwrite/auth-actions";
import { initialAuthState } from "@/lib/appwrite/auth-types";
export function LoginForm1({ className, ...props }: React.ComponentProps<"div">) {
export function LoginForm1({
className,
inviteCode,
...props
}: React.ComponentProps<"div"> & { inviteCode?: string }) {
const [state, formAction, isPending] = useActionState(signInAction, initialAuthState);
return (
@@ -21,6 +25,7 @@ export function LoginForm1({ className, ...props }: React.ComponentProps<"div">)
<Card className="overflow-hidden p-0">
<CardContent className="grid p-0 md:grid-cols-2">
<form action={formAction} className="p-6 md:p-10">
{inviteCode && <input type="hidden" name="inviteCode" value={inviteCode} />}
<div className="flex flex-col gap-6">
<div className="flex justify-center">
<Link href="/" className="flex items-center gap-2 font-medium">
@@ -31,6 +36,12 @@ export function LoginForm1({ className, ...props }: React.ComponentProps<"div">)
</Link>
</div>
{inviteCode && (
<p className="text-muted-foreground rounded-md border bg-muted/50 px-3 py-2 text-center text-xs">
Davete katılmak için giriş yapın.
</p>
)}
<div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold tracking-tight">Tekrar hoş geldiniz</h1>
<p className="text-muted-foreground text-sm text-balance mt-1">
+8 -3
View File
@@ -3,14 +3,19 @@ import { redirect } from "next/navigation";
import { LoginForm1 } from "./components/login-form-1";
import { getCurrentUser } from "@/lib/appwrite/server";
export default async function Page() {
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ invite?: string }>;
}) {
const { invite } = await searchParams;
const user = await getCurrentUser();
if (user) redirect("/dashboard");
if (user) redirect(invite ? `/d/${invite}` : "/dashboard");
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-4xl">
<LoginForm1 />
<LoginForm1 inviteCode={invite} />
</div>
</div>
);
@@ -13,7 +13,12 @@ import { cn } from "@/lib/utils";
import { signUpAction } from "@/lib/appwrite/auth-actions";
import { initialAuthState } from "@/lib/appwrite/auth-types";
export function SignupForm1({ className, ...props }: React.ComponentProps<"div">) {
export function SignupForm1({
className,
inviteCode,
prefilledEmail,
...props
}: React.ComponentProps<"div"> & { inviteCode?: string; prefilledEmail?: string }) {
const [state, formAction, isPending] = useActionState(signUpAction, initialAuthState);
return (
@@ -23,6 +28,7 @@ export function SignupForm1({ className, ...props }: React.ComponentProps<"div">
<BrandPanel />
<form action={formAction} className="p-6 md:p-10">
{inviteCode && <input type="hidden" name="inviteCode" value={inviteCode} />}
<div className="flex flex-col gap-6">
<div className="flex justify-center">
<Link href="/" className="flex items-center gap-2 font-medium">
@@ -34,9 +40,13 @@ export function SignupForm1({ className, ...props }: React.ComponentProps<"div">
</div>
<div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold tracking-tight">Hesap oluşturun</h1>
<h1 className="text-2xl font-bold tracking-tight">
{inviteCode ? "Davete katıl" : "Hesap oluşturun"}
</h1>
<p className="text-muted-foreground text-sm text-balance mt-1">
Birkaç saniye içinde hesabınız hazır
{inviteCode
? "Hesap oluşturduktan sonra çalışma alanına otomatik katılacaksınız"
: "Birkaç saniye içinde hesabınız hazır"}
</p>
</div>
@@ -60,6 +70,8 @@ export function SignupForm1({ className, ...props }: React.ComponentProps<"div">
type="email"
placeholder="ornek@firma.com"
autoComplete="email"
defaultValue={prefilledEmail}
readOnly={Boolean(prefilledEmail)}
required
/>
</div>
+8 -3
View File
@@ -3,14 +3,19 @@ import { redirect } from "next/navigation";
import { SignupForm1 } from "./components/signup-form-1";
import { getCurrentUser } from "@/lib/appwrite/server";
export default async function SignUpPage() {
export default async function SignUpPage({
searchParams,
}: {
searchParams: Promise<{ invite?: string; email?: string }>;
}) {
const { invite, email } = await searchParams;
const user = await getCurrentUser();
if (user) redirect("/dashboard");
if (user) redirect(invite ? `/d/${invite}` : "/dashboard");
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-4xl">
<SignupForm1 />
<SignupForm1 inviteCode={invite} prefilledEmail={email} />
</div>
</div>
);
@@ -0,0 +1,114 @@
"use client";
import { useActionState, useEffect, useRef, useState } from "react";
import { Check, Copy, Loader2, UserPlus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { inviteMemberAction } from "@/lib/appwrite/team-actions";
import { initialInviteState } from "@/lib/appwrite/team-types";
export function InviteForm() {
const [state, formAction, isPending] = useActionState(inviteMemberAction, initialInviteState);
const [copied, setCopied] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
if (state.ok && formRef.current) {
formRef.current.reset();
}
}, [state.ok, state.shortUrl]);
const copy = async () => {
if (!state.shortUrl) return;
try {
await navigator.clipboard.writeText(state.shortUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
/* ignore */
}
};
return (
<Card>
<CardHeader>
<CardTitle>Üye davet et</CardTitle>
<CardDescription>
Email ve rol girin, oluşturulan kısa linki kopyalayıp davet edeceğiniz kişiye gönderin.
</CardDescription>
</CardHeader>
<CardContent>
<form ref={formRef} action={formAction} className="grid gap-4 md:grid-cols-[1fr_180px_auto]">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="ornek@firma.com"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="role">Rol</Label>
<Select name="role" defaultValue="member">
<SelectTrigger id="role">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="member">Üye</SelectItem>
<SelectItem value="admin">Yönetici</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<Button type="submit" disabled={isPending} className="w-full md:w-auto">
{isPending ? (
<>
<Loader2 className="size-4 animate-spin" />
Gönderiliyor...
</>
) : (
<>
<UserPlus className="size-4" />
Davet et
</>
)}
</Button>
</div>
</form>
{state.error && (
<p className="text-destructive mt-3 text-sm" role="alert">
{state.error}
</p>
)}
{state.ok && state.shortUrl && (
<div className="bg-muted/50 mt-4 flex flex-col gap-2 rounded-md border p-3">
{state.message && (
<p className="text-muted-foreground text-xs">{state.message}</p>
)}
<div className="flex items-center gap-2">
<Input value={state.shortUrl} readOnly className="font-mono text-xs" />
<Button type="button" variant="outline" size="sm" onClick={copy}>
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
{copied ? "Kopyalandı" : "Kopyala"}
</Button>
</div>
</div>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,158 @@
"use client";
import { useState, useTransition } from "react";
import { Loader2, Trash2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { removeMemberAction, updateMemberRoleAction } from "@/lib/appwrite/team-actions";
type Member = {
id: string;
userId: string;
name: string;
email: string;
role: string;
joined: string;
invited: string;
confirm: boolean;
};
const ROLE_LABEL: Record<string, string> = {
owner: "Sahip",
admin: "Yönetici",
member: "Üye",
};
export function MembersTable({
members,
currentUserId,
isOwner,
canManage,
}: {
members: Member[];
currentUserId: string;
isOwner: boolean;
canManage: boolean;
}) {
const [busy, setBusy] = useState<string | null>(null);
const [, startTransition] = useTransition();
const setRole = (membershipId: string, role: string) => {
setBusy(membershipId);
startTransition(async () => {
const fd = new FormData();
fd.set("membershipId", membershipId);
fd.set("role", role);
await updateMemberRoleAction({ ok: false }, fd);
setBusy(null);
});
};
const remove = (membershipId: string, name: string) => {
if (!confirm(`${name} adlı üyeyi çalışma alanından çıkarmak istediğinize emin misiniz?`)) {
return;
}
setBusy(membershipId);
startTransition(async () => {
const fd = new FormData();
fd.set("membershipId", membershipId);
await removeMemberAction({ ok: false }, fd);
setBusy(null);
});
};
return (
<Card>
<CardHeader>
<CardTitle>Üyeler ({members.length})</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>İsim</TableHead>
<TableHead>Email</TableHead>
<TableHead>Rol</TableHead>
<TableHead className="text-right">İşlem</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{members.map((m) => {
const isSelf = m.userId === currentUserId;
const isMemberOwner = m.role === "owner";
return (
<TableRow key={m.id}>
<TableCell className="font-medium">
{m.name}
{isSelf && (
<Badge variant="secondary" className="ml-2 text-xs">
Siz
</Badge>
)}
</TableCell>
<TableCell className="text-muted-foreground">{m.email}</TableCell>
<TableCell>
{isOwner && !isMemberOwner && !isSelf ? (
<Select
value={m.role}
disabled={busy === m.id}
onValueChange={(v) => setRole(m.id, v)}
>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="member">Üye</SelectItem>
<SelectItem value="admin">Yönetici</SelectItem>
</SelectContent>
</Select>
) : (
<Badge variant={isMemberOwner ? "default" : "outline"}>
{ROLE_LABEL[m.role] ?? m.role}
</Badge>
)}
</TableCell>
<TableCell className="text-right">
{canManage && !isSelf && !isMemberOwner ? (
<Button
type="button"
variant="ghost"
size="sm"
disabled={busy === m.id}
onClick={() => remove(m.id, m.name)}
>
{busy === m.id ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<Trash2 className="size-3.5" />
)}
Çıkar
</Button>
) : null}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</CardContent>
</Card>
);
}
@@ -0,0 +1,138 @@
"use client";
import { useTransition, useState } from "react";
import { Check, Copy, Loader2, X } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cancelInviteAction } from "@/lib/appwrite/team-actions";
type Invite = {
id: string;
code: string;
email: string;
role: string;
expiresAt?: string;
createdAt: string;
};
export function PendingInvitesTable({
invites,
canManage,
}: {
invites: Invite[];
canManage: boolean;
}) {
const [busy, setBusy] = useState<string | null>(null);
const [, startTransition] = useTransition();
const [copiedId, setCopiedId] = useState<string | null>(null);
const baseUrl =
typeof window !== "undefined" ? window.location.origin : "";
const copy = async (code: string, id: string) => {
try {
await navigator.clipboard.writeText(`${baseUrl}/d/${code}`);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
} catch {
/* ignore */
}
};
const cancel = (id: string) => {
setBusy(id);
startTransition(async () => {
const fd = new FormData();
fd.set("inviteId", id);
await cancelInviteAction({ ok: false }, fd);
setBusy(null);
});
};
const formatDate = (iso?: string) => {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("tr-TR", {
day: "2-digit",
month: "short",
year: "numeric",
});
};
return (
<Card>
<CardHeader>
<CardTitle>Bekleyen davetler ({invites.length})</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Email</TableHead>
<TableHead>Rol</TableHead>
<TableHead>Geçerlilik</TableHead>
<TableHead className="text-right">İşlem</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invites.map((inv) => (
<TableRow key={inv.id}>
<TableCell className="font-medium">{inv.email}</TableCell>
<TableCell>
<Badge variant="outline">
{inv.role === "admin" ? "Yönetici" : "Üye"}
</Badge>
</TableCell>
<TableCell className="text-muted-foreground">
{formatDate(inv.expiresAt)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => copy(inv.code, inv.id)}
>
{copiedId === inv.id ? (
<Check className="size-3.5" />
) : (
<Copy className="size-3.5" />
)}
Linki kopyala
</Button>
{canManage && (
<Button
type="button"
variant="ghost"
size="sm"
disabled={busy === inv.id}
onClick={() => cancel(inv.id)}
>
{busy === inv.id ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<X className="size-3.5" />
)}
İptal
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
}
@@ -0,0 +1,95 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { Query } from "node-appwrite";
import { createAdminClient } from "@/lib/appwrite/server";
import { DATABASE_ID, TABLES, type InviteLink } from "@/lib/appwrite/schema";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { InviteForm } from "./components/invite-form";
import { MembersTable } from "./components/members-table";
import { PendingInvitesTable } from "./components/pending-invites-table";
export const metadata: Metadata = {
title: "İşletmem — Ekip üyeleri",
};
export default async function MembersPage() {
let ctx;
try {
ctx = await requireTenant();
} catch {
redirect("/onboarding");
}
const canManage = ctx.role === "owner" || ctx.role === "admin";
const isOwner = ctx.role === "owner";
const { teams, tablesDB } = createAdminClient();
const [memberships, invites] = await Promise.all([
teams.listMemberships(ctx.tenantId).catch(() => ({ memberships: [], total: 0 })),
tablesDB
.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.inviteLinks,
queries: [
Query.equal("tenantId", ctx.tenantId),
Query.equal("status", "pending"),
Query.orderDesc("$createdAt"),
Query.limit(50),
],
})
.catch(() => ({ rows: [] as unknown[] })),
]);
const members = memberships.memberships.map((m) => ({
id: m.$id,
userId: m.userId,
name: m.userName || m.userEmail,
email: m.userEmail,
role: m.roles[0] ?? "member",
joined: m.joined,
invited: m.invited,
confirm: m.confirm,
}));
const pendingInvites = (invites.rows as unknown as InviteLink[]).map((row) => ({
id: row.$id,
code: row.code,
email: row.email,
role: row.role ?? "member",
expiresAt: row.expiresAt,
createdAt: row.$createdAt,
}));
return (
<div className="flex-1 space-y-6 px-6 pt-0">
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-sm">{ctx.settings?.companyName ?? "Çalışma alanı"}</p>
<h1 className="text-2xl font-bold tracking-tight">Ekip üyeleri</h1>
<p className="text-muted-foreground text-sm">
Çalışma alanına üye davet edin, rolleri yönetin.
</p>
</div>
{canManage ? (
<InviteForm />
) : (
<p className="text-muted-foreground text-sm">
Yeni üye davet etmek için yönetici yetkisine ihtiyacınız var.
</p>
)}
{pendingInvites.length > 0 && (
<PendingInvitesTable invites={pendingInvites} canManage={canManage} />
)}
<MembersTable
members={members}
currentUserId={ctx.user.id}
isOwner={isOwner}
canManage={canManage}
/>
</div>
);
}
+50
View File
@@ -0,0 +1,50 @@
"use client";
import { useTransition } from "react";
import { useRouter } from "next/navigation";
import { CheckCircle2, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { acceptInviteAction } from "@/lib/appwrite/team-actions";
import { useState } from "react";
export function AcceptInviteButton({ code }: { code: string }) {
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const handleAccept = () => {
setError(null);
startTransition(async () => {
const result = await acceptInviteAction(code);
if (result.ok) {
router.push("/dashboard");
} else {
setError(result.error ?? "Beklenmeyen hata.");
}
});
};
return (
<>
<Button onClick={handleAccept} disabled={isPending} className="w-full">
{isPending ? (
<>
<Loader2 className="size-4 animate-spin" />
Katılınıyor...
</>
) : (
<>
<CheckCircle2 className="size-4" />
Daveti kabul et
</>
)}
</Button>
{error && (
<p className="text-destructive text-center text-sm" role="alert">
{error}
</p>
)}
</>
);
}
+141
View File
@@ -0,0 +1,141 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Logo } from "@/components/logo";
import { resolveInviteCode } from "@/lib/appwrite/team-actions";
import { createAdminClient, getCurrentUser } from "@/lib/appwrite/server";
import { DATABASE_ID, TABLES, type TenantSettings } from "@/lib/appwrite/schema";
import { AcceptInviteButton } from "./accept-invite-button";
import { Query } from "node-appwrite";
export const metadata: Metadata = {
title: "İşletmem — Davet",
};
async function getCompanyName(tenantId: string): Promise<string | null> {
try {
const { tablesDB } = createAdminClient();
const result = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.tenantSettings,
queries: [Query.equal("tenantId", tenantId), Query.limit(1)],
});
return (result.rows[0] as unknown as TenantSettings | undefined)?.companyName ?? null;
} catch {
return null;
}
}
export default async function InvitePage({
params,
}: {
params: Promise<{ code: string }>;
}) {
const { code } = await params;
const invite = await resolveInviteCode(code);
const user = await getCurrentUser();
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="flex w-full max-w-md flex-col gap-6">
<Link href="/" className="flex items-center justify-center gap-2 font-medium">
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
<Logo size={22} />
</div>
<span className="text-xl font-semibold">İşletmem</span>
</Link>
{!invite || invite.status === "cancelled" ? (
<InvalidCard reason="Davet bulunamadı veya iptal edilmiş." />
) : invite.status === "accepted" ? (
<InvalidCard reason="Bu davet daha önce kabul edilmiş." action="dashboard" />
) : invite.expiresAt && new Date(invite.expiresAt).getTime() < Date.now() ? (
<InvalidCard reason="Bu davetin süresi dolmuş." />
) : (
<ValidInvite
code={code}
email={invite.email}
role={invite.role ?? "member"}
companyName={(await getCompanyName(invite.tenantId)) ?? "Bir çalışma alanı"}
currentUserEmail={user?.email ?? null}
/>
)}
</div>
</div>
);
}
function InvalidCard({ reason, action }: { reason: string; action?: "dashboard" }) {
return (
<Card>
<CardHeader className="text-center">
<CardTitle>Davet kullanılamıyor</CardTitle>
<CardDescription>{reason}</CardDescription>
</CardHeader>
<CardContent className="flex justify-center">
<Button asChild variant="outline">
<Link href={action === "dashboard" ? "/dashboard" : "/sign-in"}>
{action === "dashboard" ? "Panele git" : "Giriş yap"}
</Link>
</Button>
</CardContent>
</Card>
);
}
function ValidInvite({
code,
email,
role,
companyName,
currentUserEmail,
}: {
code: string;
email: string;
role: string;
companyName: string;
currentUserEmail: string | null;
}) {
const roleLabel = role === "admin" ? "Yönetici" : "Üye";
const emailMatches = currentUserEmail?.toLowerCase() === email.toLowerCase();
return (
<Card>
<CardHeader className="text-center">
<CardTitle className="text-2xl">{companyName}</CardTitle>
<CardDescription>
<strong>{email}</strong> olarak <strong>{roleLabel}</strong> rolüyle çalışma alanına davet
edildiniz.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-3">
{!currentUserEmail ? (
<>
<Button asChild className="w-full">
<Link href={`/sign-up?invite=${code}&email=${encodeURIComponent(email)}`}>
Hesap oluşturup katıl
</Link>
</Button>
<Button asChild variant="outline" className="w-full">
<Link href={`/sign-in?invite=${code}`}>Zaten hesabım var, giriş yap</Link>
</Button>
</>
) : emailMatches ? (
<AcceptInviteButton code={code} />
) : (
<>
<p className="text-destructive text-center text-sm">
Şu an <strong>{currentUserEmail}</strong> ile giriş yapmışsınız. Davet{" "}
<strong>{email}</strong> içindir. Doğru hesabı kullanın.
</p>
<Button asChild variant="outline" className="w-full">
<Link href={`/sign-in?invite=${code}`}>Farklı hesapla giriş yap</Link>
</Button>
</>
)}
</CardContent>
</Card>
);
}