feat(auth): complete sqlite auth and client invitations

This commit is contained in:
poyrazavsever
2026-07-16 16:21:31 +03:00
parent cce7265fb1
commit 92bc99ba12
30 changed files with 14355 additions and 170 deletions
+27
View File
@@ -0,0 +1,27 @@
"use server";
import { redirect } from "next/navigation";
import {
acceptPortalInvitation,
PortalInvitationError,
} from "@/server/auth/invitations";
export async function acceptInvitation(formData: FormData) {
const token = String(formData.get("token") ?? "");
const displayName = String(formData.get("displayName") ?? "");
const password = String(formData.get("password") ?? "");
try {
await acceptPortalInvitation({ token, displayName, password });
} catch (error) {
const message =
error instanceof PortalInvitationError
? error.message
: "Portal hesabı oluşturulamadı.";
redirect(`/invite/${encodeURIComponent(token)}?error=true&message=${encodeURIComponent(message)}`);
}
redirect(
`/login?message=${encodeURIComponent("Portal hesabın oluşturuldu. Şimdi giriş yapabilirsin.")}`,
);
}
+90
View File
@@ -0,0 +1,90 @@
import { LockKeyhole, Mail, UserRound } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { acceptInvitation } from "./actions";
import { AuthPageShell } from "@/components/auth/auth-page-shell";
import { SubmitButton } from "@/components/auth/submit-button";
import { ErrorToaster } from "@/components/error-toaster";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/field";
import { getPortalInvitationPreview } from "@/server/auth/invitations";
export const dynamic = "force-dynamic";
export default async function InvitationPage({
params,
searchParams,
}: {
params: Promise<{ token: string }>;
searchParams: Promise<{ error?: string; message?: string }>;
}) {
const { token } = await params;
const invitation = getPortalInvitationPreview(token);
if (!invitation) {
notFound();
}
const query = await searchParams;
const isUsable = invitation.status === "pending";
const unavailableMessage =
invitation.status === "expired"
? "Bu davetin süresi dolmuş. Freelancer'dan yeni bir bağlantı istemelisin."
: invitation.status === "accepted"
? "Bu davet daha önce kullanılmış. Hesabınla giriş yapabilirsin."
: invitation.status === "revoked"
? "Bu davet iptal edilmiş. Freelancer'dan yeni bir bağlantı istemelisin."
: null;
return (
<>
{query.error && query.message ? <ErrorToaster message={query.message} /> : null}
<AuthPageShell
title="Müşteri portalına katıl"
description="Davet edilen hesabın için adını ve şifreni belirle."
form={
isUsable ? (
<form className="space-y-6">
<input type="hidden" name="token" value={token} />
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email" className="flex items-center gap-2">
<Mail className="h-4 w-4 text-muted-foreground" />
E-posta
</Label>
<Input id="email" type="email" value={invitation.email} disabled />
</div>
<div className="space-y-2">
<Label htmlFor="displayName" className="flex items-center gap-2">
<UserRound className="h-4 w-4 text-muted-foreground" />
Ad soyad
</Label>
<Input id="displayName" name="displayName" required maxLength={120} />
</div>
<div className="space-y-2">
<Label htmlFor="password" className="flex items-center gap-2">
<LockKeyhole className="h-4 w-4 text-muted-foreground" />
Şifre
</Label>
<Input id="password" name="password" type="password" required minLength={8} maxLength={128} />
<p className="text-xs text-muted-foreground">En az 8 karakter kullan.</p>
</div>
</div>
<SubmitButton formAction={acceptInvitation} className="h-11 w-full" pendingText="Hesap oluşturuluyor...">
Portal hesabını oluştur
</SubmitButton>
</form>
) : (
<p className="text-sm text-muted-foreground">{unavailableMessage}</p>
)
}
secondaryAction={null}
footer={
<Link href="/login" className="text-sm font-medium text-primary hover:text-primary-hover">
Giriş sayfasına dön
</Link>
}
/>
</>
);
}