feat: add better auth sqlite runtime and server-side session flow

This commit is contained in:
Poyraz
2026-07-10 22:07:37 +03:00
parent 3504a02229
commit 57aab2932e
41 changed files with 4907 additions and 143 deletions
+13 -36
View File
@@ -1,53 +1,30 @@
import { DashboardShell } from "@/components/layout/dashboard-shell";
import { createClient } from "@/lib/supabase/server";
import { requireFreelancer } from "@/server/auth/session";
export default async function DashboardLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const { user, profile } = await requireFreelancer();
const displayName = profile.displayName || user.name || user.email.split("@")[0] || "Neta Kullanıcısı";
const { data: profile } = user
? await supabase
.from("profiles")
.select("first_name, last_name, avatar_url, role")
.eq("id", user.id)
.maybeSingle()
: { data: null };
if (profile?.role === "client") {
const { redirect } = await import("next/navigation");
redirect("/portal");
}
const fallbackName = user?.email?.split("@")[0] ?? "Neta Kullanıcısı";
const displayName =
[profile?.first_name, profile?.last_name].filter(Boolean).join(" ") ||
fallbackName;
const shortName = displayName
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("")
.slice(0, 2) || "MS";
const shortName =
displayName
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("")
.slice(0, 2) || "MS";
return (
<DashboardShell
user={{
email: user?.email ?? "bilinmiyor@mindspace.local",
email: user.email,
displayName,
shortName,
avatarUrl:
profile?.avatar_url ||
user?.user_metadata?.avatar_url ||
user?.user_metadata?.picture ||
null,
avatarUrl: user.image || null,
}}
>
{children}
+7
View File
@@ -0,0 +1,7 @@
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@/server/auth/auth";
export const runtime = "nodejs";
export const { GET, POST } = toNextJsHandler(auth);
+8
View File
@@ -0,0 +1,8 @@
export const runtime = "nodejs";
export function GET() {
return Response.json({
status: "ok",
timestamp: new Date().toISOString(),
});
}
+16
View File
@@ -0,0 +1,16 @@
import { checkReadiness } from "@/server/db/health";
export const runtime = "nodejs";
export function GET() {
const readiness = checkReadiness();
return Response.json(
{
status: readiness.ok ? "ok" : "unhealthy",
checks: readiness.checks,
timestamp: new Date().toISOString(),
},
{ status: readiness.ok ? 200 : 503 },
);
}
+49 -35
View File
@@ -1,31 +1,56 @@
'use server'
import { revalidatePath } from 'next/cache'
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import { getFirstAdminSetupState } from '@/lib/auth/first-admin-setup'
import { createInternalAuthUser } from '@/lib/auth/internal-users'
import { auth } from '@/server/auth/auth'
import { getProfileByAuthUserId } from '@/server/auth/session'
import { getFirstFreelancerSetupState, recordAuthAuditEvent } from '@/server/auth/setup'
import { getDefaultDisplayName, parseAuthCredentials } from '@/server/auth/validation'
const genericLoginError = 'E-posta veya şifre hatalı.'
export async function login(formData: FormData) {
const supabase = await createClient()
const credentials = parseAuthCredentials(formData)
let redirectTarget = '/'
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
try {
const result = await auth.api.signInEmail({
body: {
email: credentials.email,
password: credentials.password,
rememberMe: true,
},
})
const profile = getProfileByAuthUserId(result.user.id)
const { error } = await supabase.auth.signInWithPassword(data)
if (!profile || profile.disabled) {
await auth.api.signOut({ headers: await headers() })
await recordAuthAuditEvent({
type: 'login_failed',
authUserId: result.user.id,
email: credentials.email,
metadata: { reason: 'missing_or_disabled_profile' },
})
redirect(`/login?error=true&message=${encodeURIComponent(genericLoginError)}`)
}
if (error) {
redirect(`/login?error=true&message=${encodeURIComponent(error.message)}`)
redirectTarget = profile.role === 'client' ? '/portal' : '/'
} catch {
await recordAuthAuditEvent({
type: 'login_failed',
email: credentials.email,
metadata: { reason: 'invalid_credentials' },
})
redirect(`/login?error=true&message=${encodeURIComponent(genericLoginError)}`)
}
revalidatePath('/', 'layout')
redirect('/')
redirect(redirectTarget)
}
export async function signup(formData: FormData) {
const setupState = await getFirstAdminSetupState()
const setupState = await getFirstFreelancerSetupState()
if (setupState.errorMessage) {
redirect(`/register?error=true&message=${encodeURIComponent(setupState.errorMessage)}`)
@@ -34,44 +59,33 @@ export async function signup(formData: FormData) {
if (!setupState.available) {
redirect(
`/login?error=true&message=${encodeURIComponent(
'Kayıt kapalı. Bu Neta kurulumunda ilk admin hesabı zaten oluşturulmuş.',
'Kayıt kapalı. Bu Neta kurulumunda ilk freelancer hesabı zaten oluşturulmuş.',
)}`,
)
}
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
const credentials = parseAuthCredentials(formData)
try {
await createInternalAuthUser({
email: data.email,
password: data.password,
role: 'freelancer',
reason: 'first_admin',
await auth.api.signUpEmail({
body: {
name: getDefaultDisplayName(credentials.email),
email: credentials.email,
password: credentials.password,
rememberMe: true,
},
})
} catch (error) {
const message =
error instanceof Error ? error.message : 'Kullanıcı oluşturulamadı.'
const message = error instanceof Error ? error.message : 'Kullanıcı oluşturulamadı.'
redirect(`/register?error=true&message=${encodeURIComponent(message)}`)
}
const supabase = await createClient()
const { error } = await supabase.auth.signInWithPassword(data)
if (error) {
redirect(`/login?error=true&message=${encodeURIComponent(error.message)}`)
}
revalidatePath('/', 'layout')
redirect('/')
}
export async function signOut() {
const supabase = await createClient()
await supabase.auth.signOut()
await auth.api.signOut({ headers: await headers() })
revalidatePath('/', 'layout')
redirect('/login')
+14 -53
View File
@@ -1,71 +1,32 @@
import { PortalShell } from "@/components/layout/portal-shell";
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
import { requireClientUser } from "@/server/auth/session";
export default async function PortalLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const { user, profile } = await requireClientUser();
const displayName = profile.displayName || user.name || user.email.split("@")[0] || "Müşteri";
if (!user) {
redirect("/login");
}
const { data: profile } = await supabase
.from("profiles")
.select("first_name, last_name, avatar_url, role")
.eq("id", user.id)
.maybeSingle();
if (profile?.role !== "client") {
redirect("/");
}
const fallbackName = user.email?.split("@")[0] ?? "Müşteri";
const displayName =
[profile?.first_name, profile?.last_name].filter(Boolean).join(" ") ||
fallbackName;
const shortName = displayName
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("")
.slice(0, 2) || "MS";
const { data: clientData } = await supabase
.from("clients")
.select("id")
.eq("client_auth_id", user.id)
.maybeSingle();
let avgProgress = 0;
if (clientData) {
const { data: projectsData } = await supabase
.from("projects")
.select("progress")
.eq("client_id", clientData.id)
.eq("status", "active");
if (projectsData && projectsData.length > 0) {
avgProgress = Math.round(projectsData.reduce((sum, p) => sum + p.progress, 0) / projectsData.length);
}
}
const shortName =
displayName
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("")
.slice(0, 2) || "MS";
return (
<PortalShell
user={{
email: user.email ?? "bilinmiyor@mindspace.local",
email: user.email,
displayName,
shortName,
avatarUrl: profile?.avatar_url || null,
avatarUrl: user.image || null,
}}
progress={avgProgress}
progress={0}
>
{children}
</PortalShell>
+5 -3
View File
@@ -1,19 +1,21 @@
import { signup } from "@/app/login/actions";
import { AuthPageShell } from "@/components/auth/auth-page-shell";
import { ErrorToaster } from "@/components/error-toaster";
import { getFirstAdminSetupState } from "@/lib/auth/first-admin-setup";
import { getFirstFreelancerSetupState } from "@/server/auth/setup";
import { LockKeyhole, Mail, UserPlus } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { Button, Input, Label } from "poyraz-ui/atoms";
import { Input, Label } from "poyraz-ui/atoms";
import { SubmitButton } from "@/components/auth/submit-button";
export const dynamic = "force-dynamic";
export default async function RegisterPage({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const setupState = await getFirstAdminSetupState();
const setupState = await getFirstFreelancerSetupState();
if (setupState.errorMessage) {
redirect(`/login?error=true&message=${encodeURIComponent(setupState.errorMessage)}`);