From 7eb0c1acc2dc3880e643eb6a89715c5ed335d3ae Mon Sep 17 00:00:00 2001 From: Ege Can Komur Date: Wed, 20 May 2026 02:29:19 +0300 Subject: [PATCH] =?UTF-8?q?fix(auth):=20SDK'y=C4=B1=20kald=C4=B1r=C4=B1p?= =?UTF-8?q?=20ince=20REST=20katman=C4=B1na=20ge=C3=A7=20(lib/appwrite-rest?= =?UTF-8?q?.ts)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sorun: - node-appwrite v20-25 hepsi Node 26'da bozuk (node-fetch-native-with-agent polyfill) - appwrite browser SDK Server Action context'inde 'unexpected response' veriyor (büyük olasılıkla browser-only global'ları kontrol ederken) Çözüm: - Tüm Appwrite SDK'larını sil (appwrite + node-appwrite) - lib/appwrite-rest.ts: native fetch üzerinde ~250 satırlık ince REST wrapper - account: createEmailPasswordSession, get, deleteSession - tablesDB: listRows, getRow, createRow, updateRow, deleteRow - storage: listFiles, createFile, deleteFile, fileViewUrl - Q helpers: equal, orderAsc/Desc, limit, offset - AppwriteError class - Session secret cookie tabanlı auth korundu (isletmem-kovakcrm'deki desen) - Tüm CRUD action ve query'ler REST katmanına bağlandı End-to-end test edildi: ✓ Login (proper 401 hata mesajları, başarılı durumda redirect) ✓ Public read (services, blog, testimonials) ✓ Anonim create (contact form) ✓ Build (24 route) ✓ Sıfır SDK bağımlılığı — Node 26 sorunu yok --- app/actions.ts | 27 +- app/admin/(protected)/blog/[id]/edit/page.tsx | 2 +- .../(protected)/hizmetler/[id]/edit/page.tsx | 2 +- app/admin/(protected)/medya/page.tsx | 14 +- app/admin/(protected)/page.tsx | 18 +- .../(protected)/projeler/[id]/edit/page.tsx | 2 +- .../referanslar/[id]/edit/page.tsx | 2 +- app/admin/(protected)/seo/[id]/edit/page.tsx | 2 +- app/admin/login/actions.ts | 54 ++-- lib/admin-actions.ts | 232 ++++++--------- lib/appwrite-rest.ts | 276 ++++++++++++++++++ lib/appwrite-server.ts | 45 --- lib/appwrite.ts | 16 - lib/auth.ts | 4 +- lib/data.ts | 90 +++--- lib/types.ts | 18 +- package-lock.json | 31 -- package.json | 1 - 18 files changed, 503 insertions(+), 333 deletions(-) create mode 100644 lib/appwrite-rest.ts delete mode 100644 lib/appwrite-server.ts delete mode 100644 lib/appwrite.ts diff --git a/app/actions.ts b/app/actions.ts index f20c624..3299b16 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,7 +1,6 @@ "use server"; -import { ID } from "appwrite"; -import { publicDB, DATABASE_ID, TABLES } from "@/lib/appwrite-server"; +import { DATABASE_ID, ID, TABLES, tablesDB } from "@/lib/appwrite-rest"; export type ContactFormState = { ok: boolean; @@ -34,20 +33,18 @@ export async function submitContact( } try { - await publicDB.createRow({ - databaseId: DATABASE_ID, - tableId: TABLES.contactMessages, - rowId: ID.unique(), - data: { - name, - email, - phone: phone || null, - subject: subject || null, - message, - status: "new", - }, + await tablesDB.createRow(DATABASE_ID, TABLES.contactMessages, ID.unique(), { + name, + email, + phone: phone || null, + subject: subject || null, + message, + status: "new", }); - return { ok: true, message: "Mesajınız iletildi. En kısa sürede dönüş yapacağız." }; + return { + ok: true, + message: "Mesajınız iletildi. En kısa sürede dönüş yapacağız.", + }; } catch (err) { const detail = err instanceof Error ? err.message : "Bilinmeyen hata"; return { ok: false, message: `Kayıt başarısız: ${detail}` }; diff --git a/app/admin/(protected)/blog/[id]/edit/page.tsx b/app/admin/(protected)/blog/[id]/edit/page.tsx index 141811d..e00f51a 100644 --- a/app/admin/(protected)/blog/[id]/edit/page.tsx +++ b/app/admin/(protected)/blog/[id]/edit/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; import { getRow } from "@/lib/data"; -import { TABLES } from "@/lib/appwrite-server"; +import { TABLES } from "@/lib/appwrite-rest"; import type { BlogPostRow } from "@/lib/types"; import { BlogForm } from "../../form"; diff --git a/app/admin/(protected)/hizmetler/[id]/edit/page.tsx b/app/admin/(protected)/hizmetler/[id]/edit/page.tsx index fbffe90..6c4f1b8 100644 --- a/app/admin/(protected)/hizmetler/[id]/edit/page.tsx +++ b/app/admin/(protected)/hizmetler/[id]/edit/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; import { getRow } from "@/lib/data"; -import { TABLES } from "@/lib/appwrite-server"; +import { TABLES } from "@/lib/appwrite-rest"; import type { ServiceRow } from "@/lib/types"; import { ServiceForm } from "../../form"; diff --git a/app/admin/(protected)/medya/page.tsx b/app/admin/(protected)/medya/page.tsx index 971864a..63f8247 100644 --- a/app/admin/(protected)/medya/page.tsx +++ b/app/admin/(protected)/medya/page.tsx @@ -1,7 +1,6 @@ import { PageHeader } from "@/components/admin/form"; -import { MEDIA_BUCKET_ID, userStorage } from "@/lib/appwrite-server"; +import { MEDIA_BUCKET_ID, Q, storage } from "@/lib/appwrite-rest"; import { requireSessionSecret } from "@/lib/auth"; -import { Query } from "appwrite"; import { Upload } from "lucide-react"; import { DeleteButton } from "@/components/admin/delete-button"; import { uploadMedia, deleteMediaFile } from "@/lib/admin-actions"; @@ -9,10 +8,11 @@ import { uploadMedia, deleteMediaFile } from "@/lib/admin-actions"; async function listFiles() { try { const secret = await requireSessionSecret(); - const res = await userStorage(secret).listFiles({ - bucketId: MEDIA_BUCKET_ID, - queries: [Query.orderDesc("$createdAt"), Query.limit(100)], - }); + const res = await storage.listFiles( + MEDIA_BUCKET_ID, + [Q.orderDesc("$createdAt"), Q.limit(100)], + secret, + ); return res.files; } catch { return []; @@ -20,7 +20,7 @@ async function listFiles() { } function fileViewUrl(id: string) { - return `${process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT}/storage/buckets/${MEDIA_BUCKET_ID}/files/${id}/view?project=${process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID}`; + return storage.fileViewUrl(MEDIA_BUCKET_ID, id); } export default async function MediaPage() { diff --git a/app/admin/(protected)/page.tsx b/app/admin/(protected)/page.tsx index 91f0d83..8bdc84d 100644 --- a/app/admin/(protected)/page.tsx +++ b/app/admin/(protected)/page.tsx @@ -1,17 +1,17 @@ import Link from "next/link"; import { ArrowRight } from "lucide-react"; -import { Query } from "appwrite"; -import { DATABASE_ID, TABLES, userDB } from "@/lib/appwrite-server"; +import { DATABASE_ID, Q, TABLES, tablesDB } from "@/lib/appwrite-rest"; import { requireSessionSecret } from "@/lib/auth"; async function safeCount(tableId: string, queries: string[] = []) { try { const secret = await requireSessionSecret(); - const res = await userDB(secret).listRows({ - databaseId: DATABASE_ID, + const res = await tablesDB.listRows( + DATABASE_ID, tableId, - queries: [...queries, Query.limit(1)], - }); + [...queries, Q.limit(1)], + secret, + ); return res.total ?? 0; } catch { return 0; @@ -21,12 +21,12 @@ async function safeCount(tableId: string, queries: string[] = []) { export default async function AdminDashboard() { const [posts, drafts, services, projects, testimonials, newMessages, totalMessages] = await Promise.all([ - safeCount(TABLES.blogPosts, [Query.equal("status", "published")]), - safeCount(TABLES.blogPosts, [Query.equal("status", "draft")]), + safeCount(TABLES.blogPosts, [Q.equal("status", "published")]), + safeCount(TABLES.blogPosts, [Q.equal("status", "draft")]), safeCount(TABLES.services), safeCount(TABLES.projects), safeCount(TABLES.testimonials), - safeCount(TABLES.contactMessages, [Query.equal("status", "new")]), + safeCount(TABLES.contactMessages, [Q.equal("status", "new")]), safeCount(TABLES.contactMessages), ]); diff --git a/app/admin/(protected)/projeler/[id]/edit/page.tsx b/app/admin/(protected)/projeler/[id]/edit/page.tsx index ae6fbb8..f2323f3 100644 --- a/app/admin/(protected)/projeler/[id]/edit/page.tsx +++ b/app/admin/(protected)/projeler/[id]/edit/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; import { getRow } from "@/lib/data"; -import { TABLES } from "@/lib/appwrite-server"; +import { TABLES } from "@/lib/appwrite-rest"; import type { ProjectRow } from "@/lib/types"; import { ProjectForm } from "../../form"; diff --git a/app/admin/(protected)/referanslar/[id]/edit/page.tsx b/app/admin/(protected)/referanslar/[id]/edit/page.tsx index 51b2d58..49aec98 100644 --- a/app/admin/(protected)/referanslar/[id]/edit/page.tsx +++ b/app/admin/(protected)/referanslar/[id]/edit/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; import { getRow } from "@/lib/data"; -import { TABLES } from "@/lib/appwrite-server"; +import { TABLES } from "@/lib/appwrite-rest"; import type { TestimonialRow } from "@/lib/types"; import { TestimonialForm } from "../../form"; diff --git a/app/admin/(protected)/seo/[id]/edit/page.tsx b/app/admin/(protected)/seo/[id]/edit/page.tsx index 9734edb..384a8d7 100644 --- a/app/admin/(protected)/seo/[id]/edit/page.tsx +++ b/app/admin/(protected)/seo/[id]/edit/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; import { getRow } from "@/lib/data"; -import { TABLES } from "@/lib/appwrite-server"; +import { TABLES } from "@/lib/appwrite-rest"; import type { SeoPageRow } from "@/lib/types"; import { SeoPageForm } from "../../page-form"; diff --git a/app/admin/login/actions.ts b/app/admin/login/actions.ts index bfd603a..4dc2d91 100644 --- a/app/admin/login/actions.ts +++ b/app/admin/login/actions.ts @@ -2,14 +2,27 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -import { Account, Client } from "appwrite"; +import { account, AppwriteError } from "@/lib/appwrite-rest"; import { SESSION_COOKIE } from "@/lib/auth"; -const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!; -const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!; - export type LoginState = { error?: string }; +function friendly(err: unknown): string { + if (err instanceof AppwriteError) { + switch (err.type) { + case "user_invalid_credentials": + return "E-posta veya şifre hatalı."; + case "user_blocked": + return "Hesabınız engellenmiş."; + case "general_rate_limit_exceeded": + return "Çok fazla deneme. Birkaç dakika sonra tekrar deneyin."; + default: + return err.message; + } + } + return err instanceof Error ? err.message : "Giriş başarısız"; +} + export async function loginAction( _prev: LoginState | undefined, formData: FormData, @@ -19,28 +32,35 @@ export async function loginAction( if (!email || !password) return { error: "E-posta ve şifre zorunlu" }; + let session; try { - const client = new Client().setEndpoint(endpoint).setProject(projectId); - const account = new Account(client); - const session = await account.createEmailPasswordSession({ email, password }); - const store = await cookies(); - store.set(SESSION_COOKIE, session.secret, { - httpOnly: true, - sameSite: "lax", - secure: process.env.NODE_ENV === "production", - path: "/", - expires: new Date(session.expire), - }); + session = await account.createEmailPasswordSession(email, password); } catch (err) { - const msg = err instanceof Error ? err.message : "Giriş başarısız"; - return { error: msg }; + return { error: friendly(err) }; } + const store = await cookies(); + store.set(SESSION_COOKIE, session.secret, { + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + path: "/", + expires: new Date(session.expire), + }); + redirect("/admin"); } export async function logoutAction() { const store = await cookies(); + const secret = store.get(SESSION_COOKIE)?.value; + if (secret) { + try { + await account.deleteSession("current", secret); + } catch { + // ignore — cookie is cleared anyway + } + } store.delete(SESSION_COOKIE); redirect("/admin/login"); } diff --git a/lib/admin-actions.ts b/lib/admin-actions.ts index ef0b800..5cfddd4 100644 --- a/lib/admin-actions.ts +++ b/lib/admin-actions.ts @@ -1,25 +1,16 @@ "use server"; import { revalidatePath } from "next/cache"; -import { ID } from "appwrite"; import { DATABASE_ID, + ID, MEDIA_BUCKET_ID, + storage, TABLES, - userDB, - userStorage, -} from "@/lib/appwrite-server"; + tablesDB, +} from "@/lib/appwrite-rest"; import { requireSessionSecret } from "@/lib/auth"; -async function db() { - const secret = await requireSessionSecret(); - return userDB(secret); -} -async function storage() { - const secret = await requireSessionSecret(); - return userStorage(secret); -} - function slugify(s: string) { return s .toLowerCase() @@ -55,37 +46,34 @@ function strArr(v: FormDataEntryValue | null) { .filter(Boolean); } -// ─── Media upload ──────────────────────────────────────────────── +// ─── Media ─────────────────────────────────────────────────────── export async function uploadMedia(formData: FormData): Promise { + const secret = await requireSessionSecret(); const file = formData.get("file"); if (!(file instanceof File) || file.size === 0) { throw new Error("Dosya seçilmedi"); } - const s = await storage(); - await s.createFile({ - bucketId: MEDIA_BUCKET_ID, - fileId: ID.unique(), - file, - }); + await storage.createFile(MEDIA_BUCKET_ID, ID.unique(), file, secret); revalidatePath("/admin/medya"); } export async function deleteMediaFile(fileId: string) { - const s = await storage(); - await s.deleteFile({ bucketId: MEDIA_BUCKET_ID, fileId }); + const secret = await requireSessionSecret(); + await storage.deleteFile(MEDIA_BUCKET_ID, fileId, secret); revalidatePath("/admin/medya"); } // ─── Blog ──────────────────────────────────────────────────────── export async function saveBlogPost(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = str(formData.get("id")); const title = str(formData.get("title")); if (!title) throw new Error("Başlık zorunlu"); const slug = str(formData.get("slug")) || slugify(title); - const status = (str(formData.get("status")) ?? "draft") as "draft" | "published"; + const status = + (str(formData.get("status")) ?? "draft") as "draft" | "published"; const data = { slug, @@ -107,19 +95,15 @@ export async function saveBlogPost(formData: FormData) { }; if (id) { - await d.updateRow({ - databaseId: DATABASE_ID, - tableId: TABLES.blogPosts, - rowId: id, - data, - }); + await tablesDB.updateRow(DATABASE_ID, TABLES.blogPosts, id, data, secret); } else { - await d.createRow({ - databaseId: DATABASE_ID, - tableId: TABLES.blogPosts, - rowId: ID.unique(), + await tablesDB.createRow( + DATABASE_ID, + TABLES.blogPosts, + ID.unique(), data, - }); + secret, + ); } revalidatePath("/admin/blog"); revalidatePath("/blog"); @@ -127,13 +111,9 @@ export async function saveBlogPost(formData: FormData) { } export async function deleteBlogPost(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = String(formData.get("id")); - await d.deleteRow({ - databaseId: DATABASE_ID, - tableId: TABLES.blogPosts, - rowId: id, - }); + await tablesDB.deleteRow(DATABASE_ID, TABLES.blogPosts, id, secret); revalidatePath("/admin/blog"); revalidatePath("/blog"); } @@ -141,7 +121,7 @@ export async function deleteBlogPost(formData: FormData) { // ─── Services ──────────────────────────────────────────────────── export async function saveService(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = str(formData.get("id")); const title = str(formData.get("title")); if (!title) throw new Error("Başlık zorunlu"); @@ -158,19 +138,15 @@ export async function saveService(formData: FormData) { featured: bool(formData.get("featured")), }; if (id) { - await d.updateRow({ - databaseId: DATABASE_ID, - tableId: TABLES.services, - rowId: id, - data, - }); + await tablesDB.updateRow(DATABASE_ID, TABLES.services, id, data, secret); } else { - await d.createRow({ - databaseId: DATABASE_ID, - tableId: TABLES.services, - rowId: slug, + await tablesDB.createRow( + DATABASE_ID, + TABLES.services, + slug, data, - }); + secret, + ); } revalidatePath("/admin/hizmetler"); revalidatePath("/hizmetler"); @@ -178,13 +154,9 @@ export async function saveService(formData: FormData) { } export async function deleteService(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = String(formData.get("id")); - await d.deleteRow({ - databaseId: DATABASE_ID, - tableId: TABLES.services, - rowId: id, - }); + await tablesDB.deleteRow(DATABASE_ID, TABLES.services, id, secret); revalidatePath("/admin/hizmetler"); revalidatePath("/hizmetler"); } @@ -192,7 +164,7 @@ export async function deleteService(formData: FormData) { // ─── Projects ──────────────────────────────────────────────────── export async function saveProject(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = str(formData.get("id")); const title = str(formData.get("title")); if (!title) throw new Error("Başlık zorunlu"); @@ -213,19 +185,15 @@ export async function saveProject(formData: FormData) { }; if (id) { - await d.updateRow({ - databaseId: DATABASE_ID, - tableId: TABLES.projects, - rowId: id, - data, - }); + await tablesDB.updateRow(DATABASE_ID, TABLES.projects, id, data, secret); } else { - await d.createRow({ - databaseId: DATABASE_ID, - tableId: TABLES.projects, - rowId: ID.unique(), + await tablesDB.createRow( + DATABASE_ID, + TABLES.projects, + ID.unique(), data, - }); + secret, + ); } revalidatePath("/admin/projeler"); revalidatePath("/projeler"); @@ -233,13 +201,9 @@ export async function saveProject(formData: FormData) { } export async function deleteProject(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = String(formData.get("id")); - await d.deleteRow({ - databaseId: DATABASE_ID, - tableId: TABLES.projects, - rowId: id, - }); + await tablesDB.deleteRow(DATABASE_ID, TABLES.projects, id, secret); revalidatePath("/admin/projeler"); revalidatePath("/projeler"); } @@ -247,7 +211,7 @@ export async function deleteProject(formData: FormData) { // ─── Testimonials ──────────────────────────────────────────────── export async function saveTestimonial(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = str(formData.get("id")); const name = str(formData.get("name")); if (!name) throw new Error("Ad zorunlu"); @@ -266,39 +230,31 @@ export async function saveTestimonial(formData: FormData) { }; if (id) { - await d.updateRow({ - databaseId: DATABASE_ID, - tableId: TABLES.testimonials, - rowId: id, - data, - }); + await tablesDB.updateRow(DATABASE_ID, TABLES.testimonials, id, data, secret); } else { - await d.createRow({ - databaseId: DATABASE_ID, - tableId: TABLES.testimonials, - rowId: ID.unique(), + await tablesDB.createRow( + DATABASE_ID, + TABLES.testimonials, + ID.unique(), data, - }); + secret, + ); } revalidatePath("/admin/referanslar"); revalidatePath("/"); } export async function deleteTestimonial(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = String(formData.get("id")); - await d.deleteRow({ - databaseId: DATABASE_ID, - tableId: TABLES.testimonials, - rowId: id, - }); + await tablesDB.deleteRow(DATABASE_ID, TABLES.testimonials, id, secret); revalidatePath("/admin/referanslar"); } -// ─── SEO Settings (singleton) ──────────────────────────────────── +// ─── SEO Settings ──────────────────────────────────────────────── export async function saveSeoSettings(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const data = { site_name: str(formData.get("site_name")), site_description: str(formData.get("site_description")), @@ -311,28 +267,30 @@ export async function saveSeoSettings(formData: FormData) { gtm_id: str(formData.get("gtm_id")), }; try { - await d.updateRow({ - databaseId: DATABASE_ID, - tableId: TABLES.seoSettings, - rowId: "global", + await tablesDB.updateRow( + DATABASE_ID, + TABLES.seoSettings, + "global", data, - }); + secret, + ); } catch { - await d.createRow({ - databaseId: DATABASE_ID, - tableId: TABLES.seoSettings, - rowId: "global", + await tablesDB.createRow( + DATABASE_ID, + TABLES.seoSettings, + "global", data, - }); + secret, + ); } revalidatePath("/", "layout"); revalidatePath("/admin/seo"); } -// ─── SEO Page (per path) ───────────────────────────────────────── +// ─── SEO Page ──────────────────────────────────────────────────── export async function saveSeoPage(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = str(formData.get("id")); const path = str(formData.get("path")); if (!path) throw new Error("Path zorunlu"); @@ -347,61 +305,55 @@ export async function saveSeoPage(formData: FormData) { }; if (id) { - await d.updateRow({ - databaseId: DATABASE_ID, - tableId: TABLES.seoPages, - rowId: id, - data, - }); + await tablesDB.updateRow(DATABASE_ID, TABLES.seoPages, id, data, secret); } else { - await d.createRow({ - databaseId: DATABASE_ID, - tableId: TABLES.seoPages, - rowId: ID.unique(), + await tablesDB.createRow( + DATABASE_ID, + TABLES.seoPages, + ID.unique(), data, - }); + secret, + ); } revalidatePath(path); revalidatePath("/admin/seo"); } export async function deleteSeoPage(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = String(formData.get("id")); - await d.deleteRow({ - databaseId: DATABASE_ID, - tableId: TABLES.seoPages, - rowId: id, - }); + await tablesDB.deleteRow(DATABASE_ID, TABLES.seoPages, id, secret); revalidatePath("/admin/seo"); } -// ─── Contact messages ──────────────────────────────────────────── +// ─── Contact ───────────────────────────────────────────────────── export async function updateMessageStatus(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = String(formData.get("id")); const status = String(formData.get("status")) as | "new" | "read" | "replied" | "archived"; - await d.updateRow({ - databaseId: DATABASE_ID, - tableId: TABLES.contactMessages, - rowId: id, - data: { status }, - }); + await tablesDB.updateRow( + DATABASE_ID, + TABLES.contactMessages, + id, + { status }, + secret, + ); revalidatePath("/admin/iletisim"); } export async function deleteMessage(formData: FormData) { - const d = await db(); + const secret = await requireSessionSecret(); const id = String(formData.get("id")); - await d.deleteRow({ - databaseId: DATABASE_ID, - tableId: TABLES.contactMessages, - rowId: id, - }); + await tablesDB.deleteRow( + DATABASE_ID, + TABLES.contactMessages, + id, + secret, + ); revalidatePath("/admin/iletisim"); } diff --git a/lib/appwrite-rest.ts b/lib/appwrite-rest.ts new file mode 100644 index 0000000..cea7b31 --- /dev/null +++ b/lib/appwrite-rest.ts @@ -0,0 +1,276 @@ +import "server-only"; + +const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!; +const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!; + +if (!ENDPOINT || !PROJECT_ID) { + throw new Error( + "Missing NEXT_PUBLIC_APPWRITE_ENDPOINT or NEXT_PUBLIC_APPWRITE_PROJECT_ID", + ); +} + +export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!; +export const MEDIA_BUCKET_ID = + process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID ?? "kovak-yazilim-media"; + +export const TABLES = { + contactMessages: "contact_messages", + services: "services", + projects: "projects", + blogPosts: "blog_posts", + testimonials: "testimonials", + seoPages: "seo_pages", + seoSettings: "seo_settings", +} as const; + +export class AppwriteError extends Error { + code: number; + type?: string; + constructor(message: string, code: number, type?: string) { + super(message); + this.name = "AppwriteError"; + this.code = code; + this.type = type; + } +} + +type FetchOpts = { + method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + body?: unknown; + session?: string; + query?: Record; + formData?: FormData; +}; + +async function aw(path: string, opts: FetchOpts = {}): Promise { + const url = new URL(ENDPOINT + path); + if (opts.query) { + for (const [k, v] of Object.entries(opts.query)) { + if (v === undefined) continue; + if (Array.isArray(v)) v.forEach((x) => url.searchParams.append(k + "[]", String(x))); + else url.searchParams.set(k, String(v)); + } + } + + const headers: Record = { + "X-Appwrite-Project": PROJECT_ID, + "X-Appwrite-Response-Format": "1.9.0", + }; + if (opts.session) headers["X-Appwrite-Session"] = opts.session; + + let body: BodyInit | undefined; + if (opts.formData) { + body = opts.formData; + } else if (opts.body !== undefined) { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(opts.body); + } + + const res = await fetch(url, { + method: opts.method ?? "GET", + headers, + body, + cache: "no-store", + }); + + if (!res.ok) { + let data: { message?: string; type?: string } = {}; + try { + data = await res.json(); + } catch { + /* ignore */ + } + throw new AppwriteError( + data.message || `HTTP ${res.status}`, + res.status, + data.type, + ); + } + + const ct = res.headers.get("content-type") ?? ""; + if (ct.includes("application/json")) { + return (await res.json()) as T; + } + return undefined as T; +} + +// ─── Query helpers ─────────────────────────────────────────────── + +export const Q = { + equal: (attr: string, value: string | number | boolean | string[]) => + JSON.stringify({ + method: "equal", + attribute: attr, + values: Array.isArray(value) ? value : [value], + }), + notEqual: (attr: string, value: string | number) => + JSON.stringify({ method: "notEqual", attribute: attr, values: [value] }), + orderAsc: (attr: string) => + JSON.stringify({ method: "orderAsc", attribute: attr }), + orderDesc: (attr: string) => + JSON.stringify({ method: "orderDesc", attribute: attr }), + limit: (n: number) => JSON.stringify({ method: "limit", values: [n] }), + offset: (n: number) => JSON.stringify({ method: "offset", values: [n] }), +}; + +// ─── Types ─────────────────────────────────────────────────────── + +export interface AwRow { + $id: string; + $createdAt: string; + $updatedAt: string; + [k: string]: unknown; +} +export interface AwListResponse { + total: number; + rows: T[]; +} +export interface AwFile { + $id: string; + name: string; + sizeOriginal: number; + mimeType: string; + $createdAt: string; +} +export interface AwSession { + $id: string; + secret: string; + expire: string; + userId: string; + provider: string; +} +export interface AwUser { + $id: string; + email: string; + name: string; + status: boolean; +} + +// ─── Account ───────────────────────────────────────────────────── + +export const account = { + createEmailPasswordSession(email: string, password: string) { + return aw("/account/sessions/email", { + method: "POST", + body: { email, password }, + }); + }, + get(session: string) { + return aw("/account", { session }); + }, + deleteSession(sessionId: string, session: string) { + return aw(`/account/sessions/${sessionId}`, { + method: "DELETE", + session, + }); + }, +}; + +// ─── TablesDB ──────────────────────────────────────────────────── + +export const tablesDB = { + listRows( + databaseId: string, + tableId: string, + queries: string[] = [], + session?: string, + ) { + return aw>( + `/tablesdb/${databaseId}/tables/${tableId}/rows`, + { query: { queries }, session }, + ); + }, + getRow( + databaseId: string, + tableId: string, + rowId: string, + session?: string, + ) { + return aw(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, { + session, + }); + }, + createRow( + databaseId: string, + tableId: string, + rowId: string, + data: Record, + session?: string, + ) { + return aw(`/tablesdb/${databaseId}/tables/${tableId}/rows`, { + method: "POST", + body: { rowId, data }, + session, + }); + }, + updateRow( + databaseId: string, + tableId: string, + rowId: string, + data: Record, + session?: string, + ) { + return aw(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, { + method: "PATCH", + body: { data }, + session, + }); + }, + deleteRow( + databaseId: string, + tableId: string, + rowId: string, + session?: string, + ) { + return aw(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, { + method: "DELETE", + session, + }); + }, +}; + +// ─── Storage ───────────────────────────────────────────────────── + +export const storage = { + listFiles(bucketId: string, queries: string[] = [], session?: string) { + return aw<{ total: number; files: AwFile[] }>( + `/storage/buckets/${bucketId}/files`, + { query: { queries }, session }, + ); + }, + createFile( + bucketId: string, + fileId: string, + file: File, + session?: string, + ) { + const fd = new FormData(); + fd.append("fileId", fileId); + fd.append("file", file); + return aw(`/storage/buckets/${bucketId}/files`, { + method: "POST", + formData: fd, + session, + }); + }, + deleteFile(bucketId: string, fileId: string, session?: string) { + return aw(`/storage/buckets/${bucketId}/files/${fileId}`, { + method: "DELETE", + session, + }); + }, + fileViewUrl(bucketId: string, fileId: string) { + return `${ENDPOINT}/storage/buckets/${bucketId}/files/${fileId}/view?project=${PROJECT_ID}`; + }, +}; + +// ─── ID generator (Appwrite "unique()") ────────────────────────── + +export const ID = { + unique() { + return "unique()"; + }, + custom(id: string) { + return id; + }, +}; diff --git a/lib/appwrite-server.ts b/lib/appwrite-server.ts deleted file mode 100644 index 94dd42d..0000000 --- a/lib/appwrite-server.ts +++ /dev/null @@ -1,45 +0,0 @@ -import "server-only"; -import { Account, Client, Storage, TablesDB } from "appwrite"; - -const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!; -const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!; - -export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!; -export const MEDIA_BUCKET_ID = - process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID ?? "kovak-yazilim-media"; - -export const TABLES = { - contactMessages: "contact_messages", - services: "services", - projects: "projects", - blogPosts: "blog_posts", - testimonials: "testimonials", - seoPages: "seo_pages", - seoSettings: "seo_settings", -} as const; - -function newClient() { - return new Client().setEndpoint(endpoint).setProject(projectId); -} - -export function publicClient() { - return newClient(); -} - -export function sessionClient(sessionSecret: string) { - return newClient().setSession(sessionSecret); -} - -export const publicDB = new TablesDB(publicClient()); -export const publicStorage = new Storage(publicClient()); -export const publicAccount = new Account(publicClient()); - -export function userDB(secret: string) { - return new TablesDB(sessionClient(secret)); -} -export function userStorage(secret: string) { - return new Storage(sessionClient(secret)); -} -export function userAccount(secret: string) { - return new Account(sessionClient(secret)); -} diff --git a/lib/appwrite.ts b/lib/appwrite.ts deleted file mode 100644 index df8f70c..0000000 --- a/lib/appwrite.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Client, TablesDB } from "appwrite"; - -const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!; -const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!; - -export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!; - -export const TABLES = { - contactMessages: "contact_messages", - services: "services", - projects: "projects", -} as const; - -export const client = new Client().setEndpoint(endpoint).setProject(projectId); - -export const tablesDB = new TablesDB(client); diff --git a/lib/auth.ts b/lib/auth.ts index 03167dc..87d6e16 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,7 +1,7 @@ import "server-only"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -import { userAccount } from "@/lib/appwrite-server"; +import { account } from "@/lib/appwrite-rest"; export const SESSION_COOKIE = "kovak_session"; @@ -14,7 +14,7 @@ export async function getCurrentUser() { const secret = await getSessionSecret(); if (!secret) return null; try { - return await userAccount(secret).get(); + return await account.get(secret); } catch { return null; } diff --git a/lib/data.ts b/lib/data.ts index 69b1593..d8e4fdb 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -1,6 +1,6 @@ import "server-only"; -import { Query } from "appwrite"; -import { publicDB, DATABASE_ID, TABLES } from "@/lib/appwrite-server"; +import { DATABASE_ID, Q, TABLES, tablesDB } from "@/lib/appwrite-rest"; +import { getSessionSecret } from "@/lib/auth"; import type { BlogPostRow, ContactMessageRow, @@ -13,98 +13,114 @@ import type { async function safeList(tableId: string, queries: string[]): Promise { try { - const res = await publicDB.listRows({ - databaseId: DATABASE_ID, + const res = await tablesDB.listRows(DATABASE_ID, tableId, queries); + return res.rows; + } catch { + return []; + } +} + +async function safeListAuth(tableId: string, queries: string[]): Promise { + try { + const secret = await getSessionSecret(); + const res = await tablesDB.listRows( + DATABASE_ID, tableId, queries, - }); - return res.rows as T[]; + secret ?? undefined, + ); + return res.rows; } catch { return []; } } export async function listServices(opts?: { featured?: boolean }) { - const q = [Query.orderAsc("order"), Query.limit(50)]; - if (opts?.featured) q.unshift(Query.equal("featured", true)); + const q = [Q.orderAsc("order"), Q.limit(50)]; + if (opts?.featured) q.unshift(Q.equal("featured", true)); return safeList(TABLES.services, q); } export async function listProjects(opts?: { featured?: boolean; limit?: number }) { - const q = [Query.orderDesc("year"), Query.limit(opts?.limit ?? 50)]; - if (opts?.featured) q.unshift(Query.equal("featured", true)); + const q = [Q.orderDesc("year"), Q.limit(opts?.limit ?? 50)]; + if (opts?.featured) q.unshift(Q.equal("featured", true)); return safeList(TABLES.projects, q); } export async function listPublishedPosts(opts?: { limit?: number }) { return safeList(TABLES.blogPosts, [ - Query.equal("status", "published"), - Query.orderDesc("published_at"), - Query.limit(opts?.limit ?? 50), + Q.equal("status", "published"), + Q.orderDesc("published_at"), + Q.limit(opts?.limit ?? 50), ]); } export async function listAllPosts() { - return safeList(TABLES.blogPosts, [ - Query.orderDesc("$createdAt"), - Query.limit(200), + return safeListAuth(TABLES.blogPosts, [ + Q.orderDesc("$createdAt"), + Q.limit(200), ]); } export async function getPostBySlug(slug: string): Promise { const res = await safeList(TABLES.blogPosts, [ - Query.equal("slug", slug), - Query.limit(1), + Q.equal("slug", slug), + Q.limit(1), ]); return res[0] ?? null; } export async function listTestimonials(opts?: { featured?: boolean }) { - const q = [Query.orderAsc("order"), Query.limit(50)]; - if (opts?.featured) q.unshift(Query.equal("featured", true)); + const q = [Q.orderAsc("order"), Q.limit(50)]; + if (opts?.featured) q.unshift(Q.equal("featured", true)); return safeList(TABLES.testimonials, q); } export async function listMessages(status?: ContactMessageRow["status"]) { - const q = [Query.orderDesc("$createdAt"), Query.limit(200)]; - if (status) q.unshift(Query.equal("status", status)); - return safeList(TABLES.contactMessages, q); + const q = [Q.orderDesc("$createdAt"), Q.limit(200)]; + if (status) q.unshift(Q.equal("status", status)); + return safeListAuth(TABLES.contactMessages, q); } export async function getSeoPage(path: string): Promise { const res = await safeList(TABLES.seoPages, [ - Query.equal("path", path), - Query.limit(1), + Q.equal("path", path), + Q.limit(1), ]); return res[0] ?? null; } export async function listSeoPages() { - return safeList(TABLES.seoPages, [ - Query.orderAsc("path"), - Query.limit(200), + return safeListAuth(TABLES.seoPages, [ + Q.orderAsc("path"), + Q.limit(200), ]); } export async function getSeoSettings(): Promise { try { - return (await publicDB.getRow({ - databaseId: DATABASE_ID, - tableId: TABLES.seoSettings, - rowId: "global", - })) as unknown as SeoSettingsRow; + return await tablesDB.getRow( + DATABASE_ID, + TABLES.seoSettings, + "global", + ); } catch { return null; } } -export async function getRow(tableId: string, rowId: string): Promise { +export async function getRow( + tableId: string, + rowId: string, +): Promise { try { - return (await publicDB.getRow({ - databaseId: DATABASE_ID, + const secret = await getSessionSecret(); + return await tablesDB.getRow( + DATABASE_ID, tableId, rowId, - })) as unknown as T; + secret ?? undefined, + ); } catch { return null; } diff --git a/lib/types.ts b/lib/types.ts index 2376c37..d231210 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,8 @@ -import type { Models } from "appwrite"; +import type { AwRow } from "@/lib/appwrite-rest"; -export interface ServiceRow extends Models.Row { +export type Row = AwRow; + +export interface ServiceRow extends AwRow { slug: string; title: string; description: string; @@ -9,7 +11,7 @@ export interface ServiceRow extends Models.Row { featured?: boolean | null; } -export interface ProjectRow extends Models.Row { +export interface ProjectRow extends AwRow { slug: string; title: string; description: string; @@ -21,7 +23,7 @@ export interface ProjectRow extends Models.Row { featured?: boolean | null; } -export interface BlogPostRow extends Models.Row { +export interface BlogPostRow extends AwRow { slug: string; title: string; excerpt?: string | null; @@ -37,7 +39,7 @@ export interface BlogPostRow extends Models.Row { seo_image?: string | null; } -export interface TestimonialRow extends Models.Row { +export interface TestimonialRow extends AwRow { name: string; role?: string | null; company?: string | null; @@ -48,7 +50,7 @@ export interface TestimonialRow extends Models.Row { featured?: boolean | null; } -export interface SeoPageRow extends Models.Row { +export interface SeoPageRow extends AwRow { path: string; title?: string | null; description?: string | null; @@ -57,7 +59,7 @@ export interface SeoPageRow extends Models.Row { noindex?: boolean | null; } -export interface SeoSettingsRow extends Models.Row { +export interface SeoSettingsRow extends AwRow { site_name?: string | null; site_description?: string | null; default_og_image?: string | null; @@ -69,7 +71,7 @@ export interface SeoSettingsRow extends Models.Row { gtm_id?: string | null; } -export interface ContactMessageRow extends Models.Row { +export interface ContactMessageRow extends AwRow { name: string; email: string; phone?: string | null; diff --git a/package-lock.json b/package-lock.json index 8332522..6ece288 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "kovak-yazilim", "version": "0.1.0", "dependencies": { - "appwrite": "^25.1.1", "lucide-react": "^1.16.0", "marked": "^18.0.4", "next": "16.2.6", @@ -1079,18 +1078,6 @@ "@types/react": "^19.2.0" } }, - "node_modules/appwrite": { - "version": "25.1.1", - "resolved": "https://registry.npmjs.org/appwrite/-/appwrite-25.1.1.tgz", - "integrity": "sha512-h+vVPErfCLZ9FCnWH1cZOphgX8MsXNNN0PvxCxoOCbqyX6XubtvvOEpp/BpVOAHp7jIDFEJu72ingBu9kh1vDQ==", - "license": "BSD-3-Clause", - "dependencies": { - "json-bigint": "1.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/baseline-browser-mapping": { "version": "2.10.31", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", @@ -1103,15 +1090,6 @@ "node": ">=6.0.0" } }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001793", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", @@ -1186,15 +1164,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", diff --git a/package.json b/package.json index 0057110..db42089 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "start": "next start" }, "dependencies": { - "appwrite": "^25.1.1", "lucide-react": "^1.16.0", "marked": "^18.0.4", "next": "16.2.6",