feat: add better auth sqlite runtime and server-side session flow
This commit is contained in:
+13
-36
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export function GET() {
|
||||
return Response.json({
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
Reference in New Issue
Block a user