feat: implement internal auth user creation and update related functionalities

This commit is contained in:
Poyraz Avsever
2026-06-14 12:41:00 +03:00
parent 8fa89168c3
commit b1620d4748
19 changed files with 382 additions and 129 deletions
+5 -6
View File
@@ -5,6 +5,7 @@ import {
type ProjectFinanceItem,
type ProjectPlanningSectionItem,
} from "@/app/(dashboard)/projects/[id]/project-detail-client";
import { createServiceRoleClient } from "@/lib/supabase/admin";
import { createClient } from "@/lib/supabase/server";
import { notFound } from "next/navigation";
@@ -105,7 +106,7 @@ export default async function ProjectDetailPage({
const projectData = projectRow as unknown as ProjectRow;
const coverImageUrl = projectData.cover_image_path
? await createProjectImageUrl(supabase, projectData.cover_image_path)
? await createProjectImageUrl(projectData.cover_image_path)
: null;
const project: ProjectDetail = {
@@ -165,11 +166,9 @@ export default async function ProjectDetailPage({
);
}
async function createProjectImageUrl(
supabase: Awaited<ReturnType<typeof createClient>>,
path: string,
) {
const { data } = await supabase.storage
async function createProjectImageUrl(path: string) {
const admin = createServiceRoleClient();
const { data } = await admin.storage
.from("project-assets")
.createSignedUrl(path, 60 * 15);
+3 -5
View File
@@ -1,6 +1,7 @@
"use server";
import { createClient } from "@/lib/supabase/server";
import { createServiceRoleClient } from "@/lib/supabase/admin";
import { randomUUID } from "crypto";
import { revalidatePath } from "next/cache";
@@ -114,12 +115,10 @@ function sanitizeFileName(name: string) {
}
async function uploadCoverImage({
supabase,
userId,
projectId,
formData,
}: {
supabase: Awaited<ReturnType<typeof createClient>>;
userId: string;
projectId: string;
formData: FormData;
@@ -132,7 +131,8 @@ async function uploadCoverImage({
const fileName = `${Date.now()}-${sanitizeFileName(file.name) || "cover-image"}`;
const path = `${userId}/projects/${projectId}/${fileName}`;
const { error } = await supabase.storage
const admin = createServiceRoleClient();
const { error } = await admin.storage
.from(PROJECT_ASSETS_BUCKET)
.upload(path, file, {
cacheControl: "3600",
@@ -157,7 +157,6 @@ export async function createProjectRecord(formData: FormData) {
}
const coverImagePath = await uploadCoverImage({
supabase,
userId,
projectId,
formData,
@@ -187,7 +186,6 @@ export async function updateProjectRecord(formData: FormData) {
}
const coverImagePath = await uploadCoverImage({
supabase,
userId,
projectId: id,
formData,
+3 -3
View File
@@ -4,6 +4,7 @@ import {
type ProjectListItem,
} from "@/app/(dashboard)/projects/projects-client";
import { createClient } from "@/lib/supabase/server";
import { createServiceRoleClient } from "@/lib/supabase/admin";
type ProjectRow = {
id: string;
@@ -59,7 +60,6 @@ export default async function ProjectsPage() {
const taskStats = countTasksByProject((taskRows || []) as TaskRow[]);
const clients = (clientRows || []) as ProjectClientOption[];
const signedUrls = await createProjectImageUrls(
supabase,
((projectRows || []) as unknown as ProjectRow[])
.map((project) => project.cover_image_path)
.filter(Boolean) as string[],
@@ -93,15 +93,15 @@ export default async function ProjectsPage() {
}
async function createProjectImageUrls(
supabase: Awaited<ReturnType<typeof createClient>>,
paths: string[],
) {
const admin = createServiceRoleClient();
const urls = new Map<string, string>();
const uniquePaths = Array.from(new Set(paths));
await Promise.all(
uniquePaths.map(async (path) => {
const { data } = await supabase.storage
const { data } = await admin.storage
.from("project-assets")
.createSignedUrl(path, 60 * 15);
+13 -10
View File
@@ -19,6 +19,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
toast,
} from "poyraz-ui/molecules";
import {
CalendarDays,
@@ -398,6 +399,13 @@ function ProjectDialog({
try {
await action(formData);
setOpen(false);
toast.success(mode === "create" ? "Proje eklendi." : "Proje güncellendi.");
} catch (error) {
toast.error(
error instanceof Error
? error.message
: "Proje kaydedilirken beklenmeyen bir hata oluştu.",
);
} finally {
setIsSubmitting(false);
}
@@ -506,14 +514,6 @@ function CoverImageInput({ project }: { project?: ProjectListItem }) {
</div>
)}
<div>
<div>
< ImageIcon className="h-6 w-6" />
</div>
</div>
{previewUrl ? (
<div className="absolute inset-x-0 bottom-0 bg-background/90 px-3 py-2 text-xs text-muted-foreground backdrop-blur">
Görseli değiştirmek için tıkla.
@@ -788,8 +788,11 @@ function AIProjectRiskDialog({ projectId }: { projectId?: string }) {
throw new Error(data.error || "Bilinmeyen bir hata oluştu.");
}
setResult(data.text);
} catch (err: any) {
setResult("Hata: " + err.message);
} catch (err) {
setResult(
"Hata: " +
(err instanceof Error ? err.message : "Bilinmeyen bir hata oluştu."),
);
} finally {
setLoading(false);
}
+4 -2
View File
@@ -2,6 +2,7 @@
import { revalidatePath } from 'next/cache'
import { createServiceRoleClient } from '@/lib/supabase/admin'
import { createClient } from '@/lib/supabase/server'
type ProfileUpdateData = {
@@ -30,8 +31,9 @@ export async function updateProfile(formData: FormData) {
if (avatarFile && avatarFile.size > 0) {
const fileExt = avatarFile.name.split('.').pop()
const fileName = `${user.id}/${Math.random()}.${fileExt}`
const admin = createServiceRoleClient()
const { error: uploadError } = await supabase.storage
const { error: uploadError } = await admin.storage
.from('avatars')
.upload(fileName, avatarFile, { upsert: true })
@@ -43,7 +45,7 @@ export async function updateProfile(formData: FormData) {
const {
data: { publicUrl },
} = supabase.storage.from('avatars').getPublicUrl(fileName)
} = admin.storage.from('avatars').getPublicUrl(fileName)
avatarUrl = publicUrl
}
+71 -91
View File
@@ -1,123 +1,103 @@
import { createInternalAuthUser } from "@/lib/auth/internal-users";
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";
type SupabaseAdminUserResponse = {
id?: string;
email?: string;
message?: string;
error_description?: string;
};
export async function POST(request: Request) {
try {
const { email, password, client_id } = await request.json();
if (!email || !password || !client_id) {
return NextResponse.json(
{ error: "Email, şifre ve müşteri ID gereklidir." },
{ error: "E-posta, şifre ve müşteri ID gereklidir." },
{ status: 400 },
);
}
const supabaseUrl =
process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
const supabase = await createClient();
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();
if (!supabaseUrl || !serviceRoleKey) {
if (userError || !user) {
return NextResponse.json(
{ error: "Supabase service role ayarı eksik." },
{ status: 500 },
{ error: "Müşteri hesabı oluşturmak için giriş yapmalısınız." },
{ status: 401 },
);
}
const userResponse = await fetch(`${supabaseUrl}/auth/v1/admin/users`, {
method: "POST",
headers: getServiceHeaders(serviceRoleKey),
body: JSON.stringify({
email,
password,
email_confirm: true,
app_metadata: {
internal_created: true,
role: "client",
},
}),
});
const userPayload = (await userResponse.json()) as SupabaseAdminUserResponse;
const { data: client, error: clientError } = await supabase
.from("clients")
.select("id, client_auth_id")
.eq("id", client_id)
.eq("user_id", user.id)
.single();
if (!userResponse.ok || !userPayload.id) {
if (clientError || !client) {
return NextResponse.json(
{ error: "Müşteri kaydı bulunamadı." },
{ status: 404 },
);
}
if (client.client_auth_id) {
return NextResponse.json(
{ error: "Bu müşteri için portal hesabı zaten oluşturulmuş." },
{ status: 409 },
);
}
const {
admin,
user: createdUser,
userId,
} = await createInternalAuthUser({
email,
password,
role: "client",
reason: "client_portal",
});
const { error: profileError } = await admin
.from("profiles")
.update({ role: "client" })
.eq("id", userId);
if (profileError) {
return NextResponse.json(
{
error:
userPayload.message ||
userPayload.error_description ||
"Kullanıcı oluşturulamadı.",
error: `Kullanıcı oluşturuldu fakat profil rolü güncellenemedi: ${profileError.message}`,
},
{ status: 400 },
);
}
const userId = userPayload.id;
await patchRestRow({
supabaseUrl,
serviceRoleKey,
table: "profiles",
filter: `id=eq.${encodeURIComponent(userId)}`,
payload: { role: "client" },
});
const clientResponse = await patchRestRow({
supabaseUrl,
serviceRoleKey,
table: "clients",
filter: `id=eq.${encodeURIComponent(client_id)}`,
payload: { client_auth_id: userId },
});
if (!clientResponse.ok) {
return NextResponse.json(
{ error: "Kullanıcı oluşturuldu fakat müşteri kaydıyla ilişkilendirilemedi." },
{ status: 500 },
);
}
return NextResponse.json({ success: true, user: userPayload });
const { error: updateClientError } = await admin
.from("clients")
.update({ client_auth_id: userId })
.eq("id", client_id)
.eq("user_id", user.id);
if (updateClientError) {
return NextResponse.json(
{
error: `Kullanıcı oluşturuldu fakat müşteri kaydıyla ilişkilendirilemedi: ${updateClientError.message}`,
},
{ status: 500 },
);
}
return NextResponse.json({ success: true, user: createdUser });
} catch (error) {
console.error("Create client user error:", error);
return NextResponse.json(
{ error: "Sunucu tarafında beklenmeyen bir hata oluştu." },
{
error:
error instanceof Error
? error.message
: "Sunucu tarafında beklenmeyen bir hata oluştu.",
},
{ status: 500 },
);
}
}
function getServiceHeaders(serviceRoleKey: string) {
return {
apikey: serviceRoleKey,
authorization: `Bearer ${serviceRoleKey}`,
"content-type": "application/json",
};
}
function patchRestRow({
supabaseUrl,
serviceRoleKey,
table,
filter,
payload,
}: {
supabaseUrl: string;
serviceRoleKey: string;
table: string;
filter: string;
payload: Record<string, unknown>;
}) {
return fetch(`${supabaseUrl}/rest/v1/${table}?${filter}`, {
method: "PATCH",
headers: {
...getServiceHeaders(serviceRoleKey),
prefer: "return=minimal",
},
body: JSON.stringify(payload),
});
}
+17 -4
View File
@@ -4,6 +4,7 @@ import { revalidatePath } from 'next/cache'
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'
export async function login(formData: FormData) {
const supabase = await createClient()
@@ -38,17 +39,29 @@ export async function signup(formData: FormData) {
)
}
const supabase = await createClient()
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
const { error } = await supabase.auth.signUp(data)
try {
await createInternalAuthUser({
email: data.email,
password: data.password,
role: 'freelancer',
reason: 'first_admin',
})
} catch (error) {
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(`/register?error=true&message=${encodeURIComponent(error.message)}`)
redirect(`/login?error=true&message=${encodeURIComponent(error.message)}`)
}
revalidatePath('/', 'layout')